From 5f340db48db07f138fd67580269afdfeb5c46e1a Mon Sep 17 00:00:00 2001 From: Julian Xhokaxhiu Date: Sun, 2 Aug 2026 23:11:05 +0200 Subject: [PATCH] Improve CRC32 by using the slice-by-16 optimization --- src/SharpCompress/Crypto/Crc32Stream.cs | 77 ++++++++++++++++++++----- 1 file changed, 64 insertions(+), 13 deletions(-) diff --git a/src/SharpCompress/Crypto/Crc32Stream.cs b/src/SharpCompress/Crypto/Crc32Stream.cs index 67dcb913..8d72f23f 100644 --- a/src/SharpCompress/Crypto/Crc32Stream.cs +++ b/src/SharpCompress/Crypto/Crc32Stream.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers.Binary; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -119,6 +120,14 @@ public sealed class Crc32Stream : Stream public static uint Compute(uint polynomial, uint seed, ReadOnlySpan buffer) => ~CalculateCrc(InitializeTable(polynomial), seed, buffer); + // Number of "slice-by-N" tables generated per polynomial (see InitializeTable). 16 gives a + // good throughput/table-size tradeoff (16 * 256 * 4 bytes = 16KB per polynomial, generated + // once and cached for the default polynomial) - a well-known technique (zlib-ng, Chromium, + // etc.) for speeding up table-driven CRC32 by processing 16 bytes/iteration through 16 + // independent (parallelizable) table lookups instead of 1 byte/iteration through a single + // lookup on the CPU's dependency chain. + private const int SliceTableCount = 16; + internal static uint[] InitializeTable(uint polynomial) { if (polynomial == DEFAULT_POLYNOMIAL && _defaultTable != null) @@ -126,38 +135,80 @@ public sealed class Crc32Stream : Stream return _defaultTable; } - var createTable = new uint[256]; + // table[0..255] is the ordinary byte-wise CRC table (unchanged layout, so existing + // single-byte callers - CalculateCrc(table, crc, byte) - keep working unmodified). + // table[256*k .. 256*k+255] for k=1..SliceTableCount-1 are the derived "slice" tables: + // table[k][i] = (table[k-1][i] >> 8) ^ table[0][table[k-1][i] & 0xFF], i.e. each table + // folds in the effect of a byte that is k positions further back in the stream than + // the byte table[0] would have processed - letting CalculateCrc(span) below combine 4 + // bytes' worth of independent lookups per 32-bit word with no cross-lookup dependency. + var table = new uint[256 * SliceTableCount]; for (var i = 0; i < 256; i++) { var entry = (uint)i; for (var j = 0; j < 8; j++) { - if ((entry & 1) == 1) - { - entry = (entry >> 1) ^ polynomial; - } - else - { - entry >>= 1; - } + entry = (entry & 1) == 1 ? (entry >> 1) ^ polynomial : entry >> 1; } + table[i] = entry; + } - createTable[i] = entry; + for (var k = 1; k < SliceTableCount; k++) + { + var prevBase = (k - 1) * 256; + var curBase = k * 256; + for (var i = 0; i < 256; i++) + { + var prev = table[prevBase + i]; + table[curBase + i] = (prev >> 8) ^ table[prev & 0xFF]; + } } if (polynomial == DEFAULT_POLYNOMIAL) { - _defaultTable = createTable; + _defaultTable = table; } - return createTable; + return table; } internal static uint CalculateCrc(uint[] table, uint crc, ReadOnlySpan buffer) { unchecked { - for (var i = 0; i < buffer.Length; i++) + var i = 0; + if (table.Length >= 256 * SliceTableCount) + { + while (buffer.Length - i >= 16) + { + var word0 = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(i)) ^ crc; + var word1 = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(i + 4)); + var word2 = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(i + 8)); + var word3 = BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(i + 12)); + + crc = + table[15 * 256 + (byte)word0] + ^ table[14 * 256 + (byte)(word0 >> 8)] + ^ table[13 * 256 + (byte)(word0 >> 16)] + ^ table[12 * 256 + (word0 >> 24)] + ^ table[11 * 256 + (byte)word1] + ^ table[10 * 256 + (byte)(word1 >> 8)] + ^ table[9 * 256 + (byte)(word1 >> 16)] + ^ table[8 * 256 + (word1 >> 24)] + ^ table[7 * 256 + (byte)word2] + ^ table[6 * 256 + (byte)(word2 >> 8)] + ^ table[5 * 256 + (byte)(word2 >> 16)] + ^ table[4 * 256 + (word2 >> 24)] + ^ table[3 * 256 + (byte)word3] + ^ table[2 * 256 + (byte)(word3 >> 8)] + ^ table[1 * 256 + (byte)(word3 >> 16)] + ^ table[word3 >> 24]; + + i += 16; + } + } + + for (; i < buffer.Length; i++) { crc = CalculateCrc(table, crc, buffer[i]); }