ein*_*ica 5 assembly gcc clang memcpy compiler-optimization
应该优化编译代码,从一个地方到另一个地方复制3个字节,比如使用memcpy(,,3)看起来像汇编指令?
考虑以下程序:
#include <string.h>
int main() {
int* p = (int*) 0x10;
int x = 0;
memcpy(&x, p, 4);
x = x * (x > 1 ? 2 : 3);
memcpy(p, &x, 4);
return 0;
}
Run Code Online (Sandbox Code Playgroud)
它有点人为,会导致分段违规,但我需要这些指令,以便编译-O3不会使所有这些都消失.当我编译它(GodBolt,GCC 6.3 -O3)时,我得到:
main:
mov edx, DWORD PTR ds:16
xor eax, eax
cmp edx, 1
setle al
add eax, 2
imul eax, edx
mov DWORD PTR ds:16, eax
xor eax, eax
ret
Run Code Online (Sandbox Code Playgroud)
great - mov从内存到寄存器的单个DWORD(= 4个字节).很好,优化.现在让我们改变memcpy(&x, p1, 4)成memcpy(&x, p1, 3)?编译结果变为:
main:
mov DWORD PTR [rsp-4], 0
movzx eax, WORD PTR ds:16
mov WORD PTR [rsp-4], ax
movzx eax, BYTE PTR ds:18
mov BYTE PTR [rsp-2], al
mov edx, DWORD PTR [rsp-4]
xor eax, eax
cmp edx, 1
setle al
add eax, 2
imul eax, edx
mov DWORD PTR ds:16, eax
xor eax, eax
ret
Run Code Online (Sandbox Code Playgroud)
我对英特尔X86_64程序集的解释不多(阅读:当它很复杂时我甚至无法正确阅读它),所以 - 我不太明白.我的意思是,我得到了前6条指令中发生的事情以及为什么这么多指令是必要的.为什么两个动作不够?一个mov WORD PTRINT al和mov BYTE PTR成ah?
......所以,我来这里问.在我写这个问题的时候,我注意到GodBolt也有铿锵作为选项.嗯,clang(3.9.0 -O3)这样做:
main: # @main
movzx eax, byte ptr [18]
shl eax, 16
movzx ecx, word ptr [16]
or ecx, eax
cmp ecx, 2
sbb eax, eax
and eax, 1
or eax, 2
imul eax, ecx
mov dword ptr [16], eax
xor eax, eax
ret
Run Code Online (Sandbox Code Playgroud)
看起来更像我的预期.是什么解释了差异?
笔记:
x = 0.mov.如果我们放弃memcpy'为以下行为,行为是相似的:
#include <string.h>
typedef struct {
unsigned char data[3];
} uint24_t;
int main() {
uint24_t* p = (uint24_t*) 0x30;
int x = 0;
*((uint24_t*) &x) = *p;
x = x * (x > 1 ? 2 : 3);
*p = *((uint24_t*) &x);
return 0;
}
Run Code Online (Sandbox Code Playgroud)如果你想对相关代码在函数中的情况进行对比,请看一下这个或uint24_t结构版本(GodBolt).然后看看4字节值会发生什么.
你应该从复制4个字节和掩盖顶部字节中获得更好的代码,例如使用x & 0x00ffffff.这让编译器知道它允许读取4个字节,而不仅仅是C源读取的3个字节.
是的,这有点帮助:它保存gcc和clang从存储4B零,然后复制三个字节并重新加载4.它们只加载4,掩码,存储和使用仍在寄存器中的值.部分原因可能是不知道*p别名*q.
int foo(int *p, int *q) {
//*p = 0;
//memcpy(p, q, 3);
*p = (*q)&0x00ffffff;
return *p;
}
mov eax, DWORD PTR [rsi] # load
and eax, 16777215 # mask
mov DWORD PTR [rdi], eax # store
ret # and leave it in eax as return value
Run Code Online (Sandbox Code Playgroud)
为什么两个动作不够?一个mov WORD PTR
al跟随一个mov BYTE PTR进入ah?
AL和AH是8位寄存器.你不能把16位字放入AL.这就是为什么你的最后一个clang-output块加载两个独立的寄存器并与shift +合并的原因,or如果它知道它允许混乱所有4个字节x.
如果要合并两个单独的单字节值,可以将它们加载到AL和AH然后使用AX,但这会导致Intel Has-Haswell上的部分寄存器停顿.
您可以通过各种原因(包括正确性和避免错误依赖EAX的旧值),左移EAX,然后将字节加载到AL中,对AX进行单词加载(或者最好将movzx加载到eax中).
但编译器并不倾向于这样做,因为部分注册的东西多年来一直是非常糟糕的juju,并且只对最近的CPU(Haswell,也许是IvyBridge)有效.这将导致Nehalem和Core2严重失速.(参见Agner Fog的microarch pdf ;搜索部分寄存器或在索引中查找.请参阅x86标签wiki 中的其他链接.)也许在几年内,-mtune=haswell将启用部分寄存器技巧来保存clang使用的OR指令合并.
编写带有args并返回一个值的函数,这样你就不必为了不优化而使它们变得非常奇怪.例如,一个函数,它接受两个int*args并在它们之间进行3字节memcpy.
这是在Godbolt(与gcc和clang),颜色突出
void copy3(int *p, int *q) { memcpy(p, q, 3); }
clang3.9 -O3 does exactly what you expected: a byte and a word copy.
mov al, byte ptr [rsi + 2]
mov byte ptr [rdi + 2], al
movzx eax, word ptr [rsi]
mov word ptr [rdi], ax
ret
Run Code Online (Sandbox Code Playgroud)
为了获得您设法生成的愚蠢,首先将目标归零,然后在三字节副本后将其读回:
int foo(int *p, int *q) {
*p = 0;
memcpy(p, q, 3);
return *p;
}
clang3.9 -O3
mov dword ptr [rdi], 0 # *p = 0
mov al, byte ptr [rsi + 2]
mov byte ptr [rdi + 2], al # byte copy
movzx eax, word ptr [rsi]
mov word ptr [rdi], ax # word copy
mov eax, dword ptr [rdi] # read the whole thing, causing a store-forwarding stall
ret
Run Code Online (Sandbox Code Playgroud)
gcc没有做得更好(除了不重命名部分regs的CPU,因为它通过使用movzx字节副本避免了对旧的EAX值的错误依赖).
大小 3 是一个丑陋的大小,编译器并不完美。
编译器无法生成对您未请求的内存位置的访问,因此需要两次移动。
虽然这对您来说似乎微不足道,但请记住您要求的是从内存到内存的memcpy(&x, p, 4);副本。
显然,GCC 和旧版本的 Clang 不够聪明,无法弄清楚没有理由在内存中传递临时值。
GCC 对前六个指令所做的基本上是[rsp-4]按照您的要求用三个字节构造一个 DWORD
mov DWORD PTR [rsp-4], 0 ;DWORD is 0
movzx eax, WORD PTR ds:16 ;EAX = byte 0 and byte 1
mov WORD PTR [rsp-4], ax ;DWORD has byte 0 and byte 1
movzx eax, BYTE PTR ds:18 ;EAX = byte 2
mov BYTE PTR [rsp-2], al ;DWORD has byte 0, byte 1 and byte 2
mov edx, DWORD PTR [rsp-4] ;As previous from henceon
Run Code Online (Sandbox Code Playgroud)
它用于movzx eax, ...防止部分寄存器停顿。
编译器已经做得很好,省略了对 and 的调用,memcpy正如您所说,这个例子“有点做作”,即使对于人类来说也是如此。优化memcpy必须适用于任何大小,包括那些无法容纳寄存器的大小。每次都做对并不容易。
考虑到 L1 访问延迟在最近的架构中已经大大降低,并且[rsp-4]很可能在缓存中,我不确定是否值得弄乱 GCC 源代码中的优化代码。
确实值得为错过的优化提交错误并看看开发人员怎么说。