From 6e69b5827fa90e5669783296fb3fbc6fa7886730 Mon Sep 17 00:00:00 2001 From: iderex <30603423+iderex@users.noreply.github.com> Date: Mon, 10 Aug 2026 11:09:22 +0200 Subject: [PATCH] Add the Snappy CPU denominator to the bench harness [#164] There was no Snappy path in the bench harness at all, so the M3 device work had no number to be read against and any later GPU figure would have arrived without a baseline to divide by. bench_snappy builds the Silesia corpus in the two shapes that disagree about what a unit is - one stream per file, and one stream per 64 KiB page, which is what the columnar formats emit and what the batch API decodes - and times snappy::RawUncompress alone over each. Every stream is round-trip verified against the reference before anything is timed, and the destination buffers and declared lengths are taken outside the timed region so the number is the decoder's rather than the allocator's. Each report carries a digest of the corpus that was actually built, folded over the produced streams. A moved compressor pin, a changed chunk size or a generator that lost its noise source all move that digest while leaving the round trip intact, so the selfcheck asserts it and CI reds on drift the round trip alone would pass. Proven by cutting kChunkBytes to 32768: the selfcheck fails with the two digests printed, and passes again when it is restored. HostCpuName moves into bench_stats.h. Every report block names the host, and that header exists precisely so a promise kept by hand-matched copies does not become a promise about the copies. Recorded in docs/BENCHMARKS.md under M3. --- bench/CMakeLists.txt | 28 +++ bench/bench_lz4.cpp | 18 +- bench/bench_snappy.cpp | 453 +++++++++++++++++++++++++++++++++++++++++ bench/bench_stats.h | 20 ++ docs/BENCHMARKS.md | 62 ++++++ 5 files changed, 565 insertions(+), 16 deletions(-) create mode 100644 bench/bench_snappy.cpp diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt index 3706914..baa0d3f 100644 --- a/bench/CMakeLists.txt +++ b/bench/CMakeLists.txt @@ -70,6 +70,33 @@ add_test(NAME bench_frame_selfcheck COMMAND bench_lz4 --frame --selfcheck) set_tests_properties(bench_frame_selfcheck PROPERTIES LABELS gpu RUN_SERIAL TRUE) +# The Snappy CPU denominator (issue #164). No CUDA in it at all: there is no +# Snappy kernel yet, so the only decoder this times is the reference, and the +# binary needs neither a device nor a kernel header. It links the same +# oracle target tests/ pins and the same fixture library, because the Snappy +# streams in this project have ONE provenance (tests/fixtures.h) and a bench +# that compressed its own would be a second one nobody reconciles. +add_executable(bench_snappy bench_snappy.cpp) +target_link_libraries(bench_snappy PRIVATE cudec cudec_test_fixtures + snappy_oracle) +# The corpus digest reads src/xxhash64.h, the hash already in the tree. +target_include_directories(bench_snappy PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../src) +set_target_properties(bench_snappy PROPERTIES CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON) +# -O2 unconditionally for the reason bench_lz4 gives above: the documented +# container command sets no CMAKE_BUILD_TYPE, and an -O0 reference decoder +# would be timed instead of the reference decoder. +target_compile_options(bench_snappy PRIVATE -Wall -Wextra -Werror -O2) + +# Same rot protection as the entries above, and one more thing besides. The +# selfcheck builds both corpus shapes from a fixed PRNG and asserts the +# digest of each, so a moved compressor pin, a changed chunk size or a +# generator that lost its noise source reds CI - none of which would stop the +# corpus round-tripping, which is why the round trip alone is not the check. +# CPU-only, so it runs on the GPU-less runner. +add_test(NAME bench_snappy_selfcheck COMMAND bench_snappy --selfcheck) + # The Zstd worst-case corpus (issue #229). No CUDA in it at all: the frames # are constructed on the host, emitted through the reference's own # sequence-compression entry point and decoded by the reference, so the @@ -112,5 +139,6 @@ add_test(NAME bench_zstd_worst_selfcheck COMMAND bench_zstd --worst set_tests_properties( bench_selfcheck bench_worst4b_selfcheck bench_longmatch_selfcheck bench_assetlike_selfcheck bench_zstd_worst_selfcheck bench_frame_selfcheck + bench_snappy_selfcheck PROPERTIES TIMEOUT ${CUDEC_TEST_TIMEOUT_SECONDS}) cudec_assert_test_timeouts() diff --git a/bench/bench_lz4.cpp b/bench/bench_lz4.cpp index 5279fd5..034b912 100644 --- a/bench/bench_lz4.cpp +++ b/bench/bench_lz4.cpp @@ -774,20 +774,6 @@ double DecodeAllSeconds(const Corpus& corpus, unsigned char* scratch) { return std::chrono::duration(end - start).count(); } -std::string HostCpuName() { - std::ifstream in("/proc/cpuinfo"); - std::string line; - while (std::getline(in, line)) { - if (line.rfind("model name", 0) == 0) { - const size_t colon = line.find(':'); - if (colon != std::string::npos) { - return line.substr(colon + 2); - } - } - } - return "unknown host CPU"; -} - std::string CudaDeviceLine() { int count = 0; if (cudaGetDeviceCount(&count) != cudaSuccess || count == 0) { @@ -994,7 +980,7 @@ bool RunFrameRung(const std::vector& source, std::printf("- decoder: cudec_lz4f_decompress (host frame in, host bytes " "out; H2D, decode, D2H, assembly and checksums are all " "inside the timed call)\n"); - std::printf("- host CPU: %s\n", HostCpuName().c_str()); + std::printf("- host CPU: %s\n", cudec_bench::HostCpuName().c_str()); std::printf("- CUDA device: %s\n", CudaDeviceLine().c_str()); std::printf("- cudec: %d\n", cudec_version()); std::printf("- corpus: %s, %.2f MB original, %.2f MB frame (ratio " @@ -1095,7 +1081,7 @@ void PrintReport(const Corpus& corpus, const std::vector& sorted, std::printf("- decoder: CPU oracle, LZ4_decompress_safe (liblz4 %s), " "single thread\n", LZ4_versionString()); - std::printf("- host CPU: %s\n", HostCpuName().c_str()); + std::printf("- host CPU: %s\n", cudec_bench::HostCpuName().c_str()); std::printf("- CUDA device: %s\n", CudaDeviceLine().c_str()); std::printf("- cudec: %d (the CPU rows time the liblz4 oracle baseline; " "the GPU rows below, when --gpu is set, time cudec's " diff --git a/bench/bench_snappy.cpp b/bench/bench_snappy.cpp new file mode 100644 index 0000000..c6672f9 --- /dev/null +++ b/bench/bench_snappy.cpp @@ -0,0 +1,453 @@ +/* The Snappy benchmark harness: the CPU denominator every later GPU number + * is read against. There is no Snappy kernel yet, so this measures the + * reference decoder alone, and it says so in its own report rather than + * leaving a reader to infer it (docs/MASTERPLAN.md section 5, honest + * numbers). + * + * Two corpus shapes, because the batch API and the format disagree about + * what a unit is. Snappy's own framing is a whole stream per file; the + * shape this library decodes is a page, which the columnar formats emit at + * 64 KiB. A denominator taken on one shape does not transfer to the other, + * so both are built from the same bytes and reported separately. + * + * Every corpus carries a digest of what was actually built, so a run that + * silently compressed something else cannot be mistaken for a comparable + * one. */ +#include "bench_stats.h" +#include "cudec.h" +#include "fixtures.h" +#include "xxhash64.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +/* The page size the columnar formats emit and the batch API targets. */ +constexpr size_t kChunkBytes = 65536; + +constexpr size_t kMaxRuns = 1000000; + +/* The selfcheck source. Compressible enough that the streams are not all + * literals, several chunks long at the chunked shape, and from a fixed PRNG + * so a failure reproduces. */ +constexpr size_t kSelfcheckBytes = 3u << 20; + +enum class Shape { + /* One Snappy raw stream per input file. */ + kWhole, + /* One Snappy raw stream per 64 KiB of input. */ + kChunked, +}; + +struct Corpus { + std::string name; + Shape shape = Shape::kWhole; + std::vector> originals; + std::vector> compressed; + size_t original_bytes = 0; + size_t compressed_bytes = 0; + /* Printed verbatim in the methodology block, so it must stay true for + * whichever corpus ran. */ + std::string provenance = + "compressed in-harness by the pinned snappy oracle"; +}; + +const char* ShapeName(Shape shape) { + return shape == Shape::kWhole ? "whole-file streams" + : "64 KiB-chunked streams"; +} + +/* The corpus lock. + * + * What has to be caught is drift: a compressor pin that moved, a chunking + * rule that changed, an input file that is not the one the report names. + * All three change the produced streams, so the digest runs over those and + * not over the inputs - the inputs are already pinned by the manifest + * bench/get-corpora.sh writes, and adding a second fetch path or a second + * input lock here would be two authorities on one fact. + * + * The digest is a fold rather than one hash over the concatenation, so it + * costs 16 bytes per stream instead of a second copy of the corpus: each + * stream contributes its length and its own XXH64, little-endian, in + * corpus order, and the reported digest is the XXH64 of that array. + * Reordering, retruncating or recompressing any stream moves it. + * + * XXH64 and not SHA-256, stated so nobody reads more into it than is + * there: this is a drift detector over data the harness just built, not a + * defence against a chosen collision, and it is the hash already in the + * tree (src/xxhash64.h) rather than a new dependency for a bench. */ +void AppendLe64(uint64_t value, std::vector* out) { + for (unsigned i = 0; i < 8; i++) { + out->push_back(static_cast(value >> (i * 8))); + } +} + +uint64_t CorpusDigest(const Corpus& corpus) { + std::vector fold; + fold.reserve(corpus.compressed.size() * 16); + for (const auto& stream : corpus.compressed) { + AppendLe64(stream.size(), &fold); + AppendLe64(cudec_detail::Xxh64(stream.data(), stream.size()), &fold); + } + return cudec_detail::Xxh64(fold.data(), fold.size()); +} + +bool AppendFile(const std::string& path, Shape shape, Corpus* corpus) { + std::ifstream in(path, std::ios::binary); + if (!in) { + std::fprintf(stderr, "cannot open corpus file: %s\n", path.c_str()); + return false; + } + const size_t before = corpus->originals.size(); + if (shape == Shape::kChunked) { + while (true) { + std::vector chunk(kChunkBytes); + in.read(reinterpret_cast(chunk.data()), + static_cast(kChunkBytes)); + const std::streamsize got = in.gcount(); + if (got <= 0) { + break; + } + chunk.resize(static_cast(got)); + corpus->originals.push_back(std::move(chunk)); + } + } else { + std::vector whole; + char buffer[1 << 16]; + while (in.read(buffer, sizeof(buffer)) || in.gcount() > 0) { + whole.insert(whole.end(), buffer, buffer + in.gcount()); + } + if (!whole.empty()) { + corpus->originals.push_back(std::move(whole)); + } + } + /* Fail closed on I/O trouble and on zero contribution, per FILE rather + * than over the accumulated corpus: the accumulated test goes vacuous + * from the second argument on, and a file that contributed nothing must + * never end up attested in the methodology block. */ + if (in.bad()) { + std::fprintf(stderr, "read error in corpus file: %s\n", path.c_str()); + return false; + } + if (corpus->originals.size() == before) { + std::fprintf(stderr, "corpus file contributed no data: %s\n", + path.c_str()); + return false; + } + return true; +} + +void CompressAll(Corpus* corpus) { + for (const auto& original : corpus->originals) { + corpus->compressed.push_back(SnappyCompressBlock(original)); + corpus->original_bytes += original.size(); + corpus->compressed_bytes += corpus->compressed.back().size(); + } +} + +/* The oracle is the sole authority on validity, and it says so before any + * timing: a number taken on a stream the reference refuses, or on one that + * does not round-trip, is a number about nothing. */ +bool VerifyCorpus(const Corpus& corpus) { + for (size_t i = 0; i < corpus.compressed.size(); i++) { + std::vector decoded; + if (!SnappyOracleDecodes(corpus.compressed[i], &decoded)) { + std::fprintf(stderr, "the oracle refuses stream %zu of %s\n", i, + corpus.name.c_str()); + return false; + } + if (decoded != corpus.originals[i]) { + std::fprintf(stderr, "stream %zu of %s does not round-trip\n", i, + corpus.name.c_str()); + return false; + } + } + return true; +} + +/* One timed pass over the whole corpus. The timed region is + * snappy::RawUncompress alone: the destination buffers are allocated and + * the declared lengths read outside it, so the number is the decoder's and + * not the allocator's. + * + * Returns a negative duration if any stream fails, so a broken decode can + * never be reported as a fast one. */ +double DecodeAllSeconds(const Corpus& corpus, + std::vector>* buffers) { + const auto start = std::chrono::steady_clock::now(); + for (size_t i = 0; i < corpus.compressed.size(); i++) { + const auto& stream = corpus.compressed[i]; + if (!snappy::RawUncompress(reinterpret_cast(stream.data()), + stream.size(), (*buffers)[i].data())) { + return -1.0; + } + } + const auto end = std::chrono::steady_clock::now(); + return std::chrono::duration(end - start).count(); +} + +bool MakeBuffers(const Corpus& corpus, + std::vector>* buffers) { + buffers->clear(); + for (const auto& stream : corpus.compressed) { + size_t declared = 0; + if (!snappy::GetUncompressedLength( + reinterpret_cast(stream.data()), stream.size(), + &declared)) { + std::fprintf(stderr, "no declared length in a %s stream\n", + corpus.name.c_str()); + return false; + } + buffers->push_back(std::vector(declared)); + } + return true; +} + +void PrintReport(const Corpus& corpus, const std::vector& sorted, + size_t warmup, size_t runs) { + std::vector sizes; + for (const auto& original : corpus.originals) { + sizes.push_back(original.size()); + } + std::sort(sizes.begin(), sizes.end()); + const double to_gbps = static_cast(corpus.original_bytes) / 1e9; + + std::printf("## bench_snappy report\n"); + std::printf("- decoder: CPU oracle, snappy::RawUncompress (google/snappy " + "%d.%d.%d), single thread. cudec has no Snappy kernel yet, so " + "this report is the denominator and carries no cudec " + "number\n", + SNAPPY_MAJOR, SNAPPY_MINOR, SNAPPY_PATCHLEVEL); + std::printf("- host CPU: %s\n", cudec_bench::HostCpuName().c_str()); + std::printf("- cudec: %d\n", cudec_version()); + std::printf("- corpus: %s, %s, %zu streams, %.2f MB original, %.2f MB " + "compressed (ratio %.3f), %s\n", + corpus.name.c_str(), ShapeName(corpus.shape), + corpus.originals.size(), + static_cast(corpus.original_bytes) / 1e6, + static_cast(corpus.compressed_bytes) / 1e6, + static_cast(corpus.compressed_bytes) / + static_cast(corpus.original_bytes), + corpus.provenance.c_str()); + std::printf("- corpus digest: %016llx (XXH64 over per-stream length and " + "XXH64, little-endian, in corpus order)\n", + static_cast(CorpusDigest(corpus))); + std::printf("- stream sizes: min %zu / median %zu / max %zu bytes " + "uncompressed\n", + sizes.front(), sizes[sizes.size() / 2], sizes.back()); + std::printf("- method: %zu warmup + %zu measured runs, wall clock per " + "whole-corpus decode; the timed region is " + "snappy::RawUncompress only (no allocation, no length " + "parse); every stream round-trip-verified against the " + "original once before timing; percentiles are nearest-rank\n", + warmup, runs); + std::printf("- wall per run: p50 %.3f ms / p90 %.3f ms / p99 %.3f ms\n", + cudec_bench::Percentile(sorted, 50) * 1e3, + cudec_bench::Percentile(sorted, 90) * 1e3, + cudec_bench::Percentile(sorted, 99) * 1e3); + std::printf("- decode throughput: p50 %.3f GB/s / p90 %.3f GB/s / p99 " + "%.3f GB/s\n", + to_gbps / cudec_bench::Percentile(sorted, 50), + to_gbps / cudec_bench::Percentile(sorted, 90), + to_gbps / cudec_bench::Percentile(sorted, 99)); +} + +bool RunCorpus(Corpus* corpus, size_t warmup, size_t runs) { + CompressAll(corpus); + if (corpus->original_bytes == 0) { + std::fprintf(stderr, "corpus is empty - nothing to benchmark\n"); + return false; + } + if (!VerifyCorpus(*corpus)) { + return false; + } + std::vector> buffers; + if (!MakeBuffers(*corpus, &buffers)) { + return false; + } + for (size_t i = 0; i < warmup; i++) { + if (DecodeAllSeconds(*corpus, &buffers) < 0) { + std::fprintf(stderr, "a warmup decode failed\n"); + return false; + } + } + std::vector times; + for (size_t i = 0; i < runs; i++) { + const double seconds = DecodeAllSeconds(*corpus, &buffers); + if (seconds < 0) { + std::fprintf(stderr, "a measured decode failed\n"); + return false; + } + times.push_back(seconds); + } + std::sort(times.begin(), times.end()); + PrintReport(*corpus, times, warmup, runs); + return true; +} + +std::vector MakeSelfcheckSource(size_t bytes) { + std::vector out(bytes); + uint64_t state = 0x9E3779B97F4A7C15ull; + for (size_t i = 0; i < bytes; i++) { + state = state * 6364136223846793005ull + 1442695040888963407ull; + /* Runs of a repeating alphabet with occasional noise: copies for the + * decoder to execute, without collapsing to one long copy. */ + out[i] = (i % 61 == 0) ? static_cast(state >> 56) + : static_cast('a' + (i / 7) % 26); + } + return out; +} + +/* The selfcheck's corpus is generated from a fixed PRNG and compressed by + * the pinned oracle, so both shapes are reproducible byte for byte and + * their digests are constants. Asserting them is what makes this a rot + * check rather than a run: a compressor pin that moved, a chunk size that + * changed, or a generator that lost its noise source all move a digest, + * and none of them would stop the corpus round-tripping. */ +constexpr uint64_t kSelfcheckWholeDigest = 0x82b0f9596c1b94e8ull; +constexpr uint64_t kSelfcheckChunkedDigest = 0xe7c9cffbc38ed2e4ull; + +bool CheckDigest(const Corpus& corpus, uint64_t expected) { + const uint64_t actual = CorpusDigest(corpus); + if (actual == expected) { + return true; + } + std::fprintf(stderr, + "the %s selfcheck corpus digest moved: expected %016llx, " + "built %016llx - the corpus this harness constructs is not " + "the one its numbers were recorded on\n", + ShapeName(corpus.shape), + static_cast(expected), + static_cast(actual)); + return false; +} + +bool ParseCount(const char* text, size_t low, size_t high, size_t* out) { + char* end = nullptr; + const unsigned long long value = std::strtoull(text, &end, 10); + if (end == text || *end != '\0' || value < low || value > high) { + return false; + } + *out = static_cast(value); + return true; +} + +} // namespace + +int main(int argc, char** argv) { + size_t runs = 30; + size_t warmup = 3; + bool selfcheck = false; + bool whole = false; + bool chunked = false; + std::vector files; + for (int i = 1; i < argc; i++) { + const std::string arg = argv[i]; + if (arg == "--selfcheck") { + selfcheck = true; + } else if (arg == "--whole") { + whole = true; + } else if (arg == "--chunked") { + chunked = true; + } else if (arg == "--runs" && i + 1 < argc) { + if (!ParseCount(argv[++i], 1, kMaxRuns, &runs)) { + std::fprintf(stderr, "--runs must be in [1, %zu]\n", kMaxRuns); + return 2; + } + } else if (arg == "--warmup" && i + 1 < argc) { + if (!ParseCount(argv[++i], 0, kMaxRuns, &warmup)) { + std::fprintf(stderr, "--warmup must be in [0, %zu]\n", + kMaxRuns); + return 2; + } + } else if (arg == "--runs" || arg == "--warmup") { + std::fprintf(stderr, "%s needs a value\n", arg.c_str()); + return 2; + } else if (!arg.empty() && arg[0] == '-') { + std::fprintf(stderr, "usage: bench_snappy [--runs N] [--warmup N] " + "[--whole] [--chunked] [--selfcheck] " + "[corpus files...]\n"); + return 2; + } else { + files.push_back(arg); + } + } + /* Neither shape named means both, which is the recorded run. */ + if (!whole && !chunked) { + whole = true; + chunked = true; + } + if (selfcheck) { + warmup = 1; + runs = 3; + } + if (files.empty() && !selfcheck) { + std::fprintf(stderr, "bench_snappy needs corpus files (the recorded " + "run uses bench/corpora/silesia/*); --selfcheck " + "runs it on a generated source instead\n"); + return 2; + } + + const std::vector generated = + selfcheck ? MakeSelfcheckSource(kSelfcheckBytes) + : std::vector(); + + const Shape shapes[] = {Shape::kWhole, Shape::kChunked}; + for (Shape shape : shapes) { + if (shape == Shape::kWhole && !whole) { + continue; + } + if (shape == Shape::kChunked && !chunked) { + continue; + } + Corpus corpus; + corpus.shape = shape; + if (selfcheck) { + corpus.name = "generated (selfcheck)"; + corpus.provenance = "generated in-harness from a fixed PRNG, " + "compressed by the pinned snappy oracle"; + if (shape == Shape::kChunked) { + for (size_t off = 0; off < generated.size(); + off += kChunkBytes) { + const size_t take = + std::min(kChunkBytes, generated.size() - off); + const auto first = + generated.begin() + static_cast(off); + corpus.originals.push_back(std::vector( + first, first + static_cast(take))); + } + } else { + corpus.originals.push_back(generated); + } + } else { + for (const auto& path : files) { + if (!AppendFile(path, shape, &corpus)) { + return 1; + } + const size_t slash = path.find_last_of("/\\"); + corpus.name += + (corpus.name.empty() ? "" : "+") + + path.substr(slash == std::string::npos ? 0 : slash + 1); + } + } + if (!RunCorpus(&corpus, warmup, runs)) { + return 1; + } + if (selfcheck && !CheckDigest(corpus, shape == Shape::kWhole + ? kSelfcheckWholeDigest + : kSelfcheckChunkedDigest)) { + return 1; + } + std::printf("\n"); + } + return 0; +} diff --git a/bench/bench_stats.h b/bench/bench_stats.h index 932e42b..0fc0673 100644 --- a/bench/bench_stats.h +++ b/bench/bench_stats.h @@ -12,6 +12,8 @@ #define CUDEC_BENCH_STATS_H #include +#include +#include #include namespace cudec_bench { @@ -40,6 +42,24 @@ inline double GbpsFromMs(double gb, double ms) { return ms > 0.0 ? gb / (ms / 1e3) : 0.0; } +/* The host the numbers were taken on, read from the kernel rather than + * configured, so a report cannot attest a CPU the run did not happen on. + * Here rather than in one harness because every report block names it and a + * second hand-matched copy is what this header exists to prevent. */ +inline std::string HostCpuName() { + std::ifstream in("/proc/cpuinfo"); + std::string line; + while (std::getline(in, line)) { + if (line.rfind("model name", 0) == 0) { + const size_t colon = line.find(':'); + if (colon != std::string::npos) { + return line.substr(colon + 2); + } + } + } + return "unknown host CPU"; +} + } // namespace cudec_bench #endif /* CUDEC_BENCH_STATS_H */ diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 10d46e2..ef6676a 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -943,6 +943,68 @@ different mechanism and is not evaluated here; overlap belongs to the streaming context work, which owns a buffer across calls and so does not pay the pinning per call. +## M3: the Snappy CPU denominator (issue #164) + +There is no Snappy kernel yet. This entry is the denominator a later device +number will be read against, and the harness says so in its own report rather +than leaving a reader to infer it from the absence of a GPU row. + +Two corpus shapes, because the format and the batch API disagree about what a +unit is. Snappy's own framing is one stream per file; the shape this library +decodes is a page, which the columnar formats emit at 64 KiB. Both are built +from the same Silesia bytes by the pinned snappy 1.2.2 compressor, verified +stream by stream against the reference decoder before anything is timed, and +reported separately. + +Each report carries a digest of the corpus that was actually built, folded +over the produced streams rather than over the inputs (the inputs are already +pinned by the manifest `bench/get-corpora.sh` writes). It is order-sensitive, +so the digests below belong to the file order a glob produces. Recorded +2026-08-10 inside the digest-pinned `nvidia/cuda:12.6.2-devel-ubuntu24.04` +container, on the host CPU named in the blocks. Reproduce with +`bench_snappy --warmup 3 --runs 30 bench/corpora/silesia/*`. + +``` +## bench_snappy report +- decoder: CPU oracle, snappy::RawUncompress (google/snappy 1.2.2), single thread. cudec has no Snappy kernel yet, so this report is the denominator and carries no cudec number +- host CPU: AMD Ryzen 9 5950X 16-Core Processor +- cudec: 100 +- corpus: dickens+mozilla+mr+nci+ooffice+osdb+reymont+samba+sao+webster+x-ray+xml, whole-file streams, 12 streams, 211.94 MB original, 101.35 MB compressed (ratio 0.478), compressed in-harness by the pinned snappy oracle +- corpus digest: 7bd83d9e3b24fd44 (XXH64 over per-stream length and XXH64, little-endian, in corpus order) +- stream sizes: min 5345280 / median 10085684 / max 51220480 bytes uncompressed +- method: 3 warmup + 30 measured runs, wall clock per whole-corpus decode; the timed region is snappy::RawUncompress only (no allocation, no length parse); every stream round-trip-verified against the original once before timing; percentiles are nearest-rank +- wall per run: p50 177.088 ms / p90 181.655 ms / p99 186.636 ms +- decode throughput: p50 1.197 GB/s / p90 1.167 GB/s / p99 1.136 GB/s +``` + +``` +## bench_snappy report +- decoder: CPU oracle, snappy::RawUncompress (google/snappy 1.2.2), single thread. cudec has no Snappy kernel yet, so this report is the denominator and carries no cudec number +- host CPU: AMD Ryzen 9 5950X 16-Core Processor +- cudec: 100 +- corpus: dickens+mozilla+mr+nci+ooffice+osdb+reymont+samba+sao+webster+x-ray+xml, 64 KiB-chunked streams, 3239 streams, 211.94 MB original, 101.36 MB compressed (ratio 0.478), compressed in-harness by the pinned snappy oracle +- corpus digest: be088850546d917c (XXH64 over per-stream length and XXH64, little-endian, in corpus order) +- stream sizes: min 8066 / median 65536 / max 65536 bytes uncompressed +- method: 3 warmup + 30 measured runs, wall clock per whole-corpus decode; the timed region is snappy::RawUncompress only (no allocation, no length parse); every stream round-trip-verified against the original once before timing; percentiles are nearest-rank +- wall per run: p50 177.413 ms / p90 184.445 ms / p99 187.988 ms +- decode throughput: p50 1.195 GB/s / p90 1.149 GB/s / p99 1.127 GB/s +``` + +**Cutting the corpus into 64 KiB pages costs the reference decoder nothing +measurable.** 177.088 ms whole against 177.413 ms chunked is 0.18% apart, well +inside the p50-to-p99 spread of either row, and the compressed size moves by +10 KB in 101 MB. That matters for what comes later: a device number quoted +against the chunked denominator is not being flattered by a handicapped +baseline, because the two baselines are the same number. + +**Snappy's reference decoder is about 2.8x slower than liblz4's on these +bytes, at the same ratio.** The LZ4 CPU-oracle row further down this file +reports 3.410 GB/s p50 over the same 211.94 MB at ratio 0.483; this one +reports 1.197 GB/s at ratio 0.478. Both are single-thread wall clock on the +same host with the timed region held to the decode call alone, so the +comparison is between the two references and says nothing about either GPU +path. + ## Community AMD results **No community results yet.** Nothing in this section is a measurement; it