我将违背普遍的看法,即std::copy 将有轻微的、几乎无法察觉的性能损失。我刚刚做了一个测试,发现这不是真的:我确实注意到了性能差异。但是,获胜者是std::copy。
我编写了一个 C++ SHA-2 实现。在我的测试中,我使用所有四个 SHA-2 版本(224、256、384、512)对 5 个字符串进行哈希处理,并且循环了 300 次。我使用 Boost.timer 测量时间。那 300 个循环计数器足以完全稳定我的结果。我每次运行测试 5 次,在 memcpy 版本和 std::copy 版本之间交替进行。我的代码利用尽可能大的数据块来获取数据(许多其他实现使用char/char *,而我使用T/T *(其中T是最大的类型)具有正确溢出行为的用户实现),因此对最大类型的快速内存访问是我算法性能的核心。这些是我的结果:
完成运行 SHA-2 测试的时间(以秒为单位)
std::copy memcpy % increase
6.11 6.29 2.86%
6.09 6.28 3.03%
6.10 6.29 3.02%
6.08 6.27 3.03%
6.08 6.27 3.03%
std::copy 相对于 memcpy 的总平均速度提升:2.99%
我的编译器是 Fedora 16 x86_64 上的 gcc 4.6.3。我的优化标志是-Ofast -march=native -funsafe-loop-optimizations。
Code for my SHA-2 implementations.
我决定也对我的 MD5 实现进行测试。结果不太稳定,所以我决定运行 10 次。然而,在我最初的几次尝试之后,我得到的结果从一次运行到下一次变化很大,所以我猜测有某种操作系统活动正在进行。我决定重新开始。
相同的编译器设置和标志。 MD5 只有一个版本,它比 SHA-2 更快,所以我在一组类似的 5 个测试字符串上进行了 3000 次循环。
这是我最后的 10 个结果:
完成运行 MD5 测试的时间(以秒为单位)
std::copy memcpy % difference
5.52 5.56 +0.72%
5.56 5.55 -0.18%
5.57 5.53 -0.72%
5.57 5.52 -0.91%
5.56 5.57 +0.18%
5.56 5.57 +0.18%
5.56 5.53 -0.54%
5.53 5.57 +0.72%
5.59 5.57 -0.36%
5.57 5.56 -0.18%
std::copy 在 memcpy 上的总平均速度下降:0.11%
Code for my MD5 implementation
这些结果表明 std::copy 在我的 SHA-2 测试中使用了一些优化,std::copy 在我的 MD5 测试中无法使用。在 SHA-2 测试中,两个数组都是在名为 std::copy / memcpy 的同一函数中创建的。在我的 MD5 测试中,其中一个数组作为函数参数传递给函数。
我做了更多的测试,看看我可以做些什么来让std::copy 再次变得更快。答案很简单:开启链接时间优化。这些是我打开 LTO 的结果(gcc 中的选项 -flto):
使用 -flto 完成运行 MD5 测试的时间(以秒为单位)
std::copy memcpy % difference
5.54 5.57 +0.54%
5.50 5.53 +0.54%
5.54 5.58 +0.72%
5.50 5.57 +1.26%
5.54 5.58 +0.72%
5.54 5.57 +0.54%
5.54 5.56 +0.36%
5.54 5.58 +0.72%
5.51 5.58 +1.25%
5.54 5.57 +0.54%
std::copy 相对于 memcpy 的总平均速度提升:0.72%
总之,使用std::copy 似乎没有性能损失。事实上,似乎有性能提升。
结果说明
那么为什么std::copy 会提升性能呢?
首先,只要打开内联优化,我不希望它对任何实现都变慢。所有编译器都积极内联;它可能是最重要的优化,因为它支持许多其他优化。 std::copy 可以(我怀疑所有现实世界的实现都可以)检测到参数是可简单复制的,并且内存是按顺序排列的。这意味着在最坏的情况下,当memcpy 合法时,std::copy 的性能应该不会变差。遵循 memcpy 的 std::copy 的简单实现应该满足编译器的“在优化速度或大小时始终内联它”的标准。
不过,std::copy 也会保留更多信息。当您调用std::copy 时,该函数会保持类型不变。 memcpy 在void * 上运行,它丢弃了几乎所有有用的信息。例如,如果我传入std::uint64_t 的数组,编译器或库实现者可能能够利用std::copy 的64 位对齐,但使用memcpy 可能更难做到这一点。像这样的算法的许多实现首先处理范围开头的未对齐部分,然后是对齐的部分,然后是末尾的未对齐部分。如果保证全部对齐,那么代码会变得更简单、更快,并且更容易让处理器中的分支预测器正确。
过早的优化?
std::copy 处于一个有趣的位置。我希望它永远不会比memcpy 慢,并且有时使用任何现代优化编译器都会更快。此外,你可以memcpy,你可以std::copy。 memcpy 不允许缓冲区中有任何重叠,而 std::copy 支持一个方向上的重叠(std::copy_backward 支持另一个方向的重叠)。 memcpy 仅适用于指针,std::copy 适用于任何迭代器(std::map、std::vector、std::deque 或我自己的自定义类型)。换句话说,当您需要复制大量数据时,您应该只使用std::copy。