如何从存储器到寄存器3个字节(24位)?

时间:2017-12-15 12:23:11

标签: assembly x86 x86-64 micro-optimization instruction-set

我可以使用MOV指令将存储在内存中的数据项移动到我选择的通用寄存器中。

MOV r8, [m8]
MOV r16, [m16]
MOV r32, [m32]
MOV r64, [m64]

现在,不要开枪,但是如何实现以下目标:MOV r24, [m24]? (我很欣赏后者不合法)。

在我的例子中,我想将字符“Pip”(即0x706950h)移动到注册rax

section .data           ; Section containing initialized data

14      DogsName: db "PippaChips"
15      DogsNameLen: equ $-DogsName

我首先认为我可以分别移动字节,即首先是一个字节,然后是一个字,或者是它们的某种组合。但是,我无法引用eaxrax的“上半部分”,所以这在第一个障碍时就会出现问题,因为我最终会过度编写先移动的数据。

我的解决方案:

26    mov al, byte [DogsName + 2] ; move the character “p” to register al
27    shl rax, 16                 ; shift bits left by 16, clearing ax to receive characters “pi”
28    mov ax, word [DogsName]     ; move the characters “Pi” to register ax

我可以将“Pip”声明为初始化数据项,但示例只是一个例子,我想了解如何在汇编中引用24位,或者40,48 ......就此而言。

是否有更类似于MOV r24, [m24]的说明?有没有办法选择一系列内存地址,而不是提供偏移量和指定大小运算符。如何从内存中移出3个字节到ASM x86_64中注册?

NASM版本2.11.08架构x86

3 个答案:

答案 0 :(得分:6)

通常情况下,您执行4字节加载并屏蔽掉您想要的字节附带的高垃圾或者只是忽略它如果您&# 39;对那些不关心高位的数据做一些事情。 Which 2's complement integer operations can be used without zeroing high bits in the inputs, if only the low part of the result is wanted?

与商店不同 1 ,加载您不应该"不应该"#34;除非你进入一个未映射的页面,否则永远不会成为正确的问题。 (例如,如果db "pip"出现在页面的末尾,并且下一页未被映射。)但在这种情况下,您知道它是更长字符串的一部分,所以唯一可能的缺点是如果宽负载扩展到下一个高速缓存行(因此负载跨越高速缓存行边界),性能就会下降。  Is it safe to read past the end of a buffer within the same page on x86 and x64?

对于任何3个字节,前面的字节或后面的字节总是可以安全访问(如果3个字节本身不在两个高速缓存行之间分割,则甚至没有跨越高速缓存行边界)。在运行时弄清楚这个可能不值得,但是如果你知道编译时的对齐,你可以做任何一个

mov   eax, [DogsName-1]     ; if previous byte is in the same page/cache line
shr   eax, 8

mov   eax, [DogsName]       ; if following byte is in the same page/cache line
and   eax, 0x00FFFFFF

