如果只需要结果的低位部分,可以使用哪种 2 的补码整数运算而无需将输入中的高位清零?

2024-01-09

在汇编编程中,想要从寄存器的低位计算某些内容是相当常见的,但不能保证其他位清零。在 C 等高级语言中,您只需将输入转换为小尺寸,然后让编译器决定是否需要分别将每个输入的高位清零,或者是否可以在事实。

This is especially common for x86-64 (aka AMD64), for various reasons1, some of which are present in other ISAs.

I'll use 64bit x86 for examples, but the intent is to ask about/discuss 2's complement https://en.wikipedia.org/wiki/Two%27s_complement and unsigned binary arithmetic in general, since all modern CPUs use it https://stackoverflow.com/questions/2931630/how-are-negative-numbers-represented-in-32-bit-signed-integer. (Note that C and C++ don't guarantee two's complement4, and that signed overflow is undefined behaviour.)

As an example, consider a simple function that can compile to an LEA instruction2. (In the x86-64 SysV(Linux) ABI http://www.x86-64.org/documentation.html3, the first two function args are in rdi and rsi, with the return in rax. int is a 32bit type.)

; int intfunc(int a, int b) { return a + b*4 + 3; }
intfunc:
    lea  eax,  [edi + esi*4 + 3]  ; the obvious choice, but gcc can do better
    ret

gcc 知道加法,即使是负符号整数,也只能从右向左进位,因此输入的高位不会影响进入的内容eax。因此,它保存一个指令字节并使用 http://goo.gl/h3UKGC lea eax, [rdi + rsi*4 + 3]

还有哪些其他操作具有结果低位的这种属性,而不依赖于输入的高位?

为什么它有效?



脚注

1 Why this comes up frequently for x86-64 https://stackoverflow.com/tags/x86/info: x86-64 has variable-length instructions, where an extra prefix byte changes the operand size (from 32 to 64 or 16), so saving a byte is often possible in instructions that are otherwise executed at the same speed. It also has false-dependencies (AMD/P4/Silvermont) when writing the low 8b or 16b of a register (or a stall when later reading the full register (Intel pre-IvB)): For historical reasons, only writes to 32b sub-registers zero the rest of the 64b register https://stackoverflow.com/questions/11177137/why-do-most-x64-instructions-zero-the-upper-part-of-a-32-bit-register. Almost all arithmetic and logic can be used on on the low 8, 16, or 32bits, as well as the full 64bits, of general-purpose registers. Integer vector instructions are also rather non-orthogonal, with some operations not available for some element sizes.

此外,与 x86-32 不同,ABI 在寄存器中传递函数参数,并且对于窄类型,高位不需要为零。

2 LEA: Like other instructions, the default operand size of LEA https://stackoverflow.com/a/7071164/224132 is 32bit, but the default address size is 64bit. An operand-size prefix byte (0x66 or REX.W) can make the output operand size 16 or 64bit. An address-size prefix byte (0x67) can reduce the address size to 32bit (in 64bit mode) or 16bit (in 32bit mode). So in 64bit mode, lea eax, [edx+esi] takes one byte more than lea eax, [rdx+rsi].

这是可以做的lea rax, [edx+esi],但地址仍然只用 32 位计算(进位不会设置的位 32)rax)。你会得到相同的结果lea eax, [rdx+rsi],短了两个字节。因此,地址大小前缀对于以下情况没有用处:LEA,正如 Agner Fog 优秀的 objconv 反汇编器的反汇编输出中的注释所警告的那样。

3 x86 ABI: The caller doesn't have to zero (or sign-extend) the upper part of 64bit registers used to pass or return smaller types by value. A caller that wanted to use the return value as an array index would have to sign-extend it (with movzx rax, eax, or the special-case-for-eax instruction cdqe. (not to be confused with cdq, which sign-extends eax into edx:eax e.g. to set up for idiv.))

