Skip to content

Commit c44a283

Browse files
committed
Fix regression in LZ4 compression performance since 10.6 (facebook#14017)
Summary: In RocksDB 10.6 with facebook#13805, due to inaccurate testing of an async system, it went undetected at the time that LZ4 compression was using more CPU despite making a change to reuse stream objects which dramatically improved LZ4HC compression efficiency. This change switches to using a basic LZ4 compress API which appears to be faster than all of these: * Legacy behavior of creating LZ4_stream_t for each compression * 10.6-10.7 behavior of re-using streams between compressions for the same file (with stream-as-WorkingArea) * using LZ4's extState APIs without streams (with extState-as-WorkingArea) (data not shown in below results) Also in this PR: more improvements to sst_dump --recompress, which is arguably the best SST construction benchmark right now since db_bench seems to be so noisy due to backgroun flush+compaction, even with no compaction (FIFO). Streamlined some output and added a SST read time test, mostly for decompression performance. Pull Request resolved: facebook#14017 Test Plan: Performance test using sst_dump --recompress with newer sst_dump back-ported to 10.5: ``` ./sst_dump --command=recompress --compression_types=kLZ4Compression test5.sst --compression_level_from=-6 --compression_level_to=-1 ``` and with default compression level. 10.5: ``` Cx level: -6 Cx size: 61608137 Write usec: 880404 Cx level: -5 Cx size: 60793749 Write usec: 840903 Cx level: -4 Cx size: 58134030 Write usec: 836365 Cx level: -3 Cx size: 55193773 Write usec: 857113 Cx level: -2 Cx size: 54013891 Write usec: 855642 Cx level: -1 Cx size: 50400393 Write usec: 865194 Cx level: 32767 Cx size: 50400393 Write usec: 886310 ``` Before this change (showing the regression, more time, from 10.6: ``` Cx level: -6 Cx size: 61608137 Write usec: 933448 Cx level: -5 Cx size: 60793749 Write usec: 893826 Cx level: -4 Cx size: 58134030 Write usec: 891138 Cx level: -3 Cx size: 55193773 Write usec: 898461 Cx level: -2 Cx size: 54013891 Write usec: 897485 Cx level: -1 Cx size: 50400393 Write usec: 936970 Cx level: 32767 Cx size: 50400393 Write usec: 958764 ``` After this change (faster than both the above): ``` Cx level: -6 Cx size: 63641883 Write usec: 874190 Cx level: -5 Cx size: 58860032 Write usec: 834662 Cx level: -4 Cx size: 57150188 Write usec: 832707 Cx level: -3 Cx size: 58791894 Write usec: 850305 Cx level: -2 Cx size: 53145885 Write usec: 839574 Cx level: -1 Cx size: 49809139 Write usec: 845639 Cx level: 32767 Cx size: 49809139 Write usec: 875199 ``` Similar tests with dictionary compression show essentially no difference (need to use stream APIs and reuse doesn't seem to matter). LZ4HC also unaffected (still improved vs. 10.5) Reviewed By: hx235 Differential Revision: D83722880 Pulled By: pdillinger fbshipit-source-id: 30149dd187686d5dd98321e6aa7d74bd7653a905
1 parent 12b6696 commit c44a283

5 files changed

Lines changed: 162 additions & 37 deletions

File tree

table/sst_file_dumper.cc

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,9 @@ Status SstFileDumper::DumpTable(const std::string& out_filename) {
231231
}
232232

233233
Status SstFileDumper::CalculateCompressedTableSize(
234-
const TableBuilderOptions& tb_options, TableProperties* props) {
234+
const TableBuilderOptions& tb_options, TableProperties* props,
235+
std::chrono::microseconds* write_time,
236+
std::chrono::microseconds* read_time) {
235237
std::unique_ptr<Env> env(NewMemEnv(options_.env));
236238
std::unique_ptr<WritableFileWriter> dest_writer;
237239
Status s =
@@ -240,6 +242,8 @@ Status SstFileDumper::CalculateCompressedTableSize(
240242
if (!s.ok()) {
241243
return s;
242244
}
245+
std::chrono::steady_clock::time_point start =
246+
std::chrono::steady_clock::now();
243247
std::unique_ptr<TableBuilder> table_builder{
244248
tb_options.moptions.table_factory->NewTableBuilder(tb_options,
245249
dest_writer.get())};
@@ -253,17 +257,69 @@ Status SstFileDumper::CalculateCompressedTableSize(
253257
if (!s.ok()) {
254258
return s;
255259
}
260+
iter.reset();
256261
s = table_builder->Finish();
262+
*write_time = std::chrono::duration_cast<std::chrono::microseconds>(
263+
std::chrono::steady_clock::now() - start);
257264
if (!s.ok()) {
258265
return s;
259266
}
267+
s = dest_writer->Close({});
268+
if (!s.ok()) {
269+
return s;
270+
}
271+
dest_writer.reset();
260272
*props = table_builder->GetTableProperties();
273+
start = std::chrono::steady_clock::now();
274+
TableReaderOptions reader_options(ioptions_, moptions_.prefix_extractor,
275+
moptions_.compression_manager.get(),
276+
soptions_, internal_comparator_,
277+
0 /* block_protection_bytes_per_key */);
278+
std::unique_ptr<RandomAccessFileReader> file_reader;
279+
s = RandomAccessFileReader::Create(env->GetFileSystem(), testFileName,
280+
soptions_, &file_reader, /*dbg=*/nullptr);
281+
if (!s.ok()) {
282+
return s;
283+
}
284+
std::unique_ptr<TableReader> table_reader;
285+
s = tb_options.moptions.table_factory->NewTableReader(
286+
reader_options, std::move(file_reader), table_builder->FileSize(),
287+
&table_reader);
288+
if (!s.ok()) {
289+
return s;
290+
}
291+
iter.reset(table_reader->NewIterator(
292+
read_options_, moptions_.prefix_extractor.get(), /*arena=*/nullptr,
293+
/*skip_filters=*/false, TableReaderCaller::kSSTDumpTool));
294+
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
295+
}
296+
s = iter->status();
297+
if (!s.ok()) {
298+
return s;
299+
}
300+
iter.reset();
301+
table_reader.reset();
302+
file_reader.reset();
303+
*read_time = std::chrono::duration_cast<std::chrono::microseconds>(
304+
std::chrono::steady_clock::now() - start);
261305
return env->DeleteFile(testFileName);
262306
}
263307

264308
Status SstFileDumper::ShowAllCompressionSizes(
265309
const std::vector<CompressionType>& compression_types,
266310
int32_t compress_level_from, int32_t compress_level_to) {
311+
#ifndef NDEBUG
312+
fprintf(stdout,
313+
"WARNING: Assertions are enabled; benchmarks unnecessarily slow\n");
314+
#endif
315+
BlockBasedTableOptions bbto;
316+
if (options_.table_factory->IsInstanceOf(
317+
TableFactory::kBlockBasedTableName())) {
318+
bbto = *(static_cast_with_check<BlockBasedTableFactory>(
319+
options_.table_factory.get()))
320+
->GetOptions<BlockBasedTableOptions>();
321+
}
322+
267323
for (CompressionType ctype : compression_types) {
268324
std::string cname;
269325
if (!GetStringFromCompressionType(&cname, ctype).ok()) {
@@ -273,10 +329,12 @@ Status SstFileDumper::ShowAllCompressionSizes(
273329
if (options_.compression_manager
274330
? options_.compression_manager->SupportsCompressionType(ctype)
275331
: CompressionTypeSupported(ctype)) {
276-
fprintf(stdout, "Compression: %-24s\n", cname.c_str());
277332
CompressionOptions compress_opt = options_.compression_opts;
333+
fprintf(stdout,
334+
"Compression: %-24s Block Size: %" PRIu64 " Threads: %u\n",
335+
cname.c_str(), bbto.block_size, compress_opt.parallel_threads);
278336
for (int32_t j = compress_level_from; j <= compress_level_to; j++) {
279-
fprintf(stdout, "Compression level: %d", j);
337+
fprintf(stdout, "Cx level: %d", j);
280338
compress_opt.level = j;
281339
Status s = ShowCompressionSize(ctype, compress_opt);
282340
if (!s.ok()) {
@@ -320,27 +378,26 @@ Status SstFileDumper::ShowCompressionSize(
320378
TablePropertiesCollectorFactory::Context::kUnknownColumnFamily,
321379
column_family_name, unknown_level, kUnknownNewestKeyTime);
322380
TableProperties props;
323-
std::chrono::steady_clock::time_point start =
324-
std::chrono::steady_clock::now();
325-
Status s = CalculateCompressedTableSize(tb_opts, &props);
381+
std::chrono::microseconds write_time;
382+
std::chrono::microseconds read_time;
383+
Status s =
384+
CalculateCompressedTableSize(tb_opts, &props, &write_time, &read_time);
326385
if (!s.ok()) {
327386
return s;
328387
}
329388

330389
uint64_t num_data_blocks = props.num_data_blocks;
331390

332-
std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
333-
fprintf(stdout, " Comp size: %10" PRIu64, props.data_size);
334-
fprintf(stdout, " Uncompressed: %10" PRIu64, props.uncompressed_data_size);
391+
fprintf(stdout, " Cx size: %10" PRIu64, props.data_size);
392+
fprintf(stdout, " Uncx size: %10" PRIu64, props.uncompressed_data_size);
335393
fprintf(stdout, " Ratio: %10s",
336394
std::to_string(static_cast<double>(props.uncompressed_data_size) /
337395
static_cast<double>(props.data_size))
338396
.c_str());
339-
fprintf(stdout, " Microsecs: %10s ",
340-
std::to_string(
341-
std::chrono::duration_cast<std::chrono::microseconds>(end - start)
342-
.count())
343-
.c_str());
397+
fprintf(stdout, " Write usec: %10s ",
398+
std::to_string(write_time.count()).c_str());
399+
fprintf(stdout, " Read usec: %10s ",
400+
std::to_string(read_time.count()).c_str());
344401
const uint64_t compressed_blocks =
345402
opts.statistics->getAndResetTickerCount(NUMBER_BLOCK_COMPRESSED);
346403
const uint64_t not_compressed_blocks =
@@ -370,11 +427,11 @@ Status SstFileDumper::ShowCompressionSize(
370427
: ((static_cast<double>(not_compressed_blocks) /
371428
static_cast<double>(num_data_blocks)) *
372429
100.0);
373-
fprintf(stdout, " Comp count: %6" PRIu64 " (%5.1f%%)", compressed_blocks,
430+
fprintf(stdout, " Cx count: %6" PRIu64 " (%5.1f%%)", compressed_blocks,
374431
compressed_pcnt);
375-
fprintf(stdout, " Not compressed (ratio): %6" PRIu64 " (%5.1f%%)",
432+
fprintf(stdout, " Not cx for ratio: %6" PRIu64 " (%5.1f%%)",
376433
ratio_not_compressed_blocks, ratio_not_compressed_pcnt);
377-
fprintf(stdout, " Not compressed (abort): %6" PRIu64 " (%5.1f%%)\n",
434+
fprintf(stdout, " Not cx otherwise: %6" PRIu64 " (%5.1f%%)\n",
378435
not_compressed_blocks, not_compressed_pcnt);
379436
return Status::OK();
380437
}

table/sst_file_dumper.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ class SstFileDumper {
5959
FilePrefetchBuffer* prefetch_buffer);
6060

6161
Status CalculateCompressedTableSize(const TableBuilderOptions& tb_options,
62-
TableProperties* props);
62+
TableProperties* props,
63+
std::chrono::microseconds* write_time,
64+
std::chrono::microseconds* read_time);
6365

6466
Status SetTableOptionsByMagicNumber(uint64_t table_magic_number);
6567
Status SetOldTableOptions();

tools/sst_dump_tool.cc

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ int SSTDumpTool::Run(int argc, char const* const* argv, Options options) {
181181
bool list_meta_blocks = false;
182182
bool has_compression_level_from = false;
183183
bool has_compression_level_to = false;
184-
bool has_specified_compression_types = false;
185184
std::string from_key;
186185
std::string to_key;
187186
std::string block_size_str;
@@ -258,7 +257,6 @@ int SSTDumpTool::Run(int argc, char const* const* argv, Options options) {
258257
std::string compression_types_csv = argv[i] + 20;
259258
std::istringstream iss(compression_types_csv);
260259
std::string compression_type;
261-
has_specified_compression_types = true;
262260

263261
while (std::getline(iss, compression_type, ',')) {
264262
auto iter =
@@ -392,12 +390,7 @@ int SSTDumpTool::Run(int argc, char const* const* argv, Options options) {
392390
}
393391
}
394392

395-
if (has_compression_level_from && has_compression_level_to) {
396-
if (!has_specified_compression_types || compression_types.size() != 1) {
397-
fprintf(stderr, "Specify one compression type.\n\n");
398-
exit(1);
399-
}
400-
} else if (has_compression_level_from || has_compression_level_to) {
393+
if (has_compression_level_from ^ has_compression_level_to) {
401394
fprintf(stderr,
402395
"Specify both --compression_level_from and "
403396
"--compression_level_to.\n\n");
@@ -536,14 +529,20 @@ int SSTDumpTool::Run(int argc, char const* const* argv, Options options) {
536529
}
537530

538531
if (command == "recompress") {
539-
fprintf(stdout, "Block Size: %zu Threads: %u\n", block_size,
540-
(unsigned)compression_parallel_threads);
541-
// TODO: consider getting supported compressions from the compression
542-
// manager
532+
if (compression_types.empty()) {
533+
if (options.compression_manager != nullptr) {
534+
for (int c = 0; c < kDisableCompressionOption; ++c) {
535+
if (options.compression_manager->SupportsCompressionType(
536+
static_cast<CompressionType>(c))) {
537+
compression_types.emplace_back(static_cast<CompressionType>(c));
538+
}
539+
}
540+
} else {
541+
compression_types = GetSupportedCompressions();
542+
}
543+
}
543544
st = dumper.ShowAllCompressionSizes(
544-
compression_types.empty() ? GetSupportedCompressions()
545-
: compression_types,
546-
compress_level_from, compress_level_to);
545+
compression_types, compress_level_from, compress_level_to);
547546
if (!st.ok()) {
548547
fprintf(stderr, "Failed to recompress: %s\n", st.ToString().c_str());
549548
exit(1);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Fixed a performance regression in LZ4 compression that started in version 10.6.0

util/compression.cc

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,7 @@ class BuiltinBZip2CompressorV2 : public CompressorWithSimpleDictBase {
516516
}
517517
};
518518

519-
class BuiltinLZ4CompressorV2 : public CompressorWithSimpleDictBase {
519+
class BuiltinLZ4CompressorV2WithDict : public CompressorWithSimpleDictBase {
520520
public:
521521
using CompressorWithSimpleDictBase::CompressorWithSimpleDictBase;
522522

@@ -527,8 +527,8 @@ class BuiltinLZ4CompressorV2 : public CompressorWithSimpleDictBase {
527527
}
528528

529529
std::unique_ptr<Compressor> CloneForDict(std::string&& dict_data) override {
530-
return std::make_unique<BuiltinLZ4CompressorV2>(opts_,
531-
std::move(dict_data));
530+
return std::make_unique<BuiltinLZ4CompressorV2WithDict>(
531+
opts_, std::move(dict_data));
532532
}
533533

534534
ManagedWorkingArea ObtainWorkingArea() override {
@@ -611,6 +611,72 @@ class BuiltinLZ4CompressorV2 : public CompressorWithSimpleDictBase {
611611
}
612612
};
613613

614+
class BuiltinLZ4CompressorV2NoDict : public BuiltinLZ4CompressorV2WithDict {
615+
public:
616+
BuiltinLZ4CompressorV2NoDict(const CompressionOptions& opts)
617+
: BuiltinLZ4CompressorV2WithDict(opts, /*dict_data=*/{}) {}
618+
619+
ManagedWorkingArea ObtainWorkingArea() override {
620+
// Using an LZ4_stream_t between compressions and resetting with
621+
// LZ4_resetStream_fast is actually slower than using a fresh LZ4_stream_t
622+
// each time, or not involving a stream at all. Similarly, using an extState
623+
// does not seem to offer a performance boost, perhaps a small regression.
624+
return {};
625+
}
626+
627+
void ReleaseWorkingArea(WorkingArea* wa) override {
628+
// Should not be called
629+
(void)wa;
630+
assert(wa == nullptr);
631+
}
632+
633+
Status CompressBlock(Slice uncompressed_data, char* compressed_output,
634+
size_t* compressed_output_size,
635+
CompressionType* out_compression_type,
636+
ManagedWorkingArea* wa) override {
637+
#ifdef LZ4
638+
(void)wa;
639+
auto [alg_output, alg_max_output_size] = StartCompressBlockV2(
640+
uncompressed_data, compressed_output, *compressed_output_size);
641+
if (alg_max_output_size == 0) {
642+
// Compression bypassed
643+
*compressed_output_size = 0;
644+
*out_compression_type = kNoCompression;
645+
return Status::OK();
646+
}
647+
int acceleration;
648+
if (opts_.level < 0) {
649+
acceleration = -opts_.level;
650+
} else {
651+
acceleration = 1;
652+
}
653+
auto outlen =
654+
LZ4_compress_fast(uncompressed_data.data(), alg_output,
655+
static_cast<int>(uncompressed_data.size()),
656+
static_cast<int>(alg_max_output_size), acceleration);
657+
if (outlen > 0) {
658+
// Compression kept/successful
659+
size_t output_size = static_cast<size_t>(
660+
outlen + /*header size*/ (alg_output - compressed_output));
661+
assert(output_size <= *compressed_output_size);
662+
*compressed_output_size = output_size;
663+
*out_compression_type = kLZ4Compression;
664+
return Status::OK();
665+
}
666+
// Compression rejected
667+
*compressed_output_size = 1;
668+
#else
669+
(void)uncompressed_data;
670+
(void)compressed_output;
671+
(void)wa;
672+
// Compression bypassed (not supported)
673+
*compressed_output_size = 0;
674+
#endif
675+
*out_compression_type = kNoCompression;
676+
return Status::OK();
677+
}
678+
};
679+
614680
class BuiltinLZ4HCCompressorV2 : public CompressorWithSimpleDictBase {
615681
public:
616682
using CompressorWithSimpleDictBase::CompressorWithSimpleDictBase;
@@ -1508,7 +1574,7 @@ class BuiltinCompressionManagerV2 : public CompressionManager {
15081574
case kBZip2Compression:
15091575
return std::make_unique<BuiltinBZip2CompressorV2>(opts);
15101576
case kLZ4Compression:
1511-
return std::make_unique<BuiltinLZ4CompressorV2>(opts);
1577+
return std::make_unique<BuiltinLZ4CompressorV2NoDict>(opts);
15121578
case kLZ4HCCompression:
15131579
return std::make_unique<BuiltinLZ4HCCompressorV2>(opts);
15141580
case kXpressCompression:

0 commit comments

Comments
 (0)