我假设您想要zero-extend the result into eax/rax, like 32-bit operand-size,而不是与现有的EAX / RAX高字节合并,如8或16位操作数大小的寄存器写入。如果您确实要合并,请屏蔽旧值OR。或者,如果您从[DogsName-1]加载,那么您想要的字节位于EAX的前3个位置,并且您希望合并到ECX:shr ecx, 24 / shld ecx, eax, 24以将旧的顶部字节向下移动到底部,然后在移动3个新字节时将其移回。 (不幸的是,shld没有内存来源形式。半相关:efficiently loading from two separate dwords into a qword。)shld在英特尔CPU上很快(特别是Sandybridge及其后:1 uop) ,但不是AMD(http://agner.org/optimize/)。

组合2个单独的负载

有很多方法可以做到这一点,但不幸的是,所有CPU都没有单一的最快方式。 Partial-register writes behave differently on different CPUs。你的方式(字节加载/移位/字加载到ax)在Core2 / Nehalem以外的CPU上相当不错(当你在组装它之后阅读eax时,它将停止插入合并的uop)。但是从movzx eax, byte [DogsName + 2]开始,以打破对rax旧值的依赖。

经典"安全无处不在"您希望编译器生成的代码是:

DEFAULT REL      ; compilers use RIP-relative addressing for static data; you should too.
movzx   eax, byte [DogsName + 2]   ; avoid false dependency on old EAX
movzx   ecx, word [DogsName]
shl     eax, 16
or      eax, ecx

这需要额外的指令,但避免编写任何部分寄存器。但是,在Core2或Nehalem以外的CPU上,2次加载的最佳选择是编写ax。 (在Core2之前的Intel P6无法运行x86-64代码,而在编写rax时,没有部分寄存器重命名的CPU将合并到ax。 Sandybridge确实仍然重命名AX,但合并只需1 uop而没有停顿,即与OR相同,但在Core2 / Nehalem上,前端在插入合并uop时会停顿约3个周期。

Ivybridge and later only rename AH, not AX or AL,所以在那些CPU上,AX的负载是微融合负载+合并。 Agner Fog没有列出Silvermont或Ryzen(或我看过的电子表格中的任何其他标签)mov r16, m的额外罚款,因此可能没有部分重命名的其他CPU也执行mov ax, [mem]作为加载+合并。

movzx   eax, byte [DogsName + 2]
shl     eax, 16
mov      ax, word [DogsName]

; using eax: 
  ; Sandybridge: extra 1 uop inserted to merge
  ; core2 / nehalem: ~3 cycle stall (unless you don't use it until after the load retires)
  ; everything else: no penalty

实际上,在运行时测试对齐可以有效地完成。给定寄存器中的指针,前一个字节在同一个高速缓存行中,除非该地址的最后几个5或6位都为零。 (即地址与高速缓存行的开头对齐)。让我们假设缓存行是64字节;所有当前的CPU都使用它,我不认为任何具有32字节行的x86-64 CPU存在。 (我们仍然绝对避免翻页)。

    ; pointer to m24 in RSI
    ; result: EAX = zero_extend(m24)

    test   sil, 111111b     ; test all 6 low bits.  There's no TEST r32, imm8, so  REX r8, imm8 is shorter and never slower.
    jz   .aligned_by_64

    mov    eax, [rsi-1]
    shr    eax, 8
.loaded:

    ...
    ret    ; end of whatever large function this is part of

 ; unlikely block placed out-of-line to keep the common case fast
.aligned_by_64:
    mov    eax, [rsi]
    and    eax, 0x00FFFFFF
    jmp   .loaded

所以在通常的情况下,额外的成本只是一个未采取的测试和分支uop。

根据CPU,输入和周围代码的不同,测试低12位(仅避免跨越4k边界)会对页面内的某些高速缓存行拆分进行更好的分支预测,但仍然不会是高速缓存行分裂。 (在这种情况下test esi, (1<<12)-1。与使用sil测试imm8不同,使用si测试imm16不值得英特尔CPU上的LCP停止以节省1代码字节。当然,如果你的指针可以在ra / b / c / dx中,你就不需要REX前缀,甚至还有一个紧凑的2字节编码{{1 }}。)

你甚至可以无分支地做到这一点,但显然不值得,只需要做两次单独的加载!

test al, imm8

真正的架构24位加载或存储

在架构上,x86没有24位加载或存储,其中整数寄存器作为目标或源。正如Brandon指出的那样,MMX / SSE屏蔽存储(如MASKMOVDQU,不要与pmovmskb eax, xmm0混淆)可以存储来自MMX或XMM reg的24位,给定一个向量仅设置低3字节的掩码。但是它们几乎从来没用过,因为它们很慢并且总是有一个NT提示(因此它们围绕缓存写入,并强制驱逐像 ; pointer to m24 in RSI ; result: EAX = zero_extend(m24) xor ecx, ecx test sil, 7 ; might as well keep it within a qword if we're not branching setnz cl ; ecx = (not_start_of_line) ? : 1 : 0 sub rsi, rcx ; normally rsi-1 mov eax, [rsi] shl ecx, 3 ; cl = 8 : 0 shr eax, cl ; eax >>= 8 : eax >>= 0 ; with BMI2: shrx eax, [rsi], ecx is more efficient and eax, 0x00FFFFFF ; mask off to handle the case where we didn't shift. )。 (AVX双字/ qword屏蔽加载/存储指令并不代表NT,但不具备字节粒度。)

AVX512BW (Skylake-server) adds vmovdqu8为加载和存储提供字节屏蔽,对被屏蔽的字节进行故障抑制。 (即,如果16字节的加载包含未映射页面中的字节,只要不为该字节设置掩码位,你就不会发生段错误。但这确实会造成大幅减速)。所以微体系结构它仍然是一个16字节的负载,但对架构状态(即除了性能之外的所有东西)的影响正是真正的3字节加载/存储(使用正确的掩码) )。

您可以在XMM,YMM或ZMM寄存器中使用它。

movntdq

这与NASM 2.13.01汇编。 IDK,如果你的NASM足够新,可以支持AVX512。您可以在没有使用英特尔Software Development Emulator (SDE)

的硬件的情况下使用AVX512

这看起来很酷,因为只有2个uop才能将结果输入;; probably slower than the integer way, especially if you don't actually want the result in a vector mov eax, 7 ; low 3 bits set kmovw k1, eax ; hoist the mask setup out of a loop ; load: leave out the {z} to merge into the old xmm0 (or ymm0 / zmm0) vmovdqu8 xmm0{k1}{z}, [rsi] ; {z}ero-masked 16-byte load into xmm0 (with fault-suppression) vmovd eax, xmm0 ; store vmovd xmm0, eax vmovdqu8 [rsi]{k1}, xmm0 ; merge-masked 16-byte store (with fault-suppression) (一旦设置了掩码)。 (但是,Skylake-X的http://instlatx64.atw.hu/电子表格of data from IACA并不包含带有掩码的eax,只包含未屏蔽的表单。这些表明它是&#39} ; s仍然是单个uop加载,或者像常规vmovdqu8

那样的微融合存储

要注意如果16字节的负载出现故障或越过缓存线边界,则会减速。我认为它内部做负载,然后丢弃字节,如果需要抑制故障,可能会有一些特殊情况。

此外,对于商店版本,请注意蒙面商店不能有效地转发到加载。 (有关详细信息,请参阅英特尔的优化手册。)

脚注:

  1. 广泛存储是一个问题,因为即使您替换旧值,您也要进行非原子读取 - 修改 - 写入,如果您放回的字节是锁定,这可能会破坏事情,例如。 不要存储在对象之外,除非您知道接下来会发生什么以及它是否安全,例如您放在那里填充以允许此操作。可以 vmovdqu/a修改后的4字节值,以确保您不会踩到另一个线程&# 39;更新额外字节,但显然做2个独立存储 比性能lock cmpxchg重试循环更好

答案 1 :(得分:1)

写24位的唯一方法是使用MMX(MASKMOVQ)或SSE(MASMODQU)和掩码来防止您不希望修改的字节被修改。但是,对于单次写入,MMX和SSE过于复杂(并且可能更慢)。

请注意,正常读取比写入更便宜(特别是涉及多个CPU时)。考虑到这一点,另一种选择是:

    shl eax,8
    mov al,[DogsName+3]
    ror eax,8
    mov [DogsName],eax

这会使用旧值覆盖之后的字节(如果后面的字节不可访问,或者后面的字节属于需要原子更新的任何字节,则可能会导致问题。)

答案 2 :(得分:0)

通过BMI2,您可以使用BZHI

BZHI r32a, r/m32, r32b   Zero bits in r/m32 starting with the position in r32b, write result to r32a
BZHI r64a, r/m64, r64b   Zero bits in r/m64 starting with the position in r64b, write result to r64a

因此load the low 24 bits from [mem]可以使用

MOV  eax, 24
BZHI eax, [mem], eax

与此同时,您还可以从内存中加载可变数量的位