我正在使用 Visual C++ 2010 和 masm(“快速调用”调用约定)进行一些 x64 汇编。
假设我有一个 C++ 函数:
extern "C" void fillArray(unsigned char* byteArray, unsigned char value);
指向数组的指针将位于 RCX 中,字符值将位于 DL 中
如何使用 DL 用值填充 RAX,这样如果我要
mov qword ptr [RCX], RAX
并打印 byteArray,所有值都将等于“char value”?
请注意,我并不是试图对我的编译器进行编码,我只是在学习。
您可以乘以 0x0101010101010101 将最低字节复制到所有其他字节中(假设其余字节一开始全部为零),这有点烦人,因为没有
imul r64, r64, imm64
但您可以这样做:
mov rax, 0x0101010101010101
imul rax, rdx ; at least as fast as mul rdx on all CPUs
如果
rdx
不是所需的形式(换句话说,如果它设置了一些额外的位),只需添加一个movzx eax, dl
在前面,并将常数移入RDX或另一个寄存器。 (movzx edx,dl
无法从 Intel CPU 上的 mov-elimination 中受益。)
如果您不喜欢代码大小(
mov r64, imm64
本身已经是 10 个字节),只需将该常量粘贴到数据段中即可。
因为您将过程称为“fillArray”,所以我假设您喜欢用字节值填充整个内存块。所以我对不同的方法进行了比较。它是 32 位 masm 代码,但结果在 64 位模式下应该类似。每种方法都使用对齐和未对齐的缓冲区进行测试。结果如下:
Simple REP STOSB - aligned....: 192
Simple REP STOSB - not aligned: 192
Simple REP STOSD - aligned....: 191
Simple REP STOSD - not aligned: 222
Simple while loop - aligned....: 267
Simple while loop - not aligned: 261
Simple while loop with different addressing - aligned....: 271
Simple while loop with different addressing - not aligned: 262
Loop with 16-byte SSE write - aligned....: 192
Loop with 16-byte SSE write - not aligned: 205
Loop with 16-byte SSE write non-temporal hint - aligned....: 126 (EDIT)
使用以下代码的最简单的变体似乎在这两种情况下都表现最佳,并且代码大小也最小:
cld
mov al, 44h ; byte value
mov edi, lpDst
mov ecx, 256000*4 ; buf size
rep stosb
编辑:这不是对齐数据最快的。添加了性能最佳的 MOVNTDQ 版本,请参见下文。
为了完整起见,以下是其他例程的摘录 - 假设该值之前已扩展为 EAX:
斯托德代表:
mov edi, lpDst
mov ecx, 256000
rep stosd
简单同时:
mov edi, lpDst
mov ecx, 256000
.while ecx>0
mov [edi],eax
add edi,4
dec ecx
.endw
不同的简单 while:
mov edi, lpDst
xor ecx, ecx
.while ecx<256000
mov [edi+ecx*4],eax
inc ecx
.endw
上交所(两者):
movd xmm0,eax
punpckldq xmm0,xmm0 ; xxxxxxxxGGGGHHHH -> xxxxxxxxHHHHHHHH
punpcklqdq xmm0,xmm0 ; xxxxxxxxHHHHHHHH -> HHHHHHHHHHHHHHHH
mov ecx, 256000/4 ; 16 byte
mov edi, lpDst
.while ecx>0
movdqa xmmword ptr [edi],xmm0 ; movdqu for unaligned
add edi,16
dec ecx
.endw
SSE(NT,对齐,编辑):
movd xmm0,eax
punpckldq xmm0,xmm0 ; xxxxxxxxGGGGHHHH -> xxxxxxxxHHHHHHHH
punpcklqdq xmm0,xmm0 ; xxxxxxxxHHHHHHHH -> HHHHHHHHHHHHHHHH
mov ecx, 256000/4 ; 16 byte
mov edi, lpDst
.while ecx>0
movntdq xmmword ptr [edi],xmm0
add edi,16
dec ecx
.endw
我在这里上传了整个代码http://pastie.org/9831404 --- 组装需要hutch的MASM包。
如果 SSSE3 可用,您可以使用
pshufb
将字节广播到寄存器的所有位置,而不是使用一系列 punpck
指令。
movd xmm0, edx
xorps xmm1,xmm1 ; xmm1 = 0
pshufb xmm0, xmm1 ; xmm0 = _mm_set1_epi8(dl)
天真的方式
xor ebx, ebx
mov bl, dl ; input in dl
mov bh, dl
mov eax, ebx
shl ebx, 16
or ebx, eax
mov eax, ebx
shl rax, 32
or rax, rbx ; output in rax
所以它可能比哈罗德的解决方案慢
您还可以查看以下代码的编译器汇编输出
uint64_t s;
s = (s << 8) | s;
s = (s << 16) | s;
s = (s << 32) | s;
gcc 8.2 生成 以下输出,结果在 rax 中
movzx edi, dil # s, c
mov rax, rdi # _1, s
sal rax, 8 # _1,
or rdi, rax # s, _1
mov rax, rdi # _2, s
sal rax, 16 # _2,
or rax, rdi # s, s
mov rdi, rax # _3, s
sal rdi, 32 # _3,
or rax, rdi # s, _3
ret
正如您所看到的,它的效率不是很高,因为即使不需要,编译器也总是使用 64 位寄存器。所以为了让编译器更容易优化就这样修改吧
uint32_t s = (c << 8) | c;
s = (s << 16) | s;
return ((uint64_t)s << 32) | s;
请注意,这仅适用于使用您提到的 DL 填充单个寄存器(如 RAX)。对于大数组,最好使用 SIMD 或其他专用指令,例如
repstos
,例如 std::fill
或 memset
的实现方式。 gcc 和 Clang 都足够聪明,可以识别 memset(&int64, bytevalue, sizeof int64)
并将其转换为乘以 0x0101010101010101,如上所示