当A是无符号vs有符号时,为什么A / <constant-int>更快?

时间:2018-03-25 15:33:08

标签: c++ optimization

我一直在阅读Optimizing C++ wikibook。在faster operations chapter中,其中一条建议如下:

  

整数除以

     

当您将整数(已知为正或零)除以a时   常量,将整数转换为无符号。

     

如果s是有符号整数,则u是无符号整数,C是a   常数整数表达式(正或负),操作s /   C比u / C慢,s%C比u%C慢。这是最多的   当C是2的幂时,显着,但在所有情况下,符号必须   在分裂期间要考虑到这一点。

     

然而,从签名到无签名的转换是免费的   它只是对相同位的重新解释。因此,如果s是a   您知道为正或零的有符号整数,您可以加快速度   它的除法使用以下(等效)表达式:( unsigned)s   / C和(无符号)s%C。

我用gcc测试了这个语句,而u / C表达似乎始终优于s / c

下面还提供了以下示例:

#include <iostream>
#include <chrono>
#include <cstdlib>
#include <vector>
#include <numeric>

using namespace std;

int main(int argc, char *argv[])
{

    constexpr int vsize = 1e6;
    std::vector<int> x(vsize);
    std::iota(std::begin(x), std::end(x), 0); //0 is the starting number

    constexpr int a = 5;

  auto start_signed = std::chrono::system_clock::now();
  int sum_signed = 0;
    for ([[gnu::unused]] auto  i : x)
    {
        // signed is by default
        int v = rand() % 30 + 1985;   // v in the range 1985-2014

        sum_signed += v / a;
    }
  auto end_signed = std::chrono::system_clock::now();

  auto start_unsigned = std::chrono::system_clock::now();
  int sum_unsigned = 0;
    for ([[gnu::unused]] auto  i : x)
    {
        int v = rand() % 30 + 1985;   // v in the range 1985-2014
        sum_unsigned += static_cast<unsigned int>(v) / a;
    }
  auto end_unsigned = std::chrono::system_clock::now();

  // signed
  std::chrono::duration<double> diff_signed = end_signed - start_signed;
  std::cout << "sum_signed: " << sum_signed << std::endl;
  std::cout << "Time it took SIGNED: " << diff_signed.count() * 1000 << "ms" << std::endl;

  // unsigned
  std::chrono::duration<double> diff_unsigned = end_unsigned - start_unsigned;
  std::cout << "sum_unsigned: " << sum_unsigned << std::endl;
  std::cout << "Time it took UNSIGNED: " << diff_unsigned.count() * 1000 << "ms" << std::endl;

  return 0;
}

您可以在此处编译并运行示例:http://cpp.sh/8kie3

为什么会这样?

1 个答案:

答案 0 :(得分:5)

在经历了一些玩笑之后,我相信我已经通过标准来确定问题的根源,即从C ++ 11开始,负整数除法被舍入为零。对于最简单的除以2的情况,请查看以下代码和相应的程序集(godbolt link)。

constexpr int c = 2;

int signed_div(int in){
    return in/c;
}

int unsigned_div(unsigned in){
    return in/c;
}

大会:

signed_div(int):
  mov eax, edi
  shr eax, 31
  add eax, edi
  sar eax
  ret

unsigned_div(unsigned int):
  mov eax, edi
  shr eax
  ret

这些额外说明有什么作用? shr eax, 31(右移31)只是隔离了符号位,这意味着如果输入是非负的,eax == 0,否则eax == 1。然后输入被添加到eax。换句话说,这两条指令转换为&#34;如果输入为负数,则向其添加1。添加的含义如下(仅适用于负面输入)。

  • 如果输入是偶数,则其最低有效位设置为1,但移位会丢弃它。输出不受此操作的影响。

  • 如果输入为奇数,则其最低有效位已经为1,因此相加会导致余数传播到其余数字。当发生右移时,如果我们没有将符号位添加到输入,则丢弃最低有效位并且输出比我们输出的输出大1。因为默认情况下,在两个补码轮中右移向负无穷大,现在输出是相同除法的结果,但是向零舍入。

简而言之,即使负数也不受影响,奇数现在向零舍入而不是向负无穷大。

对于2次幂的非幂,它会变得有点复杂。并非所有常量都提供相同的输出,但对于很多常量,它看起来类似于以下(godbolt link)。

constexpr int c = 3;

int signed_div(int in){
    return in/c;
}

int unsigned_div(unsigned in){
    return in/c;
}

大会:

signed_div(int):
  mov eax, edi
  mov edx, 1431655766
  sar edi, 31
  imul edx
  mov eax, edx
  sub eax, edi
  ret
unsigned_div(unsigned int):
  mov eax, edi
  mov edx, -1431655765
  mul edx
  mov eax, edx
  shr eax
  ret

我们不关心程序集输出中常量的更改,因为它不会影响执行时间。假设mulimul花费相同的时间(我不确定,但希望比我更有知识的人可以找到它的来源),签名版本再次需要更长的时间,因为它有额外的指令来处理负操作数的符号位。

注释

  • 使用x86-64 GCC 7.3和-O2标志在godbot上进行编译。

  • 从C ++ 11开始,标准强制要求零行为。在根据this cppreference页面确定实现之前。