【问题标题】:Does unsigned math require more CPU instructions?无符号数学是否需要更多 CPU 指令?
【发布时间】:2011-10-02 05:29:35
【问题描述】:

获取一个 C++ 整数变量 i,并假设您将其值乘以 2。

如果i 有符号,我相信这个操作在某种程度上是等价的,至少在数学上,到:

i = i << 1;

但是如果i的类型是无符号的,那么由于无符号值不会溢出而是对其范围取模,大概操作是这样的:

i = (i << 1) & (decltype(i))-1;

现在,我认为实际的机器指令可能比乘法移位序列更简洁。但是现代的,比如 x86,CPU 会有无符号/模数学的特定指令吗?或者,与使用有符号值进行数学运算相比,使用无符号值进行数学运算是否会花费额外的指令?

(是的,在编程时关心这个是很荒谬的;我只是出于好奇而感兴趣。)

【问题讨论】:

  • 由于(decltype(i))-1(最好写成static_cast&lt;decltype(i)&gt;(-1)~static_cast&lt;decltype(i)&gt;(0))全为1,因此与&amp;一起使用是无操作的,可以被优化器删除。
  • 为什么左移会导致下溢?另外,“无符号值 [...] 以它们的范围为模执行”这句话是什么意思?
  • @Mike:不一定。考虑 i 是否具有类型 unsigned short(16 位),但 int 是 32 位。那么i &lt;&lt; 1是一个32位的值,而(decltype(i))-10xFFFF,即使在&amp;被提升为int之后。在那种情况下,Tomalak 说标准需要掩码是错误的,但如果你这样做,掩码会产生影响。 结合将它分配回 i,但是,它没有任何效果,因为它只影响将被转换丢弃的位。
  • @Steve:等一下。您是说&lt;&lt; 的结果类型取决于 right 手边参数的类型,而不仅仅是 left 手边参数的类型?请链接到标准的相关部分,因为这是与几乎所有架构都不同的行为。
  • @Mike: 5.8/1,“操作数应为整数或枚举类型,并执行整数提升。结果的类型是提升的左操作数的类型”。积分提升在 4.5 中定义,如果 int 可以表示 unsigned short 的每个值(在我的示例中它可以),则 unsigned short 提升为 int,否则提升为 unsigned int。 RHS的类型与它无关。

标签: c++ cpu unsigned


【解决方案1】:

假设环绕溢出,这是二进制补码硬件上大多数(全部?)CPU 算术指令所做的,&lt;&lt; 对于无符号类型相当于乘法。因此,您遇到的唯一问题是当您使用比用于包含它的寄存器更小的类型进行算术运算时。

算术表达式中至少提升到int(或unsigned int)的规则旨在避免这种情况发生:例如,当您将unsigned short 乘以2 时,结果是int (如果shortint 大小相同,则为unsigned int)。无论是哪种,当寄存器大小与类型匹配时,都不需要取任何模数。并且使用二进制补码,无论如何,您都不需要有符号和无符号 C++ 乘法的不同指令:环绕溢出涵盖两者,除非硬件提供溢出位并且您关心它的值(-1 * 2 将是无符号溢出,但不是有符号溢出,即使结果位模式相同)。

唯一可能需要掩码的情况是/当您将结果转换回unsigned short。即便如此,我认为谨慎的实现有时会在 int 大小的寄存器顶部留下额外的“不相关”位,用于保存中间 unsigned short 值。您知道那些额外的高位不会影响加法、乘法或减法的模结果,并且如果将值存储到内存中,它们将被屏蔽(假设一条指令存储了 int 的底部 2 个字节大小的寄存器到 2 个字节的内存,模数基本上是免费的)。因此,实现只需要在除法、右移、比较以及我忘记的任何其他内容之前小心屏蔽,或者使用适当的指令(如果可用)。

