高性能CRC32

最近对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)。

300px-5_Stage_Pipeline.svg

  • 现代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优化技术的一部分,还有许多跟多核心、缓存与寄存器有关的优化点需要学习。

    你可能感兴趣的:(CRC,SIMD,CRC32)