在调试时,我经常进入memcpy和memset的手写程序集实现 . 这些通常使用流指令(如果可用),循环展开,对齐优化等实现...我最近也遇到了这个'bug' due to memcpy optimization in glibc .
问题是:为什么硬件制造商(英特尔,AMD)不能优化具体情况
rep stos
和
rep movs
被认可,并尽可能快地填写和复制他们自己的架构?
在调试时,我经常进入memcpy和memset的手写程序集实现 . 这些通常使用流指令(如果可用),循环展开,对齐优化等实现...我最近也遇到了这个'bug' due to memcpy optimization in glibc .
问题是:为什么硬件制造商(英特尔,AMD)不能优化具体情况
rep stos
和
rep movs
被认可,并尽可能快地填写和复制他们自己的架构?
6 回答
成本 .
在C库中优化
memcpy
的成本相当低,可能需要几周的开发人员时间 . 你'll have to make a new version every several years or so when processor features change enough to warrant a rewrite. For example, GNU'和Apple的libSystem
都有一个memcpy
,专门针对SSE3进行了优化 .硬件优化的成本要高得多 . 它不仅在开发人员成本方面更加昂贵(设计CPU比编写用户空间汇编代码要困难得多),但它会增加处理器的晶体管数量 . 这可能会产生一些负面影响:
功耗增加
单位成本增加
某些CPU子系统的延迟增加
降低最大时钟速度
理论上,它可能对性能和单位成本产生整体负面影响 .
Maxim: 如果软件解决方案足够好,请不要在硬件中执行此操作 .
Note: 你引用的错误并不是
glibc
w.r.t中的错误 . C规范 . 它更复杂 . 基本上,glibc人员说memcpy
的行为与标准中所宣传的完全一样,其他一些人抱怨memcpy
应该别名为memmove
.Time for a story: 这让我想起了一个Mac游戏开发者在603处理器上运行游戏而不是601游戏时的抱怨(这是从20世纪90年代开始的) . 601具有对未对齐的负载和存储的硬件支持,性能损失最小 . 603只是产生了一个例外;通过卸载到内核我想象加载/存储单元可以变得更加简单,可能使处理器更快,更便宜 . Mac OS超微内核通过执行所需的加载/存储操作并将控制权返回给进程来处理异常 .
但是这个开发人员有一个自定义的blitting例程,可以将像素写入屏幕,从而完成未对齐的加载和存储 . 601上的游戏性能很好,但是在603上是可恶的 . 大多数其他开发者没有注意到他们是否使用了Apple的blitting功能,因为Apple可以重新实现它用于更新的处理器 .
故事的寓意是,软件和硬件改进都会带来更好的性能 .
In general, 趋势似乎与所提到的硬件优化方向相反 . 虽然在x86中很容易在汇编中编写
memcpy
,但是一些较新的架构会为软件卸载更多的工作 . 特别值得注意的是VLIW架构:Intel IA64(Itanium),TI TMS320C64x DSP和Transmeta Efficeon就是例子 . 使用VLIW,汇编编程变得更加复杂:您必须明确选择哪些执行单元可以获得哪些命令以及哪些命令可以同时完成,这是现代x86将为您做的事情(除非它是Atom) . 所以写memcpy
突然变得更加困难 .这些架构技巧允许您从微处理器中切割出大量硬件,同时保留超标量设计的性能优势 . 想象一下,芯片的占位面积更接近Atom,但性能更接近Xeon . 我怀疑编程这些设备的难度是阻碍更广泛采用的主要因素 .
我想在其他答案中添加的一件事是
rep movs
在所有现代处理器上实际上并不慢 . 例如,[突出显示是我的 . ]参考:Agner Fog, Optimizing subroutines in assembly language An optimization guide for x86 platforms.,p . 156(另见第16.10节,第143页)[2011-06-08版] .
General Purpose vs. Specialized
一个因素是那些指令(rep前缀/字符串指令)是通用的,因此它们将处理任何对齐,任意数量的字节或字,并且它们将具有相对于高速缓存和/或寄存器状态等的某些行为 . 明确无法改变的副作用 .
专用内存副本可能仅适用于某些对齐,大小,并且可能与缓存有不同的行为 .
手写的程序集(在库中或者一个开发人员可能自己实现)可能会超出字符串针对使用它的特殊情况的指令实现 . 对于特殊情况,编译器通常会有几个memcpy实现,然后开发人员可能会有一个“非常特殊”的情况,他们自己推出 .
在硬件级别进行此专业化没有意义 . 太复杂(=成本) .
The law of diminishing returns
考虑它的另一种方法是,当引入新特征时,例如SSE,设计师进行架构更改以支持这些功能,例如更宽或更高带宽的存储器接口,管道的变化,新的执行单元等 . 此时,设计人员不太可能回到设计的“遗留”部分,试图将其提升到最新功能 . 这会产生适得其反的效果 . 如果您遵循这一理念,您可能会问我们为什么首先需要SIMD,设计师是否只能让狭窄的指令像SIMD一样快速地处理有人使用SIMD的情况?答案通常是不值得,因为更容易投入新的执行单元或指令 .
在嵌入式系统中,通常使用具有memcpy / memset的专用硬件 . 它通常不是作为特殊的CPU指令完成的,而是一个位于内存总线上的DMA外设 . 你写了几个寄存器来告诉它地址,硬件完成其余的工作 . 它并不真正保证特殊的CPU指令,因为它实际上只是一个内存接口问题,并不真正需要CPU .
如果它没有破坏不修复它 . 它没有破产 .
主要问题是未对齐的访问 . 根据您运行的架构,它们会从糟糕变为非常糟糕 . 很多都与程序员有关,有些与编译器有关 .
修复memcpy的最便宜的方法是不使用它,保持数据在良好的边界上对齐,并使用或替换memcpy,只支持良好对齐的块副本 . 更好的方法是让编译器为了速度而牺牲程序空间和ram . 使用大量结构的人或语言,以便编译器在内部生成对memcpy的调用,或者等效语言的任何内容都会使其结构增长,以便在内部填充或填充内部 . 59字节结构可能变为64字节 . malloc或只提供指向指定对齐的地址的指针的替代方法 . 等等
自己完成所有这些操作要容易得多 . 对齐的malloc,结构是对齐大小的倍数 . 你自己的memcpy是一致的,等等,这很容易为什么硬件人会搞乱他们的设计,编译器和用户?它没有商业案例 .
另一个原因是缓存改变了画面 . 您的dram只能以固定大小访问,32位64位,类似于此,任何小于此的直接访问都会带来巨大的性能损失 . 将缓存放在前面,性能命中率会下降,任何读取 - 修改 - 写入都会在缓存中发生,修改允许对单个读取和写入dram进行多次修改 . 您仍然希望减少缓存的内存周期数,是的,您仍然可以通过使用换档功能(8位一档,16位二档,32位三档,64位)来平滑性能增益位巡航速度,32位下移,16位下移,8位下移)
我不能说英特尔,但确实知道像ARM这样的人已经完成了你所要求的
例如,如果内核使用32位接口,则仍然是四个32位传输 . 但对于64位接口,如果在64位边界上对齐,则变为长度为2的64位传输,双方之间的一组协商和两个64位字移动 . 如果没有在64位边界上对齐,那么它将变成三个传输,一个32位,一个64位,然后是一个32位 . 您必须要小心,如果这些硬件寄存器根据寄存器逻辑的设计可能不起作用,如果它只支持单个32位传输,则您无法对该地址空间使用该指令 . 不知道为什么你会尝试这样的东西 .
最后的评论是......当我这样做时会很痛......好吧不要这样做 . 不要单步进入内存副本 . 这样做的必然结果是,任何人都无法修改硬件的设计,使用户更容易单步执行内存复制,用例非常小,不存在 . 使用该处理器的所有计算机日夜全速运行,测量所有计算机单步执行mem副本和其他性能优化代码 . 这就像比较一粒沙子和地球的宽度 . 如果您是单步执行,那么无论新解决方案是什么,您仍然必须单步执行 . 为了避免巨大的中断延迟,手动调整的memcpy仍将以if-then-else开头(如果太小的副本只是进入一小组展开的代码或字节拷贝循环)然后以一些最佳速度进入一系列块拷贝,而没有可怕的延迟大小 . 你仍然需要单步执行 .
做单步执行调试你必须编译搞砸,慢,代码无论如何,通过memcpy问题解决单步的最简单方法,是告诉编译器和链接器 Build 调试,构建和链接非 - 优化的memcpy或一般的备用非优化库 . gnu / gcc和llvm是开源的,你可以让它们做你想做的任何事情 .
曾几何时
rep movsb
是最佳解决方案 .最初的IBM PC有一个8088处理器,带有8位数据总线,没有缓存 . 那么最快的程序通常是指令字节数最少的程序 . 有特别说明有帮助 .
如今,最快的程序是可以并行使用尽可能多的CPU功能的程序 . 起初看起来很奇怪,拥有许多简单指令的代码实际上可以比单个do-it-all指令运行得更快 .
英特尔和AMD保留旧指令主要是为了向后兼容 .