这意味着函数返回unsigned int可以在 64 位临时中计算其返回值rax,并且不需要mov eax, eax 将高位清零 https://stackoverflow.com/questions/11177137/why-do-most-x64-instructions-zero-the-upper-part-of-a-32-bit-register of rax。这种设计决策在大多数情况下效果很好:调用者通常不需要任何额外的指令来忽略上半部分中未定义的位。rax.


4 C and C++

C 和 C++ 专门做not需要二进制补码二进制有符号整数(除了C++ std::atomic types http://en.cppreference.com/w/cpp/atomic/atomic/operator_arith2). 还允许使用补码和符号/量值 https://stackoverflow.com/a/2931680/224132, 因此对于fully可移植 C,这些技巧仅适用于unsigned类型。显然,对于有符号运算,符号/数值表示中设置的符号位意味着其他位被减去,而不是相加。我还没有研究过补语的逻辑

However, bit-hacks http://www.hackersdelight.org/ that only work with two's complement https://graphics.stanford.edu/~seander/bithacks.html#IntegerAbs are widespread http://aggregate.org/MAGIC/#Average%20of%20Integers, because in practice nobody cares about anything else. Many things that work with two's complement should also work with one's complement, since the sign bit still doesn't change the interpretation of the other bits: it just has a value of -(2N-1) (instead of 2N). Sign/magnitude representation does not have this property: the place value of every bit is positive or negative depending on the sign bit.

另请注意,C 编译器可以假设有符号溢出永远不会发生,因为这是未定义的行为。所以例如编译器可以并且确实假设(x+1) < x总是假的 http://goo.gl/h3UKGC。这使得在 C 中检测有符号溢出相当不方便。请注意无符号环绕(进位)和有符号溢出之间的区别 http://teaching.idallen.com/dat2343/10f/notes/040_overflow.txt.


可以与高位中的垃圾一起使用的宽操作:

  • 按位逻辑
  • 左移(包括*scale in [reg1 + reg2*scale + disp])
  • 加法/减法(因此LEA说明:永远不需要地址大小前缀。如果需要,只需使用所需的操作数大小来截断。)
  • 乘法的低半部分。例如16b x 16b -> 16b 可以用 32b x 32b -> 32b 来完成。你可以避免 LCP 停顿(和部分寄存器问题)imul r16, r/m16, imm16 https://stackoverflow.com/questions/34111959/packing-bcd-to-dpd-how-to-improve-this-amd64-assembly-routine通过使用 32 位imul r32, r/m32, imm32然后只读取结果的低 16 位。 (如果使用更宽的内存引用,请小心m32不过版本。)

    正如Intel的insn ref手册所指出的,2和3操作数形式imul可安全地用于无符号整数。输入的符号位不影响结果的 N 位N x N -> N位乘法。)

  • 2x (i.e. shift by x): Works at least on x86, where the shift count is masked, rather than saturated, down to the width of the operation, so high garbage in ecx, or even the high bits of cl, don't affect the shift count. Also applies to BMI2 flagless shifts (shlx etc), but not to vector shifts (pslld xmm, xmm/m128 etc, which saturate the count). Smart compilers optimize away masking of the shift count, allowing for a safe idiom for rotates in C (no undefined behaviour) https://stackoverflow.com/questions/776508/best-practices-for-circular-shift-rotate-operations-in-c.

显然,像进位/溢出/符号/零这样的标志都会受到更广泛操作的高位垃圾的影响。 x86 的移位将最后一位移出到进位标志中,因此这甚至会影响移位。

操作can't与高位中的垃圾一起使用:

  • 右移
  • 完全乘法:例如对于 16b x 16b -> 32b,请确保在执行 32b x 32b -> 32b 之前对输入的高 16 位进行零或符号扩展imul。或者使用16位单操作数mul or imul不方便地将结果放入dx:ax。 (有符号指令与无符号指令的选择将影响高 16b,就像 32b 之前的零或符号扩展一样imul.)

  • 内存寻址([rsi + rax]):根据需要进行符号或零扩展。没有[rsi + eax]寻址模式。

  • 除法和余数

  • log2 https://stackoverflow.com/questions/3272424/compute-fast-log-base-2-ceiling(即最高设置位的位置)
  • 尾随零计数(除非您知道您想要的部分中有一个设置位,或者只是在未找到检查时检查大于 N 的结果。)

