如何用重复的字节值填充 64 位寄存器

问题描述 投票:0回答:3

我正在使用 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”?

请注意,我并不是试图对我的编译器进行编码,我只是在学习。

assembly x86 x86-64 64-bit masm
3个回答
13
投票

您可以乘以 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 个字节),只需将该常量粘贴到数据段中即可。


5
投票

因为您将过程称为“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)

3
投票

天真的方式

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,如上所示

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.