为什么C#中的CRC32实现如此之慢?

我正在使用以下函数来计算VS2008,.NET 3.5项目中文件的CRC32:

public UInt32 ComputeHash(System.IO.Stream stream) { unchecked { const int BUFFER_SIZE = 1024; UInt32 crc32Result = 0xFFFFFFFF; byte[] buffer = new byte[BUFFER_SIZE]; int count = stream.Read(buffer, 0, BUFFER_SIZE); while (count > 0) { for (int i = 0; i > 8) ^ _crc32Table[(buffer[i]) ^ (crc32Result) & _LOOKUP_TABLE_MAX_INDEX]; } count = stream.Read(buffer, 0, BUFFER_SIZE); } return ~crc32Result; } } 

为简洁起见,我遗漏了构建查找表(_crc32Table)的函数。 该表是一个UInt32数组,是在实例化类时构建的,包含256个值(256也是_LOOKUP_TABLE_MAX_INDEX + 1的值)。

我已经运行了一些基准测试,将它与MD5CryptoServiceProvider和SHA1CryptoServiceProvider ComputeHash函数进行比较,它们的速度要快得多。 MD5function的速度提高了两倍,SHA1哈希的速度提高了约35%。 我被告知CRC32很快,但那不是我所看到的。

我在我的假设中弄错了吗? 这是预期的还是这个算法有缺陷?

您正在将代码与内置函数进行比较,并询问它们为何更快。 您需要做的是找到内置函数的源代码。 他们是如何工作的? 看看有什么不同。

Betcha内置函数调用本机库,并且不必在托管内存框架内运行就作弊。

分析可能有助于确定IO调用(读取)与CRC计算所花费的时间。 代码通常是IO绑定的。 但是,作为在C#中实现了相当快的CRCfunction的人,我可以给出一些指示,说明为什么它比MD5慢。

  • 你一次读取一个字节的内存。 实现四个切片,这样你就可以一次读取四个字节,或者可以一次读取八个字节,这样你就可以一次读取八个字节(但只有当代码实际上以64位模式运行时 – 你应该回退一下在32位模式下切换为四,您应该使用if(sizeof(IntPtr)<8)或类似的方法进行测试)。
  • 您正在为每个循环迭代处理一个字节,从而为每个字节支付循环开销。 实现切片N或者考虑循环展开。 (两者都可能是不必要的。)
  • 您每个字节需要进行两次数组边界检查。 您可以使用“不安全”代码来避免边界检查。 对于不安全的代码,您还应确保对齐读取指针,尽管由于您只访问.NET数组,因此您可能认为它们已经与机器字大小对齐。 请注意,不安全的代码是不安全的,所以要小心!
  • MD5被设计为一种非常快速的算法,并没有上面列出的问题。 它一次读取多个字节并并行处理它们,并在非托管代码中实现。
  • 这是次要的,但您的循环结构不是最佳的。 既然知道count!= 0,那么预先递减计数(即–count)并且比较为零的do / while循环比比较两个变量的for循环更好。 使用您的代码,这将节省一些指令,并且可能每个字节读取一次内存。

如果实现切片为N,则将所有查找表打包到一个大表中,以便可以通过相同的指针访问它们。 您也可以使用相同的表进行4切片和8切片。 另请注意,典型的N切片实现会假定特定的机器字节序,因此您可能需要为big-endian机器提供单独的版本,您可以使用BitConverter.IsLittleEndian进行检查。

我不太熟悉在执行此代码时自动执行的优化,但如果分析不适合您,则有几个选项。

我可以建议尝试不安全的代码,并使用指针运算缓冲区[i]和_crc32Table查找,以防它尚未被优化。

我可以看到你遇到性能问题的另一个地方是Stream.Read调用。 您是否尝试过不同的BUFFER_SIZE值?

如果没有自动优化,使用更大的字节缓冲区并可能进行一些手动循环展开也可以帮助您。

也许:你在计算CRC吞吐量的时候计算查找表吗? 通常,查找表计算一次并缓存。 如果不缓存它,则每次计算CRC时都会计算它。 此外,如果您只测量一个CRC,那么您可能已将表计算成本包括在CRC计算成本中。 最好测量每个哈希的多次迭代。


附录 :当我测量时,我看到了2.6倍的因子,比较你的CRC32和MD5哈希值,当应用程序是用/ debug +和/ optimize-编译的时候。 使用/ debug-和/ optimize +,我看到了1.6倍的因子。 当我更改编译标志时,绝对MD5性能没有变化。 没有调试,CRC仍然较慢,但它更接近。