Two's complement, like unsigned base 2, is a place-value system. The MSB for unsigned base2 has a place value of 2N-1 in an N bit number (e.g. 231). In 2's complement, the MSB has a value of -2N-1 (and thus works as a sign bit). The wikipedia article https://en.wikipedia.org/wiki/Two%27s_complement explains many other ways of understanding 2's complement and negating an unsigned base2 number.

关键是设置符号位不改变其他位的解释。加法和减法的工作方式与无符号 base2 完全相同,只是有符号和无符号结果的解释不同。 (例如。当符号位有进位但没有进位时,就会发生有符号溢出 http://teaching.idallen.com/dat2343/10f/notes/040_overflow.txt.)

此外,进位仅从 LSB 传播到 MSB(从右到左)。减法也是一样:不管高位有没有借,低位都借。如果这导致溢出或进位,则只有高位会受到影响。例如。:

 0x801F
-0x9123
-------
 0xeefc

低8位,0xFC,不要依赖于他们借来的东西。它们“环绕”并将借位传递给高 8 位。

因此,加法和减法具有结果的低位不依赖于操作数的任何高位的特性。

Since LEA仅使用加法(和左移),使用默认地址大小总是可以的。延迟截断直到操作数大小对结果起作用总是可以的。

(例外:16 位代码可以使用地址大小前缀进行 32 位数学运算。在 32 位或 64 位代码中,地址大小前缀会减小宽度而不是增加宽度。)


乘法可以被认为是重复加法,或者移位加法。低半部分不受任何高位的影响。在这个 4 位示例中,我写出了所有加到低 2 位结果位中的位积。仅涉及任一源的低 2 位。很明显,这通常是有效的:部分乘积在加法之前会被移位,因此源中的高位通常不会影响结果中的低位。

See 维基百科有一个更大的版本,有更详细的解释 https://en.wikipedia.org/wiki/Binary_multiplier#More_advanced_approach:_signed_integers。有很多好的谷歌点击二进制有符号乘法 https://www.google.ca/?q=binary%20signed%20multiplication,包括一些教材。

    *Warning*: This diagram is probably slightly bogus.


       ABCD   A has a place value of -2^3 = -8
     * abcd   a has a place value of -2^3 = -8
     ------
   RRRRrrrr

   AAAAABCD * d  sign-extended partial products
 + AAAABCD  * c
 + AAABCD   * b
 - AABCD    * a  (a * A = +2^6, since the negatives cancel)
  ----------
          D*d
         ^
         C*d+D*c

进行有符号乘法而不是无符号乘法仍然会在下半部分给出相同的结果(本例中为低 4 位)。部分积的符号扩展仅发生在结果的上半部分。

这个解释不是很彻底(甚至可能有错误),但是有充分的证据表明它是真实的并且可以在生产代码中安全使用:

  • 海湾合作委员会使用imul来计算unsigned long两个的乘积unsigned long输入。请参阅 Godbolt 编译器资源管理器上的 gcc 利用 LEA 实现其他功能的示例 http://gcc.godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(j:1,options:(compileOnChange:'0'),source:'%23include+%3Cstdint.h%3E%0A%0Aint+intfunc(int+a,+int+b)+%7B+return+a+%2B+b*4+%2B+3%3B+%7D%0A%0Along+longfunc(int+a,+int+b)++++++++++%7B+return+a+%2B+b*4+%2B+3%3B+%7D%0Along+longfunc_longa(long+a,+int+b)+++%7B+return+a+%2B+b*4+%2B+3%3B+%7D%0Along+longfunc_longb(int+a,+long+b)+++%7B+return+a+%2B+b*4+%2B+3%3B+%7D%0Along+longfunc_longboth(long+a,+long+b)%7Breturn+a+%2B+b*4+%2B+3%3B+%7D%0A%0Aunsigned+long+longmul(unsigned+int+a,+unsigned+int+b)+%7B+return+(unsigned+long)a+*+(unsigned+long)b%3B+%7D%0A%0A//+signed+overflow+is+undefined+behaviour:+compilers+optimize+by+pretending+it+doesn!'t+happen%0Aint+overflow_check(int+x)+%7B+return+(x%2B1)+%3C+x%3B+%7D%0Aint+carry_check(unsigned+x)+%7B+return+(x%2B1)+%3C+x%3B+%7D'),l:'5',n:'1',o:'C%2B%2B+source+%231',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0'),(g:!((h:compiler,i:(compiler:g62,filters:(b:'0',commentOnly:'0',directives:'0',intel:'0'),options:'-xc+-std%3Dgnu11+-Wall+-O3+-march%3Dhaswell'),l:'5',n:'0',o:'%231+with+x86-64+gcc+6.2',t:'0')),k:50,l:'4',n:'0',o:'',s:0,t:'0')),l:'2',n:'0',o:'',t:'0')),version:4.

  • 英特尔的 insn 参考手册说:

二操作数和三操作数形式也可以与无符号一起使用 操作数,因为无论如何,乘积的下半部分都是相同的 操作数是否有符号。然而,CF 和 OF 标志 不能用来确定结果的上半部分是否是 非零。

  • Intel 的设计决定只引入 2 和 3 操作数形式imul, not mul.

显然,按位二进制逻辑运算(和/或/异或/非)独立处理每个位:位位置的结果仅取决于该位位置的输入值。位移也相当明显。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如果只需要结果的低位部分,可以使用哪种 2 的补码整数运算而无需将输入中的高位清零? 的相关文章

  • 汇编8086监听键盘中断

    我有与此完全相同的问题 边画边听键盘 https stackoverflow com questions 13970325 8086 listen to keyboard while drawing 但第一个答案 接受的答案 只听键盘一次
  • 为什么 GCC 不将 a*a*a*a*a*a 优化为 (a*a*a)*(a*a*a)?

    我正在对科学应用程序进行一些数值优化 我注意到的一件事是 GCC 会优化调用pow a 2 通过将其编译成a a 但是调用pow a 6 没有优化 实际会调用库函数pow 这大大降低了性能 相比之下 英特尔 C 编译器 http en wi
  • 为什么是补码?

    我正在编写一个教程来教孩子们 9 至 13 岁 编程 我从计算机本身开始 它们与计算机科学没有太大关系 更多的是涉及解决计算问题的过程 以此为出发点 我引导他们认识到机器可以帮助我们解决某些计算问题 人们擅长抽象思维和想象力 但计算机非常擅
  • 是否存在 UTF-8 编码中未使用的字节?

    据我了解 UTF 8 是 ASCII 的超集 因此包括不用于表示可打印字符的控制字符 我的问题是 是否有任何字节 256 个不同的字节 未被 UTF 8 编码使用 我想知道你是否可以转换 编码UTF 8 文本转二进制 这是我的思考过程 我不
  • 在 x86 ASM 中测试零通常哪个更快:“TEST EAX, EAX”与“TEST AL, AL”?

    测试 AL 中的字节是否为零 非零通常哪个更快 TEST EAX EAX TEST AL AL 假设之前有一个 MOVZX EAX BYTE PTR ESP 4 指令加载了一个带有零扩展的字节参数到 EAX 的其余部分 防止了我已经知道的组
  • 为什么当大小大于 50 时,该程序花费的时间会呈指数级增长?

    所以我正在为类编写一个 ARM 汇编快速排序方法 我对大部分内容都有了解 除了复杂性没有意义 我们将其与我们制作的另一种冒泡排序方法进行比较 它对于具有 1 个参数和 10 个参数的示例表现更好 然而 我什至无法比较 100 个参数测试 因
  • 二进制增量存储

    我正在寻找一种二进制增量存储解决方案来版本化大型二进制文件 数字音频工作站文件 使用 DAW 文件时 与用于存储原始数据 波形 的大量数据相比 大多数更改 尤其是在混音结束时 都非常小 如果我们的 DAW 文件有一个版本控制系统 让我们可以
  • 使用 (float&)int 进行类型双关可以正常工作,(float const&)int 会像 (float)int 一样转换吗?

    VS2019 发布 x86 template
  • 使用 NEON 优化 Cortex-A8 颜色转换

    我目前正在执行颜色转换例程 以便从 YUY2 转换为 NV12 我有一个相当快的函数 但没有我预期的那么快 主要是由于缓存未命中 void convert hd uint8 t orig uint8 t result uint32 t wi
  • Linux内核页表更新

    在linux x86 中分页 每个进程都有它自己的页面目录 页表遍历从 CR3 指向的页目录开始 每个进程共享内核页目录内容 假设三个句子是正确的 假设某个进程进入内核 模式并更新他的内核页目录内容 地址映射 访问 权利等 问题 由于内核地
  • 阴影空间示例

    EDIT 我接受了下面的答案 并添加了我自己的代码的最终修订版 希望它向人们展示影子空间分配的实际示例 而不是更多的文字 编辑 2 我还设法在 YouTube 视频 所有内容 的注释中找到了一个调用约定 PDF 的链接 其中有一些关于 Li
  • 在哈希图中存储字符和二进制数

    我正在尝试存储字母到二进制数的映射 这是我的映射 h 001 i 010 k 011 l 100 r 101 s 110 t 111 为此 我创建了一个哈希映射并存储了键值对 我现在想显示给定句子的相应二进制值 这是我的代码 package
  • 设置 IRQ 映射

    我正在遵循一些教程和参考文献来尝试设置我的内核 我在教程中遇到了一些不熟悉的代码 但根本没有解释它 这是我被告知映射的代码16 IRQs 0 15 到 ISR 地点32 47 void irq remap void outportb 0x2
  • 使用 R 读取和转换二进制原始数据

    我有一个file https drive google com file d 0BxMpk0nhnJy6SFhxd2xuMzJYYlk edit usp sharing其中包含原始 二进制数据和 ascii 它包含一个时间戳和一个代表速度的
  • 按字节数对向量进行混洗

    有什么办法可以左移 v 0 gt v 1 a m128i by n字节 其中n仅在运行时才知道 我目前仅限于 AVX1 但如果 AVX2 512 使这变得更容易 我非常感兴趣 I found mm bslli si128 m128i imm
  • FreePascal x64 上系统单元函数的汇编调用

    我有一些 Delphi 汇编代码 可以在 Win32 Win64 和 OSX 32 上编译并正常工作 XE2 但是 由于我需要它在 Linux 上工作 所以我一直在考虑编译它的 FPC 版本 到目前为止 Win32 64 Linux32 6
  • X86 预取优化:“计算 goto”线程代码

    我有一个相当重要的问题 我的计算图有循环和多个 计算路径 我没有制作一个调度程序循环 其中每个顶点将被一一调用 而是将所有预先分配的 框架对象 放置在堆中 代码 数据 这有点类似于线程代码 甚至更好 CPS 只是在堆中跳转 执行代码 每个代
  • 为什么 clang 使用 -O0 生成低效的 asm(对于这个简单的浮点和)?

    我正在 llvm clang Apple LLVM 版本 8 0 0 clang 800 0 42 1 上反汇编此代码 int main float a 0 151234 float b 0 2 float c a b printf f c
  • 这种没有推送寄存器的交换有多安全?

    我对汇编非常陌生 下面的代码应该通过两个不同的函数交换两个整数 首先使用swap c然后使用swap asm 但我怀疑 我是否需要push 我的意思是保存 汇编代码之前寄存器的每个值和pop稍后 就在返回之前 main 换句话说 如果我返回
  • 为什么我的代码显示垃圾?

    当我也想打印列表中的每个数字时 我的代码显示垃圾 有什么问题吗 输出应如下所示 给定的数组是 2G 4 PT为什么这是垃圾总数是 7 Code ASSUME CS CODE DS DATA SS STK ORG 0000H DATA SEG

随机推荐