最近对CPU计算优化的兴趣来自于,在ADC上听到彦军关于CPU寄存器优化的报告,以及最近读的关于HANA数据库的书。这里借前不久接触过的CRC32性能优化整理一下所学知识。本文算法的内容主要翻译整理自http://create.stephan-brumme.com/crc32/,CPU优化内容加入了我的一些个人理解
基本算法:按位计算
const uint32_t Polynomial = 0xEDB88320;
uint32_t crc32_bitwise(const void* data, size_t length, uint32_t previousCrc32 = 0)
{
uint32_t crc = ~previousCrc32;
unsigned char* current = (unsigned char*) data;
while (length--)
{
crc ^= *current++;
for (unsigned int j = 0; j < 8; j++)
if (crc & 1)
crc = (crc >> 1) ^ Polynomial;
else
crc = crc >> 1;
}
return ~crc;
}
Polynomial是CRC32算法中的多项式常量,由于CRC32的算法数学基础是伽罗华域上的多项式运算,这里给出的0xEDB88320是少数性质较好的参数之一。外层while循环每次编码一个byte,而内层for循环则循环其中每一个bit,最内层的if-else根据结果值末位bit选择不同的编码分支。
该算法原文给出的性能是35MBps
改进1:优化CPU分支预测
由于CRC要求checksum值无规律分布,基本算法中的if-else很容易产生CPU分支预测失败(branch misprediction)。
- 现代CPU引入指令流水线(instruction pipeline)机制优化多个指令并行执行不同的Stage.
- 由于分支指令会破坏流水线并行,还引入分支预测器(branch predictor),它帮助CPU猜测最可能的分支,将该分支的指令加载到pipeline。
- 一次分支预测失败将导致后续若干个CPU周期浪费,最大影响周期数取决于CPU的pipline长度,通常为10-20,例如最新的Intel Haswell架构处理器拥有14个stage。
uint32_t crc32_bitwise(const void* data, size_t length, uint32_t previousCrc32 = 0)
{
uint32_t crc = ~previousCrc32;
unsigned char* current = (unsigned char*) data;
while (length--)
{
crc ^= *current++;
for (unsigned int j = 0; j < 8; j++)
crc = (crc >> 1) ^ (-int(crc & 1) & Polynomial);
}
return ~crc;
}
正如这篇文章提到,减少分支预测失败的最有效途径是,尽可能避免分支判断。改进算法中,表达式(-int(crc & 1) & Polynomial),当crc末位为1值为Polynomial,当crc末位为0时值为0。那么crc = (crc >> 1) ^ (-int(crc & 1) & Polynomial)与基本算法中的if-else结算结果是一致的。
虽然改进的算法增加了一些逻辑运算,但成功避免了频率很高的分支操作,从而避免了分支预测造成的CPU周期浪费,性能大幅提升,原文给出的性能是70MBps,比基础算法提升一倍。
改进2:查表优化
目前使用最广泛的CRC32算法是Dilip V Sarwate在1988年提出的查表算法,该算法发表在ACM的原文在此(文章后面也附有CRC算法的数学原理)。
uint32_t crc32Lookup[256];
//初始化数组值
for (unsigned int i = 0; i <= 0xFF; i++)
{
uint32_t crc = i;
for (unsigned int j = 0; j < 8; j++)
crc = (crc >> 1) ^ (-int(crc & 1) & Polynomial);
crc32Lookup[i] = crc;
}
//计算CRC32
uint32_t crc32_1byte(const void* data, size_t length, uint32_t previousCrc32 = 0)
{
uint32_t crc = ~previousCrc32;
unsigned char* current = (unsigned char*) data;
while (length--)
crc = (crc >> 8) ^ crc32Lookup[(crc & 0xFF) ^ *current++];
return ~crc;
}
该算法将内层的for循环(循环8次,每次都需要进行位移和逻辑计算)简化为一次查表操作。注意到每隔for循环,实际上是输入crc作为初始参数,然后又以crc作为结果输出。显然该算法需要预先一次性计算生成查询数组,并占住1KB内存。
该算法的另一个巧妙之处在于,并没有根据输入参数的值域(32位整型)建立查询数组(否则容量将非常大),而是抽象出8位查询表,每次输入的值其实是crc的末8位(crc & 0xFF),这是因为算法利用了以下两个运算性质,算法的等价性推导并不困难,本文不详述:
- 异或运算交换率:(A^B)^C = A^(B^C)
- 异或运算对位移运算的结合律:(A^B)>>x = (A>>x)^(B>>x)
根据原文提供的数据,Dilip的算法将性能进一步提升到之前的5倍以上,达到了375MBps。由于性能已经超过磁盘或网络的IO速度,该算法一直以来被许多开源工程采用。
改进3:多表查询
2006年Intel工程师提出了更为先进的CRC32算法Slicing-by-8,其最显著变化是,将原有查询数组扩展到8个(8*256二维数组),而外层循环次数为原来的1/8。该算法很容易理解,需要注意的是由于每次循环8byte,末尾可能会遗留1-7个byte,这部分就按照改进算法2逐个byte计算。
uint32_t** crc32Lookup; //8*256二维数组,省略数组声明代码
//第一行的初始化方法同上
for (unsigned int i = 0; i <= 0xFF; i++)
{
uint32_t crc = i;
for (unsigned int j = 0; j < 8; j++)
crc = (crc >> 1) ^ ((crc & 1) * Polynomial);
crc32Lookup[0][i] = crc;
}
//赋值其它行
for (unsigned int i = 0; i <= 0xFF; i++)
{
// for Slicing-by-4 and Slicing-by-8
crc32Lookup[1][i] = (crc32Lookup[0][i] >> 8) ^ crc32Lookup[0][crc32Lookup[0][i] & 0xFF];
crc32Lookup[2][i] = (crc32Lookup[1][i] >> 8) ^ crc32Lookup[0][crc32Lookup[1][i] & 0xFF];
crc32Lookup[3][i] = (crc32Lookup[2][i] >> 8) ^ crc32Lookup[0][crc32Lookup[2][i] & 0xFF];
// only Slicing-by-8
crc32Lookup[4][i] = (crc32Lookup[3][i] >> 8) ^ crc32Lookup[0][crc32Lookup[3][i] & 0xFF];
crc32Lookup[5][i] = (crc32Lookup[4][i] >> 8) ^ crc32Lookup[0][crc32Lookup[4][i] & 0xFF];
crc32Lookup[6][i] = (crc32Lookup[5][i] >> 8) ^ crc32Lookup[0][crc32Lookup[5][i] & 0xFF];
crc32Lookup[7][i] = (crc32Lookup[6][i] >> 8) ^ crc32Lookup[0][crc32Lookup[6][i] & 0xFF];
}
//计算CRC32
uint32_t crc32_8bytes(const void* data, size_t length, uint32_t previousCrc32 = 0)
{
uint32_t* current = (uint32_t*) data;
uint32_t crc = ~previousCrc32;
// process eight bytes at once
while (length >= 8)
{
uint32_t one = *current++ ^ crc;
uint32_t two = *current++;
crc = crc32Lookup[7][ one & 0xFF] ^
crc32Lookup[6][(one>> 8) & 0xFF] ^
crc32Lookup[5][(one>>16) & 0xFF] ^
crc32Lookup[4][ one>>24 ] ^
crc32Lookup[3][ two & 0xFF] ^
crc32Lookup[2][(two>> 8) & 0xFF] ^
crc32Lookup[1][(two>>16) & 0xFF] ^
crc32Lookup[0][ two>>24 ];
length -= 8;
}
unsigned char* currentChar = (unsigned char*) current;
// remaining 1 to 7 bytes
while (length--)
crc = (crc >> 8) ^ crc32Lookup[0][(crc & 0xFF) ^ *currentChar++];
return ~crc;
}
Slicing-by-8的性能达到了惊人的1.8GBps,在Dilip算法基础之上又提升了5倍。该算法是BSD协议的,作者也声称未申请专利。
改进4:借助Intel SSE指令
Intel在2009年发布了Nehalem架构处理器,随之发布是SSE4.2指令集(共7条新增指令),其中就包括CRC32指令。该指令的延时是3个cycle,吞吐率是(每64bit)1cycle。这篇文章提供了更多Intel对该指令的官方介绍。本文引用的代码是http://www.evanjones.ca/crc32c.html提供的,它使用了GCC4.4版本的buildin函数(基于CRC32指令)
运行前请使用gcc -v检查GCC版本,以及使用cat /proc/cpuinfo判断你的主机CPU是否支持SSE4.2指令
uint32_t crc32cHardware64(uint32_t crc, const void* data, size_t length) {
const char* p_buf = (const char*) data;
uint64_t crc64bit = crc;
for (size_t i = 0; i < length / sizeof(uint64_t); i++) {
crc64bit = __builtin_ia32_crc32di(crc64bit, *(uint64_t*) p_buf);
p_buf += sizeof(uint64_t);
}
uint32_t crc32bit = (uint32_t) crc64bit;
length &= sizeof(uint64_t) - 1;
while (length > 0) {
crc32bit = __builtin_ia32_crc32qi(crc32bit, *p_buf++);
length--;
}
return crc32bit;
}
与改进算法3相比,循环逻辑是类似的,但不再需要建表和查表,让我们看看该代码作者提供的bench程序的测试结果吧,测试主机CPU为Intel E5620,GCC版本是4.4.5,截取了与SlicingBy8比较的部分:
function,aligned,bytes,cycles,cycles,cycles,cycles,cycles
crc32cSlicingBy8,false,15,7158,900,918,888,888
crc32cSlicingBy8,false,63,3798,1758,1764,1746,1752
crc32cSlicingBy8,false,255,8496,5976,6006,5982,5976
crc32cSlicingBy8,false,1023,23148,22854,22896,22836,22914
crc32cSlicingBy8,false,4095,89832,104196,89328,89940,89502
crc32cSlicingBy8,false,8191,178926,178026,177960,178050,178704
crc32cSlicingBy8,false,16383,361542,362028,360684,446940,372492
crc32cSlicingBy8,true,16,1026,726,690,678,630
crc32cSlicingBy8,true,64,1728,1602,1650,1584,1638
crc32cSlicingBy8,true,256,6090,5832,5814,5778,5838
crc32cSlicingBy8,true,1024,22908,22782,22842,22764,22782
crc32cSlicingBy8,true,4096,89400,89142,89706,89376,89412
crc32cSlicingBy8,true,8192,178332,177936,179112,178236,178932
crc32cSlicingBy8,true,16384,444360,373932,361554,361044,361680
crc32cHardware64,false,15,606,444,444,438,450
crc32cHardware64,false,63,462,324,348,336,348
crc32cHardware64,false,255,1200,1050,1068,1032,1068
crc32cHardware64,false,1023,4992,4950,4968,4950,4962
crc32cHardware64,false,4095,22488,22482,22482,22488,22482
crc32cHardware64,false,8191,45900,45882,45888,45888,45882
crc32cHardware64,false,16383,92688,92682,92688,92682,92682
crc32cHardware64,true,16,378,162,186,168,186
crc32cHardware64,true,64,492,342,372,348,366
crc32cHardware64,true,256,1230,1068,1092,1068,1080
crc32cHardware64,true,1024,5082,5034,5094,5046,5076
crc32cHardware64,true,4096,22368,22320,22380,22332,22356
crc32cHardware64,true,8192,45414,45354,45420,45372,45396
crc32cHardware64,true,16384,91488,91434,91500,91452,129942
从结果看,对于大数据块的CRC32计算,几乎仅使用了1/3的CPU周期数,可以视作性能为原来的3倍。
小结,从CRC32算法的进化,可以看出CPU性能挖掘的重要价值,本文涉及到的分支预测,查表优化和单指令多数据流(SIMD)仅仅是CPU优化技术的一部分,还有许多跟多核心、缓存与寄存器有关的优化点需要学习。