为什么复杂的memcpy / memset优越?

时间:2012-01-13 23:45:22

标签: c optimization assembly x86 64-bit

调试时,我经常进入memcpy和memset的手写程序集实现。这些通常使用流指令(如果可用),循环展开,对齐优化等实现...我最近也遇到了这个'bug' due to memcpy optimization in glibc

问题是:为什么硬件制造商(英特尔,AMD)不能优化

的具体情况
rep stos

rep movs

被认可,并在他们自己的架构上尽可能最快填充和复制?

6 个答案:

答案 0 :(得分:23)

费用。

在C库中优化memcpy的成本非常低,可能需要几周的开发人员时间。当处理器功能发生变化以保证重写时,您必须每隔几年左右制作一个新版本。例如,GNU的glibc和Apple的libSystem都有memcpy,专门针对SSE3进行了优化。

硬件优化的成本要高得多。它不仅在开发人员成本方面更加昂贵(设计CPU比编写用户空间汇编代码要困难得多),但它会增加处理器的晶体管数量。这可能会产生一些负面影响:

  • 功耗增加
  • 增加单位成本
  • 某些CPU子系统的延迟增加
  • 降低最高时钟速度

从理论上讲,它可能会对性能和单位成本产生整体负面影响。

Maxim:如果软件解决方案足够好,请不要在硬件中执行此操作。

注意:您引用的错误实际上不是glibc w.r.t中的错误。 C规范。它更复杂。基本上,glibc人员说memcpy的行为与标准中所宣传的完全相同,其他一些人抱怨memcpy应该别名为memmove

故事的时间:这让我想起了一个Mac游戏开发者在603处理器而不是601(这是从20世纪90年代)运行游戏时的抱怨。 601具有对未对齐的负载和存储的硬件支持,性能损失最小。 603只是产生了一个例外;通过卸载到内核我想象加载/存储单元可以变得更加简单,可能使处理器更快,更便宜。 Mac OS超微内核通过执行所需的加载/存储操作并将控制权返回给流程来处理异常。

但是这个开发人员有一个自定义的blitting例程,可以将像素写入屏幕,从而完成未对齐的加载和存储。 601上的游戏性能很好,但是在603上是可恶的。大多数其他开发者没有注意到他们是否使用了Apple的blitting功能,因为Apple可能会为新的处理器重新实现它。

故事的寓意是,软件和硬件改进都会带来更好的性能。

一般来说,趋势似乎与所提到的硬件优化方向相反。虽然在x86中很容易在汇编中编写memcpy,但一些较新的架构会将更多工作卸载到软件中。特别值得注意的是VLIW架构:Intel IA64(Itanium),TI TMS320C64x DSP和Transmeta Efficeon就是例子。使用VLIW,汇编编程变得更加复杂:您必须明确选择哪些执行单元可以同时执行哪些命令和哪些命令,这是现代x86将为您做的事情(除非它是Atom)。所以写memcpy突然变得更加困难。

这些架构技巧允许您从微处理器中切割出大量硬件,同时保留超标量设计的性能优势。想象一下,芯片的占地面积更接近Atom但性能更接近Xeon。我怀疑编程这些设备的难度是阻碍更广泛采用的主要因素。

答案 1 :(得分:15)

我想在其他答案中添加的一件事是rep movs在所有现代处理器上实际上并不慢。例如,

  

通常,REP MOVS指令的选择开销很大   并设置正确的方法。因此,它不是最佳的   小块数据。对于大块数据,它可能是相当的   当满足对准等的某些条件时有效。这些   条件取决于特定的CPU(参见第143页)。 在英特尔Nehalem上   和Sandy Bridge处理器一样,这是最快的移动方式   大数据块,即使数据未对齐。

[突出显示是我的。]参考:Agner Fog, Optimizing subroutines in assembly language An optimization guide for x86 platforms. ,p。 156(另见第16.10节,第143页)[2011-06-08版]。

答案 2 :(得分:5)

通用与专业

一个因素是那些指令(rep前缀/字符串指令)是通用的,因此它们将处理任何对齐,任意数量的字节或字,并且它们将具有相对于高速缓存和/或寄存器状态的某些行为等,即无法改变的明确副作用。

专用内存副本可能仅适用于某些对齐,大小,并且可能与缓存有不同的行为。

手写程序集(在库中或者一个开发人员可能自己实现)可能会超出使用它的特殊情况下的字符串指令实现。对于特殊情况,编译器通常会有几个memcpy实现,然后开发人员可能会有一个“非常特殊”的情况,他们会自行推出。

在硬件级别进行此专业化没有意义。太复杂(=成本)。

收益递减法则

