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

在调试时,我经常进入memcpy和memset的手写程序集实现。 这些通常使用流指令(如果可用),循环展开,对齐优化等实现…我最近也遇到了由于glibc中的memcpy优化而导致的“错误” 。

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

rep stos 

 rep movs 

被认可,并尽可能快地填写和复制他们自己的架构?

成本。

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

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

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

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

Maxim:如果软件解决方案足够好,请不要在硬件中使用它。

注意:您引用的错误实际上并不是glibc与C规范中的错误。 它更复杂。 基本上,glibc人员说memcpy行为与标准中的广告完全一样,其他一些人抱怨memcpy应该为memmove别名。

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

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

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

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

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

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

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

[突出显示是我的。]参考: Agner Fog,用汇编语言优化子程序x86平台的优化指南。 页。 156(另见第16.10节,第143页)[2011-06-08版]。

通用与专业

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

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

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

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

收益递减规律

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

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

如果它没有破坏不修复它。 它没有破产。

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

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

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

另一个原因是缓存改变了画面。 您的dram只能以固定大小访问,32位64位,类似于此,任何小于此的直接访问都会带来巨大的性能损失。 将缓存放在前面,性能命中率下降,任何读取 – 修改 – 写入都发生在缓存中,修改允许多次修改以进行单次读取和写入dram。 您仍然希望减少缓存的内存周期数,是的,您仍然可以通过使用换档function(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是开源的,你可以让它们做你想做的任何事情。

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

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

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

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