当前位置: 首页 > 知识库问答 >
问题:

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

齐昊焱
2023-03-14

在汇编编程中,通常需要从寄存器的低位计算一些东西,但其他位不能保证为零。在更高级的语言(如C)中,您只需将输入转换为较小的大小,然后让编译器决定是否需要将每个输入的上限分别归零,或者是否可以在事实发生后将结果的上限截去。

这对于x86-64(又名AMD64)尤其常见,原因有很多1,其中一些原因在其他ISA中也存在。

我将使用64bit x86作为示例,但目的是询问/讨论2的补码和无符号二进制算术,因为所有现代CPU都使用它。(请注意,C和C++不保证二补4,带符号溢出是未定义行为。)

例如,考虑一个简单的函数,它可以编译成lea指令2。(在x86-64 SysV(Linux)ABI3中,前两个函数参数为RDIRSI,返回值为RAXint是32位类型。)

; 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中的内容。因此,它节省了一个指令字节,并使用lea eax,[rdi+RSI*4+3]

为什么它能起作用?

1为什么x86-64经常出现这种情况:x86-64具有可变长度指令,其中一个额外的前缀字节会改变操作数大小(从32到64或16),因此在其他以相同速度执行的指令中保存一个字节通常是可能的。当写入寄存器的低8B或16B时,它也有假依赖关系(AMD/P4/Silvermont)(或在以后读取完整寄存器时(Intel pre-IvB)):由于历史原因,只写入32B子寄存器,其馀64B寄存器为零。几乎所有的算术和逻辑都可以用在低8、16或32位的通用寄存器上,也可以用在全64位的通用寄存器上。整数向量指令也是相当非正交的,有些操作对于某些元素大小是不可用的。

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

2LEA:与其他指令一样,LEA的默认操作数大小为32bit,但默认地址大小为64bit。操作数大小前缀字节(0x66rex.w)可以使输出操作数大小为16或64位。地址大小前缀字节(0x67)可以将地址大小减少到32位(在64位模式下)或16位(在32位模式下)。因此,在64位模式下,LEA eax,[EDX+ESI]LEA eax,[RDX+RSI]多占用一个字节。

可以执行lea rax,[edx+esi],但地址仍然只计算32位(进位不设置rax的位32)。使用LEA eax,[RDX+RSI]可以得到相同的结果,它短两个字节。因此,地址大小前缀对于lea永远没用,正如Agner Fog出色的objconv反汇编器的反汇编输出中的注释所警告的那样。

3x86 ABI:调用方不必将64位寄存器的上半部分归零(或符号扩展),该寄存器用于通过值传递或返回较小类型。想要将返回值用作数组索引的调用方必须对其进行符号扩展(使用movzx rax、eax或EAX特殊大小写指令cdqe。(不要与cdq混淆,后者将eax符号扩展为edx:eax例如为idiv设置。))

这意味着返回unsigned int的函数可以在RAX中的64位临时值中计算其返回值,并且不需要mov eax、eax来将RAX的高位归零。这个设计决策在大多数情况下工作得很好:通常调用方不需要任何额外的指令来忽略RAX上半部分中的未定义位。

C和C++特别不需要二进制带符号的二进制补码整数(C++std::atomic类型除外)。一个人的补码和符号/幅度也是允许的,所以对于完全可移植的C,这些技巧只对unsigned类型有用。显然,对于有符号操作,符号/幅度表示中的设置符号位意味着其他位被减去,而不是相加,例如。我还没有研究出一个人补充的逻辑

然而,只对two的补码起作用的bit-hacks很普遍,因为实际上没有人关心其他的东西。许多适用于2的补码的东西也应该适用于1的补码,因为符号位仍然不会改变其他位的解释:它的值只是-(2n-1)(而不是2n)。符号/幅度表示没有这个属性:每个位的位置值都是正的或负的,这取决于符号位。

还要注意,C编译器允许假定签名溢出永远不会发生,因为它是未定义的行为。因此,例如,编译器可以并且确实假设(x+1) 总是false。这使得在C中检测带符号溢出相当不方便。注意,无符号wraparound(carry)和带符号溢出之间的区别。


共有1个答案

祁鸿晖
2023-03-14