另一种思考方式是,当引入新功能时,例如SSE,设计人员进行架构更改以支持这些功能,例如更宽或更高带宽的存储器接口,管道更改,新执行单元等。此时设计人员不太可能回到设计的“遗留”部分,试图将其提升到最新功能。这会产生适得其反的效果。如果您遵循这一理念,您可能会问我们为什么首先需要SIMD,对于有人使用SIMD的情况,设计师难道不能让狭窄的指令像SIMD一样快速工作吗?答案通常是它不值得,因为它更容易投入新的执行单元或指令。

答案 3 :(得分:1)

在嵌入式系统中,拥有memcpy / memset的专用硬件很常见。它通常不是作为特殊的CPU指令完成的,而是一个位于内存总线上的DMA外设。你写了几个寄存器来告诉它地址,HW完成剩下的工作。它并不真正保证特殊的CPU指令,因为它实际上只是一个内存接口问题,并不真正需要CPU。

答案 4 :(得分:1)

如果它不能解决它。它没坏了。

主要问题是未对齐的访问。根据您运行的体系结构,它们会从糟糕变为非常糟糕。很多都与程序员有关,有些与编译器有关。

修复memcpy的最便宜的方法是不使用它,保持数据在良好的边界上对齐,并使用或制作只支持良好对齐的块副本的memcpy的替代方法。更好的方法是让编译器切换为了速度而牺牲程序空间和ram。使用大量结构的人或语言,以便编译器在内部生成对memcpy的调用,或者等效的语言将使其结构增长,以便在内部填充或填充内部。 59字节结构可能变为64字节。 malloc或只提供指向指定对齐的地址的指针的替代方法。等等。

自己完成所有这些操作要容易得多。对齐的malloc,结构是对齐大小的倍数。你自己的memcpy是一致的,因为它很容易为什么硬件人会搞乱他们的设计和编译器和用户?没有商业案例。

另一个原因是缓存改变了画面。你的dram只能以固定大小,32位64位访问,类似的东西,任何小于此的直接访问都是一个巨大的性能影响。将缓存放在前面,性能命中率下降,任何读取 - 修改 - 写入都发生在缓存中,修改允许多次修改以进行单次读取和写入dram。您仍然希望减少缓存的内存周期数,是的,您仍然可以通过使用换档功能(8位一档,16位二档,32位三档,64位)来平滑性能增益。位巡航速度,32位下移,16位下移,8位下移)

我不能说英特尔,但确实知道像ARM这样的人已经做了你要求的事情

ldmia r0!,{r2,r3,r4,r5}
例如,如果内核使用32位接口,则仍然是四个32位传输。但是对于64位接口,如果在64位边界上对齐,则变为长度为2的64位传输,各方之间的一组协商和两个64位字移动。如果没有在64位边界上对齐,那么它将变成三个传输,一个32位,一个64位,然后是一个32位。您必须要小心,如果这些硬件寄存器可能不起作用,具体取决于寄存器逻辑的设计,如果它只支持单个32位传输,则您无法对该地址空间使用该指令。不知道为什么你会尝试这样的东西。

最后的评论是......当我这样做时会很痛......好吧不要这样做。不要单步进入内存副本。这样做的必然结果是,没有人会修改硬件的设计,使用户更容易单步执行内存复制,用例非常小,不存在。使用该处理器的所有计算机日夜全速运行,测量所有计算机单步执行mem副本和其他性能优化代码。这就像比较一粒沙子和地球的宽度。如果您是单步执行,那么无论新解决方案是什么,您仍然需要单步执行。为了避免巨大的中断延迟,手动调整的memcpy仍将以if-then-else开始(如果太小的副本只是进入一小组展开的代码或字节复制循环),那么就进入一系列的块拷贝一些最佳速度,没有可怕的延迟大小。你仍然需要单步执行。

进行单步调试你必须编译搞砸了,慢,代码无论如何,通过memcpy问题解决单步的最简单方法,是告诉编译器和链接器构建调试,构建和链接一般来说,针对非优化的memcpy或备用的非优化库。 gnu / gcc和llvm是开源的,你可以让他们做任何你想做的事。

答案 5 :(得分:1)

曾几何时rep movsb 最佳解决方案。

最初的IBM PC有一个8088处理器,带有8位数据总线,没有缓存。那么最快的程序通常是指令字节数最少的程序。有特别指示帮助。

如今,最快的程序是可以并行使用尽可能多的CPU功能的程序。一开始可能看起来很奇怪,拥有许多简单指令的代码实际上比单个do-it-all指令运行得更快。

英特尔和AMD保留旧指令主要是为了向后兼容。