gpt4 book ai didi

assembly - 如何将 3 个字节(24 位)从内存移动到寄存器?

转载 作者:行者123 更新时间:2023-12-02 06:28:04 25 4
gpt4 key购买 nike

我可以使用 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

我首先考虑我可以单独移动字节,即首先是一个字节,然后是一个字,或它们的某种组合。但是,我无法引用 eax 的“上半部分” , rax ,所以这在第一个障碍处下降,因为我最终会覆盖首先移动的任何数据。

我的解决方案:
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 字节 int 不在页面末尾,通常您会 执行 4 字节加载并屏蔽掉随您想要的字节而来的高垃圾 或干脆忽略它 _0x104567如果您正在处理不关心高位的数据。 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 不同,加载“不应该”的数据永远不会成为正确性问题,除非您进入未映射的页面。 (例如,如果 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 ,而不是像 8 位或 16 位操作数大小的寄存器写入那样与 EAX/RAX 的现有高字节合并。如果确实要合并,请屏蔽旧值和 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 在 Intel CPU(尤其是 Sandybridge 和更高版本:1 uop)上很快,但在 AMD ( http://agner.org/optimize/ ) 上不是。

合并 2 个独立的负载
有很多方法可以做到这一点,但不幸的是,没有一种最快的方法适用于所有 CPU。 Partial-register writes behave differently on different CPUs 。您的方式(字节加载/移位/字加载到 ax )在 Core2/Nehalem 以外的 CPU 上相当不错(当您在组装 eax 后读取 movzx eax, byte [DogsName + 2] 时,它​​将停止插入合并 uop)。但是从 rax 开始打破对 ax 旧值的依赖。
您希望编译器生成的经典“无处不在”代码是:
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 次加载的最佳选择是编写 rax 。 (Core2 之前的 Intel P6 不能运行 x86-64 代码,没有部分寄存器重命名的 CPU 在编写 ax 时会合并到 AH 中)。 Sandybridge 仍然重命名 AX,但合并仅花费 1 uop,没有停顿,即与 OR 相同,但在 Core2/Nehalem 上,前端在插入合并 uop 时会停顿大约 3 个周期。
Ivybridge and later only rename AX , not AL or mov r16, m ,因此在这些 CPU 上,AX 的负载是微融合负载+合并。 Agner Fog 没有列出 Silvermont 或 Ryzen(或我查看的电子表格中的任何其他选项卡)上的 mov ax, [mem] 的额外惩罚,因此大概其他没有部分 reg 重命名的 CPU 也会将 test esi, (1<<12)-1 作为加载+合并执行。
movzx   eax, byte [DogsName + 2]
shl eax, 16
mov ax, word [DogsName]

; when read 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 (including IvB+): no penalty, merge already done

实际上, 在运行时测试对齐可以有效地完成 。给定寄存器中的指针,前一个字节在同一高速缓存行中,除非地址的最后几个 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
所以在一般情况下,额外的成本只是一个未采取的 test-and-branch uop。
根据 CPU、输入和周围代码,测试低 12 位(仅避免跨越 4k 边界)会为页面内的某些缓存行拆分更好的分支预测进行权衡,但仍然永远不会发生页行拆分。 (在那种情况下 sil 。与使用 imm8 测试 si 不同,使用 imm16 测试 test al, imm8 不值得在 Intel CPU 上使用 LCP 停顿以节省 1 个字节的代码。当然,如果您可以将指针放在 ra/b/c/dx,您不需要 REX 前缀,而且 MASKMOVDQU 甚至还有一个紧凑的 2 字节编码。)
您甚至可以无分支地执行此操作,但与仅执行 2 个单独的加载相比,这显然不值得!
    ; 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.

真正的架构 24 位加载或存储
在架构上,x86 没有以整数寄存器作为目标或源的 24 位加载或存储。正如 Brandon 指出的那样,MMX/SSE 掩码存储(如 pmovmskb eax, xmm0 ,不要与 movntdq 混淆)可以存储来自 MMX 或 XMM reg 的 24 位,给定一个仅设置低 3 个字节的向量掩码。但是它们几乎从来没有用过,因为它们很慢并且总是有 NT 提示(所以它们围绕缓存写入,并像 vmovdqu8 一样强制逐出)。 (AVX dword/qword 掩码加载/存储指令不暗示 NT,但不能用于字节粒度。)
AVX512BW (Skylake-server) adds eax 为您提供加载和存储的字节掩码 对被掩码的字节进行故障抑制。 (即,如果 16 字节负载包括未映射页面中的字节,只要未为该字节设置掩码位,您就不会出现段错误。但这确实会导致大幅减速)。所以在微架构上它仍然是一个 16 字节的加载,但对架构状态的影响(即除了性能之外的一切)正是真正的 3 字节加载/存储(使用正确的掩码)的效果。
您可以在 XMM、YMM 或 ZMM 寄存器上使用它。
;; 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)
这与 NASM 2.13.01 一起组装。 IDK 如果您的 NASM 足够新以支持 AVX512。您可以使用英特尔的 Software Development Emulator (SDE) 在没有硬件的情况下玩 AVX512
这看起来很酷,因为将结果放入 vmovdqu8 只需 2 uop(一旦设置了掩码)。 (但是,用于 Skylake-X 的 http://instlatx64.atw.hu/'s 电子表格 of data from IACA 不包括带掩码的 vmovdqu/a ,只有未掩码的形式。这些确实表明它仍然是一个单一的 uop 负载,或者像常规 lock cmpxchg 一样的微融合存储)
但是 如果 16 字节的加载会出现故障或越过缓存线边界 ,请注意减速。我认为它在内部执行加载然后丢弃字节,如果需要抑制故障,则具有潜在昂贵的特殊情况。
此外,对于商店版本,请注意掩码商店不能有效地转发到负载。 (有关更多信息,请参阅英特尔的优化手册)。

脚注:
  • 宽存储是一个问题,因为即使您替换旧值,您也会进行非原子读取-修改-写入,例如,如果您放回的那个字节是锁,这可能会破坏事情。 除非您知道接下来会发生什么并且它是安全的,否则不要在对象之外存储,例如您放置在那里以允许此操作的填充。 您可以将修改后的 4 字节值 cmpxchg 到位,以确保您不会踩到另一个线程对额外字节的更新,但显然,执行 2 个单独的存储比原子 ojit_code 重试循环对性能要好得多。
  • 关于assembly - 如何将 3 个字节(24 位)从内存移动到寄存器?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47832367/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com