diff --git a/compression/JFJochCompressor.cpp b/compression/JFJochCompressor.cpp index c144abb1..4540db96 100644 --- a/compression/JFJochCompressor.cpp +++ b/compression/JFJochCompressor.cpp @@ -16,6 +16,13 @@ extern "C" { void bshuf_write_uint64_BE(void* buf, uint64_t num); } +// Necessary condition for BlockSize() to be a valid bitshuffle block: with elem_size a power of +// two, a byte target that is a multiple of BSHUF_BLOCKED_MULT keeps the element count a multiple +// of BSHUF_BLOCKED_MULT too. +static_assert(JFJochBitShuffleCompressor::DefaultBlockSizeBytes(CompressionAlgorithm::BSHUF_LZ4) % BSHUF_BLOCKED_MULT == 0 + && JFJochBitShuffleCompressor::DefaultBlockSizeBytes(CompressionAlgorithm::BSHUF_ZSTD) % BSHUF_BLOCKED_MULT == 0, + "block byte target must be a multiple of the bitshuffle block multiple"); + // Worst-case size of one compressed block, including its 4-byte length prefix. Mirrors the // per-block term of MaxCompressedSize(), so a dest sized to MaxCompressedSize() never fails. static size_t MaxCompressedBlockSize(CompressionAlgorithm algorithm, size_t src_size) { @@ -82,8 +89,6 @@ int64_t JFJochBitShuffleCompressor::Compress(void *dest, size_t dest_size, const auto c_dest = (char *) dest; auto c_source = (char *) source; - static_assert(DefaultBlockSize % BSHUF_BLOCKED_MULT == 0, "Block size must be multiple of 8"); - if (algorithm == CompressionAlgorithm::NO_COMPRESSION) { // Trivial case if no compression - copy content if (nelements * elem_size > dest_size) @@ -95,25 +100,27 @@ int64_t JFJochBitShuffleCompressor::Compress(void *dest, size_t dest_size, const if (dest_size < 12) return -1; + const size_t block_size = BlockSize(algorithm, elem_size); + bshuf_write_uint64_BE(c_dest, nelements * elem_size); - bshuf_write_uint32_BE(c_dest + 8, DefaultBlockSize * elem_size); + bshuf_write_uint32_BE(c_dest + 8, block_size * elem_size); - if (tmp_space.size() < DefaultBlockSize * elem_size) - tmp_space.resize(DefaultBlockSize * elem_size); - if (scratch.size() < DefaultBlockSize * elem_size) - scratch.resize(DefaultBlockSize * elem_size); + if (tmp_space.size() < block_size * elem_size) + tmp_space.resize(block_size * elem_size); + if (scratch.size() < block_size * elem_size) + scratch.resize(block_size * elem_size); - size_t num_full_blocks = nelements / DefaultBlockSize; - size_t reminder_size = nelements - num_full_blocks * DefaultBlockSize; + size_t num_full_blocks = nelements / block_size; + size_t reminder_size = nelements - num_full_blocks * block_size; size_t compressed_size = 12; // Blocks are small relative to the image, so before each one we just check that the // remaining space still covers that block's worst case, and bail out (-1) if not. for (int i = 0; i < num_full_blocks; i++) { - if (compressed_size + MaxCompressedBlockSize(algorithm, DefaultBlockSize * elem_size) > dest_size) + if (compressed_size + MaxCompressedBlockSize(algorithm, block_size * elem_size) > dest_size) return -1; compressed_size += CompressBlock(c_dest + compressed_size, - c_source + i * DefaultBlockSize * elem_size, DefaultBlockSize, elem_size); + c_source + i * block_size * elem_size, block_size, elem_size); } size_t last_block_size = reminder_size - reminder_size % BSHUF_BLOCKED_MULT; @@ -121,14 +128,14 @@ int64_t JFJochBitShuffleCompressor::Compress(void *dest, size_t dest_size, const if (compressed_size + MaxCompressedBlockSize(algorithm, last_block_size * elem_size) > dest_size) return -1; compressed_size += CompressBlock(c_dest + compressed_size, - c_source + num_full_blocks * DefaultBlockSize * elem_size, last_block_size, elem_size); + c_source + num_full_blocks * block_size * elem_size, last_block_size, elem_size); } size_t leftover_bytes = (reminder_size % BSHUF_BLOCKED_MULT) * elem_size; if (leftover_bytes > 0) { if (compressed_size + leftover_bytes > dest_size) return -1; - memcpy(c_dest + compressed_size, c_source + (num_full_blocks * DefaultBlockSize + last_block_size) * elem_size, leftover_bytes); + memcpy(c_dest + compressed_size, c_source + (num_full_blocks * block_size + last_block_size) * elem_size, leftover_bytes); compressed_size += leftover_bytes; } return compressed_size; diff --git a/compression/JFJochCompressor.h b/compression/JFJochCompressor.h index 30bcbcc7..a6a3f97c 100644 --- a/compression/JFJochCompressor.h +++ b/compression/JFJochCompressor.h @@ -21,7 +21,18 @@ class JFJochBitShuffleCompressor { size_t CompressBlock(char *dest, const char * source, size_t nelements, size_t elem_size); public: - constexpr static const size_t DefaultBlockSize = 16384; + // The bitshuffle block size is chosen as a byte target rather than a fixed element count, so + // the per-block working set - and thus the cache behaviour - stays constant across pixel bit + // depths. The target is per-algorithm: LZ4 favours a small, cache-resident block (throughput), + // ZSTD/RLE a large block (ratio). The element block size scales inversely with elem_size, + // which is always a power of two here, so the result is a power of two >= 8 (a valid bitshuffle + // block, multiple of 8). E.g. ZSTD 128 kB -> 65536 elem at 16-bit, 32768 at 32-bit. + constexpr static size_t DefaultBlockSizeBytes(CompressionAlgorithm algorithm) { + return algorithm == CompressionAlgorithm::BSHUF_LZ4 ? 16384 : 131072; + } + constexpr static size_t BlockSize(CompressionAlgorithm algorithm, size_t elem_size) { + return DefaultBlockSizeBytes(algorithm) / elem_size; + } explicit JFJochBitShuffleCompressor(CompressionAlgorithm algorithm); diff --git a/compression/MaxCompressedSize.cpp b/compression/MaxCompressedSize.cpp index 2d2b9243..613a3480 100644 --- a/compression/MaxCompressedSize.cpp +++ b/compression/MaxCompressedSize.cpp @@ -9,10 +9,10 @@ int64_t MaxCompressedSize(CompressionAlgorithm algorithm, int64_t pixels_number, uint16_t pixel_depth) { switch (algorithm) { case CompressionAlgorithm::BSHUF_LZ4: - return bshuf_compress_lz4_bound(pixels_number, pixel_depth, JFJochBitShuffleCompressor::DefaultBlockSize) + 12; + return bshuf_compress_lz4_bound(pixels_number, pixel_depth, JFJochBitShuffleCompressor::BlockSize(algorithm, pixel_depth)) + 12; case CompressionAlgorithm::BSHUF_ZSTD: case CompressionAlgorithm::BSHUF_ZSTD_RLE: - return bshuf_compress_zstd_bound(pixels_number, pixel_depth,JFJochBitShuffleCompressor::DefaultBlockSize) + 12; + return bshuf_compress_zstd_bound(pixels_number, pixel_depth, JFJochBitShuffleCompressor::BlockSize(algorithm, pixel_depth)) + 12; default: return pixels_number * pixel_depth; } diff --git a/tests/FrameTransformationTest.cpp b/tests/FrameTransformationTest.cpp index 1ac5d701..22f99752 100644 --- a/tests/FrameTransformationTest.cpp +++ b/tests/FrameTransformationTest.cpp @@ -97,7 +97,8 @@ TEST_CASE("FrameTransformation_Raw_NoCompression_bshuf_lz4" ,"") { auto image = transformation.GetCompressedImage(); REQUIRE(read_be64(image.GetCompressed()) == experiment.GetPixelsNum() * experiment.GetByteDepthImage()); - REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::DefaultBlockSize * + REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::BlockSize(experiment.GetCompressionAlgorithm(), + experiment.GetByteDepthImage()) * experiment.GetByteDepthImage()); std::vector output; @@ -238,7 +239,8 @@ TEST_CASE("FrameTransformation_Converted_bshuf_lz4" ,"") { auto image = transformation.GetCompressedImage(); REQUIRE(read_be64(image.GetCompressed()) == experiment.GetPixelsNum() * experiment.GetByteDepthImage()); - REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::DefaultBlockSize * + REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::BlockSize(experiment.GetCompressionAlgorithm(), + experiment.GetByteDepthImage()) * experiment.GetByteDepthImage()); std::vector output; @@ -297,7 +299,8 @@ TEST_CASE("FrameTransformation_Converted_bshuf_zstd" ,"") { auto image = transformation.GetCompressedImage(); REQUIRE(read_be64(image.GetCompressed()) == experiment.GetPixelsNum() * experiment.GetByteDepthImage()); - REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::DefaultBlockSize * + REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::BlockSize(experiment.GetCompressionAlgorithm(), + experiment.GetByteDepthImage()) * experiment.GetByteDepthImage()); std::vector output; @@ -356,7 +359,8 @@ TEST_CASE("FrameTransformation_Converted_bshuf_zstd_rle" ,"") { auto image = transformation.GetCompressedImage(); REQUIRE(read_be64(image.GetCompressed()) == experiment.GetPixelsNum() * experiment.GetByteDepthImage()); - REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::DefaultBlockSize * + REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::BlockSize(experiment.GetCompressionAlgorithm(), + experiment.GetByteDepthImage()) * experiment.GetByteDepthImage()); std::vector output; @@ -418,7 +422,8 @@ TEST_CASE("FrameTransformation_Converted_bshuf_zstd_32bit" ,"") { auto image = transformation.GetCompressedImage(); REQUIRE(read_be64(image.GetCompressed()) == experiment.GetPixelsNum() * experiment.GetByteDepthImage()); - REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::DefaultBlockSize * + REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::BlockSize(experiment.GetCompressionAlgorithm(), + experiment.GetByteDepthImage()) * experiment.GetByteDepthImage()); std::vector output; @@ -481,7 +486,8 @@ TEST_CASE("FrameTransformation_Converted_bshuf_zstd_8bit" ,"") { auto image = transformation.GetCompressedImage(); REQUIRE(read_be64(image.GetCompressed()) == experiment.GetPixelsNum() * experiment.GetByteDepthImage()); - REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::DefaultBlockSize * + REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::BlockSize(experiment.GetCompressionAlgorithm(), + experiment.GetByteDepthImage()) * experiment.GetByteDepthImage()); std::vector output; @@ -548,7 +554,8 @@ TEST_CASE("FrameTransformation_Converted_bshuf_zstd_unsigned_16bit" ,"") { auto image = transformation.GetCompressedImage(); REQUIRE(read_be64(image.GetCompressed()) == experiment.GetPixelsNum() * experiment.GetByteDepthImage()); - REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::DefaultBlockSize * + REQUIRE(read_be32(image.GetCompressed() + 8) == JFJochBitShuffleCompressor::BlockSize(experiment.GetCompressionAlgorithm(), + experiment.GetByteDepthImage()) * experiment.GetByteDepthImage()); std::vector output; diff --git a/writer/HDF5DataFile.cpp b/writer/HDF5DataFile.cpp index 378b1ca3..88fc38ed 100644 --- a/writer/HDF5DataFile.cpp +++ b/writer/HDF5DataFile.cpp @@ -140,7 +140,9 @@ void HDF5DataFile::CreateFile(const DataMessage& msg, std::shared_ptr xpixel = msg.image.GetWidth(); ypixel = msg.image.GetHeight(); - dcpl.SetCompression(msg.image.GetCompressionAlgorithm(), JFJochBitShuffleCompressor::DefaultBlockSize); + dcpl.SetCompression(msg.image.GetCompressionAlgorithm(), + JFJochBitShuffleCompressor::BlockSize(msg.image.GetCompressionAlgorithm(), + msg.image.GetByteDepth())); dcpl.SetChunking( {1, ypixel, xpixel}); H5Pset_fill_time(dcpl.GetID(), H5D_FILL_TIME_NEVER);