【讨论】:

    【解决方案2】:

    我认为你弄错了:在 unsigned 数据类型上,位移与它在锡上所说的完全一样,未占用的位用零填充。这会导致对左移类型的值进行正确的模算术运算,即乘法。右移没有算术类比,因为 Z/nZ 一般不是除法环,也没有除法的概念;右移只是截断除法。

    另一方面,有符号 类型存在歧义,因为有不同的方式将位模式解释为有符号整数。通过 2 的补码左移,您将获得预期的乘法“环绕”,但没有右移行为的规范选择。在旧的 C 标准中,我认为这是完全实现定义的,但我认为 C99 使该行为具体化。

    【讨论】:

    • 是的,我搞错了。
    【解决方案3】:

    interjay 的回答涵盖了基础知识。更多细节:

    现在,我认为实际的机器指令可能比乘法移位序列更简洁。

    这取决于处理器。在过去,当晶体管价格昂贵时,6500 和 6800 等处理器的指令一次只能向左或向右移动一位。

    后来,当芯片变得更大并且参数操作码中有更多位时,实现了“桶形移位器”,它可以在单个周期内移动任意数量的位。这就是现代 CPU 所使用的。

    与使用有符号值的数学相比,使用无符号值执行数学运算是否会花费额外的指令?

    从来没有。当一个操作在无符号和有符号之间有所不同时,每个操作都会有单独的说明。

    【讨论】:

    • “当一个操作在无符号和有符号之间有所不同时,每个都有单独的说明。”我认为这与其他答案有些对比?
    • 真的吗?这几乎就是 interjay 在他最后两段中所说的。 (例如 6800 上的 LSR/ASR)有趣的部分是,当从较小的操作构建操作时,例如在只有 32 位 ALU 的处理器上进行 64 位数学运算时,您会使用有符号和无符号指令的混合实现签名操作。
    【解决方案4】:

    正如其他人已经写的那样:CPU 无关紧要。有符号和无符号指令需要相同的时间,无符号算术中的某些操作甚至更容易执行,并且可能需要比有符号变体更少的周期(多精度除法就是一个例子)。

    然而,这只是故事的一半。

    C++ 将有符号整数溢出定义为未定义行为,将无符号整数定义为模2。这提供了完全不同的优化机会,从而导致不同的代码。

    一个例子:

    int foo (int a)
    {
      return a * 1000 / 500;
    }
    
    unsigned bar (unsigned a)
    {
      return a * 1000 / 500;
    }
    

    这里 foo 可以优化为:

    int foo (int a)
    {
      return a * 2;
    }
    

    bar 将保持不变。

    请注意,这两个函数在数学上是相同的,但如果参数超过 INT_MAX/1000,它们会开始给出不同的结果。

    由于未定义有符号溢出的影响,编译器可以选择在简化表达式时假装没有 INT_MAX。对于无符号算术,情况并非如此,编译器必须发出执行乘法和除法的代码。这当然比优化后的变体慢。

    注意:大多数编译器在进行此类优化时都很保守,只有在您要求时才启用它们,因为它们往往会破坏代码和溢出检查。其他编译器,尤其是在嵌入式和 DSP 世界中,即使在低优化级别,也总是会进行此类优化。为这类机器编写代码的程序员都知道细微的细节,所以这几乎不是问题。

    我们已经讨论过 C/C++ 程序员在 stackoverflow 上不止一次陷入这个陷阱的故事。

    【讨论】:

    • “对于无符号算术,情况并非如此,编译器必须发出执行乘法和除法的代码。这当然比优化变体慢。”这就是我想知道的,谢谢。
    【解决方案5】:

    不,它不需要更多指令,至少在 x86 上是这样。

    对于某些操作(例如加法和减法),相同的指令用于有符号和无符号类型。这是可能的,因为在使用带符号值的 2 补码表示时它们的工作方式相同。

    左移也没有区别:最左边的位被硬件简单地丢弃,不需要像您的示例中那样执行按位操作。

    对于其他操作(例如右移),有单独的指令:SHR(右移)用于无符号值,SAR(算术右移)用于有符号值,保留符号位。

    还有用于有符号/无符号乘法和除法的单独指令:MUL/IMUL 和 DIV/IDIV,其中 IMUL 和 IDIV 用于有符号值。

    【讨论】:

    【解决方案6】:

    无符号数学确实会溢出,因此隐含地取模它们各自的范围。

    【讨论】:

    • 它们确实不会溢出,因为它们是以它们的范围为模的。而且我在问这个事实的实现是否会导致更多的 CPU 指令,一般来说。
    【解决方案7】:

    据我所知,大多数 CPU 在硬件中都有无符号操作,我很确定 x86 也有。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-07-26
      • 2010-10-16
      • 1970-01-01
      • 2020-11-25
      • 2015-07-19
      • 1970-01-01
      • 2020-02-11
      相关资源
      最近更新 更多