>

  • 按位html" target="_blank">逻辑
  • 左移(包括[reg1+reg2*scale+disp]中的*scale)
  • 加减法(因此lea指令:地址大小前缀是不需要的。如果需要,只需使用所需的操作数大小来截断。)
  • 乘法的下半部分。例如16b×16b->16b可以用32b×32b->32b来完成。通过使用32位的IMUL r32,R/M32,IMM32,然后只读取结果的低16,可以避免IMUL r16,R/M16,IMM16中的LCP停顿(和部分寄存器问题)。(但是,如果使用M32版本,请小心使用更宽的内存引用。)

    正如Intel的insn参考手册所指出的,imul的2和3操作数形式在无符号整数上使用是安全的。输入的符号位不影响N x n->N位乘法中结果的N位。)

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

    >

  • 右移
  • 全乘法:例如,对于16b x 16b->32b,在执行32b x 32b->32bimul之前,确保输入的上部16是零或符号扩展的。或者使用16位单操作数MULIMUL来不便地将结果放入dx:ax中。(选择有符号还是无符号的指令将以与32Bimul之前的零或符号扩展相同的方式影响上部16B。)

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

    除法与余数

    二的补码,就像无符号基2一样,是位值系统。无符号base2的MSB在N比特数中具有2n-1的位置值(例如231)。在2的补码中,MSB的值为-2n-1(因此用作符号位)。维基百科的这篇文章解释了理解2的补码和否定无符号基2数的许多其他方法。

    关键是设置符号位不会改变其他位的解释。加法和减法的工作原理与无符号的base2完全相同,只有对有符号和无符号的结果的解释不同。(例如,当符号位有进位但没有进位出位时,就会发生带符号溢出。)

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

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

    低8位(0xfc)不依赖于它们借用的内容。它们“绕圈”并将借位传递给上8位。

    因为lea只使用加法(和左移),所以使用默认地址大小总是可以的。将截断延迟到操作数大小对结果起作用为止总是很好的。

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

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

    参见维基百科的更大版本,并有更详细的解释。有很多关于二进制带符号乘法的好的谷歌点击,包括一些教材。

        *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位)仍然会得到相同的结果。部分乘积的符号扩张只发生在结果的上半部分。

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

    >

  • GCC使用IMUL计算两个无符号长输入的无符号长乘积。请参阅Godbolt编译器资源管理器上的gcc利用LEA实现其他功能的示例

    英特尔的insn参考手册说:

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

    • 英特尔的设计决定只引入IMUL的2和3个操作数形式,而不是MUL

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

  •  类似资料:
    • 问题内容: 我是Java的新手,所以我编写了这段代码,以便将这整个五年都称为布尔值,并为所有布尔值生成答案。但是,它仅调用最后一个。我该怎么做呢? 问题答案: 您每年需要使用单独的对象,或者至少在创建该年份的对象后立即调用the年检查方法。 您所拥有的是对函数的一系列调用,该函数将值分配给同一对象的属性。因此,只有最后一条语句才起作用,因为先前的值将被覆盖。 另外请注意,您的代码似乎没有正确组织。

    • 问题内容: 我已经读过(this),但无法找到一种方法来解决我的特定问题。我知道这是一个聚合函数,不按原样使用它是没有意义的,但是在这种特定情况下,我必须在保持每一行的同时获取所有结果。 这是桌子: 我需要数量,但要保留每条记录,因此输出应为: 我有此查询,但它只对每一行求和,而不是对所有结果求和: 没有,它只会返回一行,但是我需要维护所有ID。 注意:是的,这是一个非常基本的示例,我可以在此处使

    • 当ran返回null,但是哪一个呢?我没有预见到这有什么实际的目的--我只是好奇而已。

    • 我有一个带有分页页面的搜索结果,它只适用于第一个结果页面1。当用户输入具有11个结果的搜索关键字时,他们将进行搜索。php并正确查看结果: «è第1页,共2页,显示11个结果中的1-8个›» 但是,当用户单击 "在2页中的第1页,显示11个结果中的1-8个" 前8个结果与预期的最后3个结果相同 这是我单击第2页的下一个箭头时看到的URL:http://example.com/search/2/?s

    • 我正在编写一个代码,要求用户输入一个数字,如果输入的不是数字,它将要求用户再次输入。哪个环路最好?

    • 我写了这段代码,它只是对n个数字的列表进行求和,以练习浮点运算,但我不明白这一点: 我正在使用float,这意味着我有7位精度,因此,如果我执行10002*10002=100040004的操作,数据类型float的结果将是100040000.000000,因为我丢失了第7位以外的任何数字(程序仍然知道指数,如图所示)。 如果此程序中的输入是 然而,您将看到,当这个程序计算30003*30003=9