Recent Intel processors (Core i7/i5) can calculate CRC-32C and bit count in hardware. With other domain-specific instructions (string handling, AES encryption) in SSE 4.2, it may seem that low-level optimization for these algorithms will soon become irrelevant, because the hardware acceleration is always faster. But do the new instructions cover all usage cases?
CRC32 instruction
CRC (Cyclic Redundancy Check) is a remainder from dividing your message by a polynomial. Most popular file formats and protocols (Ethernet, MPEG-2, ZIP, RAR, 7-Zip, GZip, and PNG) use the polynomial 0x04C11DB7, while Intel's hardware implementation is based on another polynomial, 0x1EDC6F41 (used in iSCSI and Btrfs). Newly designed protocols and formats can choose the second polynomial to benefit from hardware acceleration, but CRC-32 with polynomial 0x04C11DB7 has to be calculated in software. The CRC32 instruction is not supported by AMD processors.
Most programmers use Dilip Sarwate's table-lookup code for calculating CRC. Intel researchers Michael Kounavis and Frank Berry devised a fast slicing algorithm with unrolled loop and precalculated shifted CRC values (thanks to Andrew M. for pointing this out). The optimized version cannot beat hardware implementation, but it's useful if you have to use the polynomial 0x04C11DB7 or if your processor has no support for the CRC32 instruction.
Another unrolled implementation by Richard Black is included for comparison. It's used in Linux kernel and Info-ZIP.
File size, bytes | 250 | 1025 | 4103 | 16384 | 65537 | 262151 | 1048576 |
---|---|---|---|---|---|---|---|
Dilip Sarwate | 1529 | 6049 | 24003 | 95643 | 382383 | 1529357 | 6231023 |
Richard Black | 1629 | 6472 | 25709 | 102469 | 409695 | 1638652 | 6603394 |
Slicing-by-4 | 823 | 3146 | 12326 | 48949 | 195577 | 781806 | 3133560 |
Slicing-by-8 | 529 | 1820 | 6946 | 27651 | 111392 | 445292 | 1788348 |
Hardware | 251 | 732 | 2660 | 10329 | 41057 | 163957 | 655555 |
Hardware, unrolled | 263 | 760 | 2663 | 10355 | 41080 | 163963 | 655574 |
The benchmarks were run on Core i5-750 processor; time is reported in clock cycles. File size is not always a power of 2 to avoid bias in favor of unrolled implementations.
Unrolling does not provide any benefit because of dependency chain between CRC32 instructions. 64-bit version of the instruction should be faster (not tested yet).
PopCnt instruction
Calculating the number of bits set (population count) is useful for implementing sparse arrays or finding Hamming distance. IBM Stretch, CDC 6000, SPARC V9, and Alpha 21264A processors have an instruction for this purpose. Previously, there were no such instruction in x86 ISA, but now it's implemented by Intel and AMD.
AMD also added support for LZCNT instruction, which can be replaced with BSR and CMOVZ. Intel does not implement this instruction.
File size, bytes | 250 | 1025 | 4103 | 16384 | 65537 | 262151 | 1048576 |
---|---|---|---|---|---|---|---|
Bit hacks | 569 | 2080 | 8055 | 31909 | 126766 | 507126 | 2036128 |
Table | 474 | 1723 | 6565 | 25940 | 103983 | 415989 | 1670457 |
SSE2 | 212 | 566 | 1783 | 6749 | 26797 | 107134 | 433857 |
Hardware | 206 | 683 | 2443 | 9426 | 37466 | 149263 | 606648 |
Hardware by Subbu N | 215 | 546 | 1829 | 6946 | 30063 | 119349 | 484509 |
Hardware, unrolled | 206 | 451 | 1226 | 4572 | 17523 | 69940 | 286254 |
SSSE3 | 169 | 423 | 1195 | 4266 | 16931 | 68083 | 277169 |
The branchless software implementation uses clever bit hacks to avoid branching: the adjacent bits are summed up in a tree-like pattern. Another function just looks up a 256-element table with number of bits for each possible byte.
SSE2 implementation is based on algorithm from the book by Henry S. Warren, Jr. The formulas were slightly changed to adapt for SSE2 instruction set. Bit count is accumulated in a byte vector. When it can overflow a byte variable, the bytes are horizontally summed up into a DWORD vector. In the original version of the program, PUNPCK* instructions were used for that. Wojciech Muła found a faster way with PSADBW instruction (packed sum of absolute differences between bytes). Finally, the results are summed up horizontally again into a single 32-bit register.
SSSE3 implementation is devised by Wojciech Muła. Each byte is separated into two nibbles (4 bits). The PSHUFB instruction looks up a 16-element table located in a XMM register (similar to the table method). The resulting bytes are summed up horizontally. The only disadvantage of the method: SSSE3 (PSHUFB instruction) is not supported on AMD processors.
Data alignment speeds up the code by 8% and makes it more portable (thanks to Dmitry for the idea!) To avoid the alignment loop for POPCNT, you can read the first partial word and mask out invalid bytes. Agner Fog uses this trick in his strlen implementation. A similar code reads the last partial word without unnecessary branches. SSE2 and SSSE3 functions also benefit from this optimization.
Conclusion
The hardware-accelerated CRC32 and population count are useful additions to x86 ISA. They are 2-3 times faster than smartly optimized calculations in software. However, old optimization tricks are still relevant if you have to use another polynomial for CRC. In this case, you can use "Slicing-by-8" algorithm.
For population count, PSHUFB (SSSE3) is 3% faster than the specialized POPCNT instruction. You can use POPCNT (unrolled hardware implementation) if you need to target AMD processors.
Download the code (MSVC, 30 KB)
44 comments
Piotr, it is indeed an excellent and very useful generalisation of CRC32 algorithms! SSE2 is a winner for a generic-level CPUs, not speaking about CRC32 intrinsic.
While reviewing implementations I would like to add some remarks for those who might consider using Slicing-By-8 algoritm (as a winner within non-SIMD powered algorithms) on platforms with srict data alignment requirements, for example ARM. All algorithms (except Dilip Sarwate) read a byte stream into a 32-bit integer through pointer casting. If bytestream is not aligned to 4-bytes your application will trigger hardware exception.
To circumvent this problem algorithm can be sligtly modified to have alignment requirements fulfilled:
Here you can notice
while (((((intptr_t)buf) & (sizeof(DWORD)-1)) != 0) && (len != 0))
aligning loop. Such alignment does not affect performance and provides robust operations on any platform.I made test results, and non-surprisingly 64-bit version of Slicing-by-8 which aligns data to 4-bytes wins from its unaligned parent by around 8%!
Thank you very much! You can avoid the second loop condition by calculating the minimum of required alignment and length:
I will download a 64-bit compiler and do more tests with alignment in the next few days. Thanks again.
for (SIZE_T i = 0; i < NUM_OF(crc_funcs); i++)
TestPopCntFunc(popcnt_funcs[i]);
should be:
for (SIZE_T i = 0; i < NUM_OF(popcnt_funcs); i++)
TestPopCntFunc(popcnt_funcs[i]);
TestPopCntFunc(POPCNT_Hardware);
TestPopCntFunc(POPCNT_HardwareUnrolled);
create an exception on a processor not having hardware popcnt.
Here is one routine when no hardware popcnt is available:
int POPCNT64(UINT64 v)
{
unsigned int v1, v2;
v1 = (unsigned int) (v & 0xFFFFFFFF);
v1 -= (v1 >> 1) & 0x55555555;
v1 = (v1 & 0x33333333) + ((v1 >> 2) & 0x33333333);
v1 = (v1 + (v1 >> 4)) & 0x0F0F0F0F;
v2 = (unsigned int) (v >> 32);
v2 -= (v2 >> 1) & 0x55555555;
v2 = (v2 & 0x33333333) + ((v2 >> 2) & 0x33333333);
v2 = (v2 + (v2 >> 4)) & 0x0F0F0F0F;
return ((v1 * 0x01010101) >> 24) + ((v2 * 0x01010101) >> 24);
}
Is it possible to do better ?
(not much information on this particular function on the web)
Anyway, I found some solutions.
If somebody is interested, I can post code here...
You can find 4 solutions with source code and timing results posted here:
http://www.z390.org/z390_Mainframe_Assemble_Coding_Contest.htm
Don Higgins
don@higgins.net
http://multi-up.com/318633
in C++ and optimized ASM.
http://lxr.linux.no/linux+v2.6.36/lib/crc32.c
Where does it stands in the benchmark table (is it "Richard Black"?)
2. We are using Java latest J2SE JDK which uses CRC32 for DEFALTE (on 64Bit machine)
Anyone knows which algorithm Sun uses?
Thanks
your implementation of POPCNT_Hardware() is sensitive to data alignment issue and also has performance issue. The modified one below rectifies both the issue.
1. Yaniv, I already mentioned in the article that "Richard Black" is used in Linux kernel. 2. If the Java virtual machine is not open source, you have no practical way to find out.
Subbu N, thank you very much! I added your function to the benchmark. Surprisingly, reversed memory access (from the end to the beginning of array) is fast! Usually, it's much slower, so you have to use a special trick, counting from "minus length" to zero:
But on my Core i5, there is no difference between this code and yours! Probably, they optimized reversed memory access in the recent processors.
A CRC instruction with arbitrary polynomial would be slower. The classic hardware implementation is a feedback shift register with hardwired polynomial (bit-by-bit calculation). Intel most likely uses a lookup table in ROM (lower latency; does not need 32 iterations to get the result).
But I wonder why they choose the iSCSI polynomial, which is rarer than 0x04C11DB7.
Here are the results from Sandy Bridge. I have compiled x86 and x64 versions, both with and without AVX
x86 results
x64 results
I don't know the reason behind the exceptionally high performance of hardware POPCNT/CRC32 in x64 version, but I've checked that the compiler didn't throw up the entire function.
Thank you very much for reporting this.
On x64, inline assembly is not available. If you replace the inline assembly block with intrinsic __rdtsc:
then the compiler will inline BenchmarkFunction and stupidly reorder __rdtsc calls:
So it no longer measures the runtime of CRC_Hardware, because the first rdtsc is moved down.
I tried to make GetRDTSC volatile, but it does not help. In this program, you can forbid to inline the function using /Ob1 (in IDE: Optimization → Inline Function Expansion → Only __inline). After that, you will get realistic results:
Tested on Intel Core i5 750 in 64-bit mode.
See also _ReadWriteBarrier (http://msdn.microsoft.com/en-us/library/f20w0x5e(v=vs.90).aspx) which prevents the compiler reordering of reads and writes to global variables.
Yes, that was likely the case. I recompiled with
and I also disabled buffer security check.
x86 results:
x64 results:
And also the results with 64-bit POPCNT and CRC32C (all tests on 64-bit code)
The code
Thank you! Both functions are much faster in the 64-bit version; it's also interesting that SSSE3 code using AVX is still faster than the 64-bit POPCNT instruction. I will add 64-bit benchmarks to the article.
Ace, thank you; I will try _ReadWriteBarrier.
Thanks,
Helped me a lot,i thought i optimized my code but i guess you can always do better.
Spasiba moi druge
Glad I found your site.
Great contribution to science here.
Was thinking there had to be something better then the plain old table way.
If the table memory access has anything to do with it anyhow.
Can you explain how "Align to DWORD boundary" works?
and what the g_crc_slicing do
thanks
The code first calculates the number of bytes to the next DWORD boundary (0..3 bytes). If the length is smaller than that, only len bytes with be processed.
You can read about the mathematics of g_crc_slicing in Intel's paper.
Too bad there isn't an SSE instruction to do column-wise popcount: given a vector of m128 elements, count the number of bits set in the 0th bit of each element, then count the number of bits set in the 1st bit of each element, etc (128 counters in total, regardless of vector size).
I'm forced to count the old fashioned way, with shifts and masks:
The performance is awful - 11 ops and 4 memory accesses for every element in the vector, plus large overhead every 31 elements - much slower than memory speed...
Song: there's a neat trick described at http://steike.com/code/bits/vertical-counter/ that does vertical bit counting.
Hi, paper Anatomy of High-Performance 2D Similarity Calculations (http://www.cs.stanford.edu/people/ihaque/#publications) contains some real-life comparison of popcount procedures. SSSE3 method is nearly as fast as hardware popcnt instruction.
Very cool article, thanks for gathering the algorithms and explaining the experiment.
One thing to note, is that SSE2/SSE3 popcount techniques work on 16byte chunks, so if you need to work on singe words here and there they are not that convenient.
Peter, great website and an interesting comparison. For completeness would be nice if you used the 64-bit crc32 version - should provide an extra boost.
Btw, in Song's question/solution, doesn't he need to sum every 15 counts, not 31?
Marcin, thank you for the kind words. Yes, it should be 15 counts, not 31. I will do the benchmarks for the 64-bit crc32 instruction in future.
Peter, CRC32 has a latency of 3 cycles and a throughput of 1 cycle.
Using 64-bit instruction and splitting input buffer into three chunks will make hardware implementation noticeably faster.