如何用重复的字节值填充 64 位寄存器
How to populate a 64 bit register with duplicate byte values
我正在使用 Visual C++ 2010 和 masm('fast call' 调用约定)进行一些 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 r64, imm64
本身已经是 10 个字节),只需将该常量粘贴到您的数据段中即可。
天真的方式
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 生成 the following output 结果在 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 ,如上
因为你调用了你的过程'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
SSE(两者):
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)
我正在使用 Visual C++ 2010 和 masm('fast call' 调用约定)进行一些 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 r64, imm64
本身已经是 10 个字节),只需将该常量粘贴到您的数据段中即可。
天真的方式
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 生成 the following output 结果在 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 ,如上
因为你调用了你的过程'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
SSE(两者):
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)