【问题标题】:Why addition using bitwise operators in this code very slower than arithmetic addition为什么在此代码中使用按位运算符进行加法比算术加法要慢得多
【发布时间】:2014-04-20 16:39:12
【问题描述】:

我尝试将算术加法与我编写的使用按位运算的函数进行比较 - 发现后者几乎 10x 慢。造成这种速度差异的原因是什么?由于我在循环中添加了相同的数字,编译器是否在第一种情况下将其重写为更优化的值?

使用算术运算:

int main()
{
    clock_t begin = clock();
    int x;
    int i = 1000000000;
    while(i--) {
    x = 1147483000 + i;
    }
    printf("%d\n", x);

    clock_t end = clock();
    double time_spent = (double)(end - begin) / CLOCKS_PER_SEC;

    printf("time spent = %f\n", time_spent);
    return 0;
}

输出:

1147483000
time spent = 3.520000

使用位运算符:

while loop 内的行被替换为:

x = add(1147483000, i);

这是add 函数:

int add(int x, int y) {
    while(y != 0) {
        int carry = (x & y);
        x = x ^ y;
        y = carry << 1;
    }
    return x;
}

输出:

1147483000
time spent = 32.940000

【问题讨论】:

  • 你确定这个额外的时间不是函数调用所需的时间(开销)。我在某处读过这些。尝试将您的 add 功能块放入代码中,看看它是否在运行时间上有很大差异。还可以尝试查看这两种方法生成的汇编代码。
  • @CrystalMeth 如果此处的函数调用导致额外的 29 秒,您需要担心更大的问题。
  • @hvd :我想说的是函数调用可能是造成额外时间的因素之一。我没有说这是唯一的原因。
  • 你正在用三个指令的循环替换单个指令,你很惊讶它变慢了?
  • @CrystalMeth 我将功能块放入代码中 - 没有区别。我不太擅长组装。

标签: c bit-manipulation bitwise-operators compiler-optimization


【解决方案1】:

整数运算通常在硬件中以非常少的时钟周期执行。

您将无法在软件中接近这种性能。您使用按位运算的实现涉及函数调用和循环。您执行的按位运算通常花费与算术相似的时钟周期数。

您在每次迭代中执行三个按位运算。坦率地说,我很惊讶这里只有 10 倍。

我还想知道您的编译器设置是什么,特别是任何优化。一个好的编译器可以消除算术版本中的 while 循环。对于性能比较,您应该比较优化的代码。看起来你可能不会这样做。

很难知道您要在这里实现什么,但不要指望能击败硬件算术单元的性能。

【讨论】:

  • 算术运算被编译成专用的算术操作码。 CPU 的算术单元执行算术运算的速度比任何软件都快。
  • CPU 具有硬件加法器,可以在一条指令中完成所有工作,可能需要 1 个或几个时钟,具体取决于硬件。您的软件加法器执行一个循环,然后循环 y 中的位数。由于一个循环至少需要 3 条指令(不计算循环开销),因此在最坏的情况下(32 循环 x 3 指令/循环),您的加法器运行可能会慢近 100 倍。展开你的循环以减少循环开销,你会看到不同
【解决方案2】:

你已经替换了这个:

x = 1147483000 + i;

用这个:

while(y != 0) {
    int carry = (x & y);
    x = x ^ y;
    y = carry << 1;
}

当然,您的速度会大大降低!两个整数的+ 是一条汇编指令。您的while 循环执行许多指令,有效地在软件中模拟硬件在执行加法时的行为。


更详细地说,这是full adder 的样子。加上 32 位,ALU 包含 32 个级联单元。这些硬件元素中的每一个都具有非常非常小的延迟。电线的延迟可以忽略不计。因此,如果软件将两个 32 位数字相加,所花费的时间非常少。

另一方面,如果你尝试手动模拟加法,你会让 CPU 进入内存,获取和解码一些指令 32 次,这需要相当长的时间。

【讨论】:

  • 另外,调用函数 100 万次的开销。编译器可能会通过一些具有不同设置(例如 -O3)的自动内联来优化它。但它不会完全摆脱开销。
  • @selbie,我确信将添加作为软件循环重新实现会产生重大影响。当然可以通过简单地使函数内联并再次测试来进行测试。
  • 事实上,现代 CPU 通常使用更复杂的加法器,因为一系列全加器需要与位数成正比的时钟数。 32 位加法器需要 32 个时钟
【解决方案3】:

当您用函数调用替换添加时: 调用函数比简单的加法更耗时,因为函数调用与堆栈操作相关。

在函数中,您将用三个按位运算替换加法 - 它们与加法相比有多快可能是一个问题 - 尽管未经测试无法确认这一点。您可以在这里发布三个按位运算的各个时间吗?:

1.

//tic
while(i--) {
    int carry = (x & y);
}
//toc

2.

//tic
while(i--) {
    x = x ^ y;
}
//toc

3.

//tic
while(i--) {
    y = carry << 1;
}
//toc

但函数调用应该是主要原因。

【讨论】:

    猜你喜欢
    • 2019-05-31
    • 1970-01-01
    • 1970-01-01
    • 2016-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多