16
16
import com .google .common .annotations .VisibleForTesting ;
17
17
import com .google .common .primitives .Longs ;
18
18
import io .airlift .units .DataSize ;
19
- import io .airlift .units .DataSize .Unit ;
20
19
21
20
import java .util .ArrayList ;
22
21
import java .util .Iterator ;
28
27
import static com .google .common .base .Preconditions .checkArgument ;
29
28
import static com .google .common .base .Preconditions .checkState ;
30
29
import static com .google .common .collect .ImmutableList .toImmutableList ;
30
+ import static io .airlift .units .DataSize .Unit .MEGABYTE ;
31
31
import static java .lang .Math .toIntExact ;
32
32
import static java .util .Objects .requireNonNull ;
33
33
34
+ /**
35
+ * DictionaryCompressionOptimizer has 2 objectives:
36
+ * 1) Bound the dictionary memory of the reader, when all columns are read. Reader's dictionary memory
37
+ * should not exceed the dictionaryMemoryMaxBytesHigh.
38
+ * 2) When dictionary encoding for a column produces size comparable to the direct encoding, choose
39
+ * direct encoding over dictionary encoding. Dictionary encoding/decoding is memory and CPU intensive,
40
+ * so for comparable column sizes, direct encoding is mostly better.
41
+ * <p>
42
+ * Note: Dictionary writer might use more memory as they over-allocate dictionary sizes as the writers
43
+ * build dictionary as they see new data. The hash tables implementation in the dictionary writer's allocate
44
+ * hash buckets in power of 2. So after a million entries, the overallocation consumes large amount of memory.
45
+ * <p>
46
+ * DictionaryCompressionOptimizer functionality can be controlled by the following configs to the constructor.
47
+ * <p>
48
+ * 1. dictionaryMemoryMaxBytes -> Max size of the dictionary when all columns are read. Note: Writer
49
+ * might consume more memory due to the over-allocation.
50
+ * <p>
51
+ * 2. dictionaryMemoryAlmostFullRangeBytes -> When the dictionary size exceeds dictionaryMaxMemoryBytes
52
+ * dictionary columns will be converted to direct to reduce the dictionary size. By setting a range
53
+ * the stripe can be flushed, before the dictionary is full. When dictionary size is higher than
54
+ * (dictionaryMemoryMaxBytes - dictionaryMemoryAlmostFullRangeBytes), it is considered almost full
55
+ * and is ready for flushing. This setting is defined as a delta on dictionaryMemoryMaxBytes for backward compatibility.
56
+ * <p>
57
+ * 3. dictionaryUsefulCheckColumnSizeBytes -> Columns start with dictionary encoding and when the dictionary memory
58
+ * is almost full, usefulness of the dictionary is measured. For large dictionaries (> 40 MB) the check
59
+ * might happen very late and large dictionary might cause writer to OOM due to writer over allocating for
60
+ * dictionary growth. When a dictionary for a column grows beyond the dictionaryUsefulCheckColumnSizeBytes the
61
+ * dictionary usefulness check will be performed and if dictionary is not useful, it will be converted to direct.
62
+ * <p>
63
+ * 4. dictionaryUsefulCheckPerChunkFrequency -> dictionaryUsefulCheck could be costly if performed on every chunk.
64
+ * The dictionaryUsefulCheck will be performed when a column dictionary is above the dictionaryUsefulCheckColumnSizeBytes
65
+ * and per every dictionaryUsefulCheckPerChunkFrequency chunks written.
66
+ */
34
67
public class DictionaryCompressionOptimizer
35
68
{
36
69
private static final double DICTIONARY_MIN_COMPRESSION_RATIO = 1.25 ;
37
70
38
- // Instead of waiting for the dictionary to fill completely, which would force a column into
39
- // direct mode, close the stripe early assuming it has hit the minimum row count.
40
- static final DataSize DICTIONARY_MEMORY_MAX_RANGE = new DataSize (4 , Unit .MEGABYTE );
41
-
42
- static final DataSize DIRECT_COLUMN_SIZE_RANGE = new DataSize (4 , Unit .MEGABYTE );
71
+ static final DataSize DIRECT_COLUMN_SIZE_RANGE = new DataSize (4 , MEGABYTE );
43
72
44
73
private final List <DictionaryColumnManager > allWriters ;
45
74
private final List <DictionaryColumnManager > directConversionCandidates = new ArrayList <>();
@@ -49,15 +78,21 @@ public class DictionaryCompressionOptimizer
49
78
private final int stripeMaxRowCount ;
50
79
private final int dictionaryMemoryMaxBytesLow ;
51
80
private final int dictionaryMemoryMaxBytesHigh ;
81
+ private final int dictionaryUsefulCheckColumnSizeBytes ;
82
+ private final int dictionaryUsefulCheckPerChunkFrequency ;
52
83
53
84
private int dictionaryMemoryBytes ;
85
+ private int dictionaryUsefulCheckCounter ;
54
86
55
87
public DictionaryCompressionOptimizer (
56
88
Set <? extends DictionaryColumn > writers ,
57
89
int stripeMinBytes ,
58
90
int stripeMaxBytes ,
59
91
int stripeMaxRowCount ,
60
- int dictionaryMemoryMaxBytes )
92
+ int dictionaryMemoryMaxBytes ,
93
+ int dictionaryMemoryAlmostFullRangeBytes ,
94
+ int dictionaryUsefulCheckColumnSizeBytes ,
95
+ int dictionaryUsefulCheckPerChunkFrequency )
61
96
{
62
97
requireNonNull (writers , "writers is null" );
63
98
this .allWriters = writers .stream ()
@@ -74,9 +109,14 @@ public DictionaryCompressionOptimizer(
74
109
this .stripeMaxRowCount = stripeMaxRowCount ;
75
110
76
111
checkArgument (dictionaryMemoryMaxBytes >= 0 , "dictionaryMemoryMaxBytes is negative" );
112
+ checkArgument (dictionaryMemoryAlmostFullRangeBytes >= 0 , "dictionaryMemoryRangeBytes is negative" );
77
113
this .dictionaryMemoryMaxBytesHigh = dictionaryMemoryMaxBytes ;
78
- this .dictionaryMemoryMaxBytesLow = ( int ) Math .max (dictionaryMemoryMaxBytes - DICTIONARY_MEMORY_MAX_RANGE . toBytes () , 0 );
114
+ this .dictionaryMemoryMaxBytesLow = Math .max (dictionaryMemoryMaxBytes - dictionaryMemoryAlmostFullRangeBytes , 0 );
79
115
116
+ checkArgument (dictionaryUsefulCheckPerChunkFrequency >= 0 , "dictionaryUsefulCheckPerChunkFrequency is negative" );
117
+ this .dictionaryUsefulCheckPerChunkFrequency = dictionaryUsefulCheckPerChunkFrequency ;
118
+
119
+ this .dictionaryUsefulCheckColumnSizeBytes = dictionaryUsefulCheckColumnSizeBytes ;
80
120
directConversionCandidates .addAll (allWriters );
81
121
}
82
122
@@ -87,12 +127,12 @@ public int getDictionaryMemoryBytes()
87
127
88
128
public boolean isFull (long bufferedBytes )
89
129
{
90
- // if the strip is big enough to flush, stop before we hit the absolute max, so we are
130
+ // if the stripe is big enough to flush, stop before we hit the absolute max, so we are
91
131
// not forced to convert a dictionary to direct to fit in memory
92
132
if (bufferedBytes > stripeMinBytes ) {
93
133
return dictionaryMemoryBytes > dictionaryMemoryMaxBytesLow ;
94
134
}
95
- // strip is small, grow to the high water mark (so at the very least we have more information)
135
+ // stripe is small, grow to the high watermark (so at the very least we have more information)
96
136
return dictionaryMemoryBytes > dictionaryMemoryMaxBytesHigh ;
97
137
}
98
138
@@ -107,30 +147,43 @@ public void reset()
107
147
public void finalOptimize (int bufferedBytes )
108
148
{
109
149
updateDirectConversionCandidates ();
110
- convertLowCompressionStreams (bufferedBytes );
150
+ convertLowCompressionStreams (true , bufferedBytes );
111
151
}
112
152
113
- public void optimize (int bufferedBytes , int stripeRowCount )
153
+ @ VisibleForTesting
154
+ boolean isUsefulCheckRequired (int dictionaryMemoryBytes )
114
155
{
115
- // recompute the dictionary memory usage
116
- dictionaryMemoryBytes = allWriters .stream ()
117
- .filter (writer -> !writer .isDirectEncoded ())
118
- .mapToInt (DictionaryColumnManager ::getDictionaryBytes )
119
- .sum ();
156
+ if (dictionaryMemoryBytes < dictionaryUsefulCheckColumnSizeBytes ) {
157
+ return false ;
158
+ }
120
159
121
- // update the dictionary growth history
122
- allWriters .stream ()
123
- .filter (writer -> !writer .isDirectEncoded ())
124
- .forEach (column -> column .updateHistory (stripeRowCount ));
160
+ dictionaryUsefulCheckCounter ++;
161
+ if (dictionaryUsefulCheckCounter == dictionaryUsefulCheckPerChunkFrequency ) {
162
+ dictionaryUsefulCheckCounter = 0 ;
163
+ return true ;
164
+ }
125
165
126
- if (dictionaryMemoryBytes <= dictionaryMemoryMaxBytesLow ) {
127
- return ;
166
+ return false ;
167
+ }
168
+
169
+ public void optimize (int bufferedBytes , int stripeRowCount )
170
+ {
171
+ // recompute the dictionary memory usage
172
+ int totalDictionaryBytes = 0 ;
173
+ for (DictionaryColumnManager writer : allWriters ) {
174
+ if (!writer .isDirectEncoded ()) {
175
+ totalDictionaryBytes += writer .getDictionaryBytes ();
176
+ writer .updateHistory (stripeRowCount );
177
+ }
128
178
}
179
+ dictionaryMemoryBytes = totalDictionaryBytes ;
129
180
130
- updateDirectConversionCandidates () ;
181
+ boolean isDictionaryAlmostFull = dictionaryMemoryBytes > dictionaryMemoryMaxBytesLow ;
131
182
132
- // before any further checks, convert all low compression streams
133
- bufferedBytes = convertLowCompressionStreams (bufferedBytes );
183
+ if (isDictionaryAlmostFull || isUsefulCheckRequired (dictionaryMemoryBytes )) {
184
+ updateDirectConversionCandidates ();
185
+ bufferedBytes = convertLowCompressionStreams (isDictionaryAlmostFull , bufferedBytes );
186
+ }
134
187
135
188
if (dictionaryMemoryBytes <= dictionaryMemoryMaxBytesLow || bufferedBytes >= stripeMaxBytes ) {
136
189
return ;
@@ -161,7 +214,7 @@ private void optimizeDictionaryColumns(int stripeRowCount, BufferedBytesCounter
161
214
return ;
162
215
}
163
216
164
- // if the stripe is larger then the minimum stripe size, we are not required to convert any more dictionary columns to direct
217
+ // if the stripe is larger than the minimum stripe size, we are not required to convert any more dictionary columns to direct
165
218
if (bufferedBytesCounter .getBufferedBytes () >= stripeMinBytes ) {
166
219
// check if we can get better compression by converting a dictionary column to direct. This can happen when then there are multiple
167
220
// dictionary columns and one does not compress well, so if we convert it to direct we can continue to use the existing dictionaries
@@ -196,27 +249,35 @@ private boolean convertDictionaryColumn(BufferedBytesCounter bufferedBytesCounte
196
249
}
197
250
198
251
@ VisibleForTesting
199
- int convertLowCompressionStreams (int bufferedBytes )
252
+ int convertLowCompressionStreams (boolean tryAllStreams , int bufferedBytes )
200
253
{
201
254
// convert all low compression column to direct
202
255
Iterator <DictionaryColumnManager > iterator = directConversionCandidates .iterator ();
203
256
while (iterator .hasNext ()) {
204
257
DictionaryColumnManager dictionaryWriter = iterator .next ();
205
- if (dictionaryWriter .getCompressionRatio () < DICTIONARY_MIN_COMPRESSION_RATIO ) {
206
- int columnBufferedBytes = toIntExact (dictionaryWriter .getBufferedBytes ());
207
- OptionalInt directBytes = tryConvertToDirect (dictionaryWriter , getMaxDirectBytes (bufferedBytes ));
208
- iterator .remove ();
209
- if (directBytes .isPresent ()) {
210
- bufferedBytes = bufferedBytes + directBytes .getAsInt () - columnBufferedBytes ;
211
- if (bufferedBytes >= stripeMaxBytes ) {
212
- return bufferedBytes ;
258
+ if (tryAllStreams || dictionaryWriter .getDictionaryBytes () >= dictionaryUsefulCheckColumnSizeBytes ) {
259
+ if (dictionaryWriter .getCompressionRatio () < DICTIONARY_MIN_COMPRESSION_RATIO ) {
260
+ int columnBufferedBytes = toIntExact (dictionaryWriter .getBufferedBytes ());
261
+ OptionalInt directBytes = tryConvertToDirect (dictionaryWriter , getMaxDirectBytes (bufferedBytes ));
262
+ iterator .remove ();
263
+ if (directBytes .isPresent ()) {
264
+ bufferedBytes = bufferedBytes + directBytes .getAsInt () - columnBufferedBytes ;
265
+ if (bufferedBytes >= stripeMaxBytes ) {
266
+ return bufferedBytes ;
267
+ }
213
268
}
214
269
}
215
270
}
216
271
}
217
272
return bufferedBytes ;
218
273
}
219
274
275
+ @ VisibleForTesting
276
+ List <DictionaryColumnManager > getDirectConversionCandidates ()
277
+ {
278
+ return directConversionCandidates ;
279
+ }
280
+
220
281
private void updateDirectConversionCandidates ()
221
282
{
222
283
// Writers can switch to Direct encoding internally. Remove them from direct conversion candidates.
@@ -255,14 +316,14 @@ private double currentCompressionRatio(int totalNonDictionaryBytes)
255
316
}
256
317
257
318
/**
258
- * Choose a dictionary column to convert to direct encoding. We do this by predicting the compression ration
319
+ * Choose a dictionary column to convert to direct encoding. We do this by predicting the compression ratio
259
320
* of the stripe if a singe column is flipped to direct. So for each column, we try to predict the row count
260
321
* when we will hit a stripe flush limit if that column were converted to direct. Once we know the row count, we
261
322
* calculate the predicted compression ratio.
262
323
*
263
324
* @param totalNonDictionaryBytes current size of the stripe without non-dictionary columns
264
325
* @param stripeRowCount current number of rows in the stripe
265
- * @return the column that would produce the best stripe compression ration if converted to direct
326
+ * @return the column that would produce the best stripe compression ratio if converted to direct
266
327
*/
267
328
private DictionaryCompressionProjection selectDictionaryColumnToConvert (int totalNonDictionaryBytes , int stripeRowCount )
268
329
{
@@ -305,7 +366,7 @@ private DictionaryCompressionProjection selectDictionaryColumnToConvert(int tota
305
366
long currentIndexBytes = totalDictionaryIndexBytes - column .getIndexBytes ();
306
367
long currentTotalBytes = currentRawBytes + currentDictionaryBytes + currentIndexBytes ;
307
368
308
- // estimate the size of each new row if we were convert this column to direct
369
+ // estimate the size of each new row if we were to convert this column to direct
309
370
double rawBytesPerFutureRow = totalNonDictionaryBytesPerRow + column .getRawBytesPerRow ();
310
371
double dictionaryBytesPerFutureRow = totalDictionaryBytesPerNewRow - column .getDictionaryBytesPerFutureRow ();
311
372
double indexBytesPerFutureRow = totalDictionaryIndexBytesPerRow - column .getIndexBytesPerRow ();
@@ -317,7 +378,7 @@ private DictionaryCompressionProjection selectDictionaryColumnToConvert(int tota
317
378
long rowsToStripeRowLimit = stripeMaxRowCount - stripeRowCount ;
318
379
long rowsToLimit = Longs .min (rowsToDictionaryMemoryLimit , rowsToStripeMemoryLimit , rowsToStripeRowLimit );
319
380
320
- // predict the compression ratio at that limit if we were convert this column to direct
381
+ // predict the compression ratio at that limit if we were to convert this column to direct
321
382
long predictedUncompressedSizeAtLimit = totalNonDictionaryBytes + totalDictionaryRawBytes + (totalUncompressedBytesPerRow * rowsToLimit );
322
383
long predictedCompressedSizeAtLimit = (long ) (currentTotalBytes + (totalBytesPerFutureRow * rowsToLimit ));
323
384
double predictedCompressionRatioAtLimit = 1.0 * predictedUncompressedSizeAtLimit / predictedCompressedSizeAtLimit ;
@@ -371,7 +432,8 @@ public interface DictionaryColumn
371
432
boolean isDirectEncoded ();
372
433
}
373
434
374
- private static class DictionaryColumnManager
435
+ @ VisibleForTesting
436
+ static class DictionaryColumnManager
375
437
{
376
438
private final DictionaryColumn dictionaryColumn ;
377
439
@@ -481,6 +543,12 @@ public boolean isDirectEncoded()
481
543
{
482
544
return dictionaryColumn .isDirectEncoded ();
483
545
}
546
+
547
+ @ VisibleForTesting
548
+ public DictionaryColumn getDictionaryColumn ()
549
+ {
550
+ return dictionaryColumn ;
551
+ }
484
552
}
485
553
486
554
private static class DictionaryCompressionProjection
0 commit comments