使用C中的移位运算符的乘法和除法实际上更快吗?

eku*_*eku 282 c c++ bit-shift multiplication division

例如,可以使用位运算符来实现乘法和除法

i*2 = i<<1
i*3 = (i<<1) + i;
i*10 = (i<<3) + (i<<1)
Run Code Online (Sandbox Code Playgroud)

等等.

实际上使用say (i<<3)+(i<<1)乘以10比i*10直接使用更快吗?是否有任何类型的输入不能以这种方式倍增或分割?

Dre*_*all 474

简短回答:不太可能.

答案很长:您的编译器中有一个优化器,它知道如何像目标处理器架构一样快速地倍增.最好的办法是清楚地告诉编译器你的意图(即i*2而不是i << 1)并让它决定最快的汇编/机器代码序列是什么.处理器本身甚至可能将乘法指令实现为一系列移位并添加微码.

底线 - 不要花很多时间担心这个问题.如果你的意思是转移,转移.如果你的意思是乘以,乘以.做什么是语义上最清晰的 - 你的同事稍后会感谢你.或者,如果不这样做,更有可能在以后诅咒你.

  • +1"你的同事会在以后感谢你." (203认同)
  • 是的,正如所说的几乎每个应用程序的可能收益将完全超过所引入的默默无闻.不要过早担心这种优化.建立什么是明确的,识别瓶颈并从那里优化... (28认同)
  • 同意,优化可读性和可维护性可能会让您花费更多时间来实际优化*profiler*所说的热门代码路径. (4认同)
  • 这些评论听起来就像是在告诉编译器如何完成工作而放弃潜在的性能.不是这种情况.你实际上从x86上的``gcc -O3`获得*更好*代码,返回i*10`而不是移位版本(http://goo.gl/PwKKfs).作为一个经常看编译器输出的人(参见我的许多asm /优化答案),我并不感到惊讶.有时它可以帮助[将编译器手持到一种处理方式](http://stackoverflow.com/a/34410357/224132),但这不是其中之一.gcc擅长整数数学,因为它很重要. (4认同)
  • @PaulWieland 嵌入式处理器与 x86 兼容处理器有很大不同。MSP430 既没有除法指令,也没有乘法指令。其中一些具有单独的乘法_外设_,速度不是快如闪电,2 个 16 位值需要 4 个周期,结果被分解为 4 个 8 位寄存器。当您在 16MHz 下工作并且没有花哨的管道优化或预测执行时,这种事情开始变得重要了。是的,您可以将其留给编译器,但同样,您比此处讨论的速度慢两个数量级。 (3认同)
  • 我使用优化 -O3 在 gcc 上针对 cortex-a9(没有硬件划分)测试了 `i / 32` 与 `i &gt;&gt; 5` 和 `i / 4` 与 `i &gt;&gt; 2` 的比较,得到的汇编结果与相同。我不喜欢首先使用除法,但它描述了我的意图并且输出是相同的。 (3认同)

Jam*_*nze 92

只是一个具体的衡量标准:多年前,我对两个版本的哈希算法进行了基准测试:

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = 127 * h + (unsigned char)*s;
        ++ s;
    }
    return h;
}
Run Code Online (Sandbox Code Playgroud)

unsigned
hash( char const* s )
{
    unsigned h = 0;
    while ( *s != '\0' ) {
        h = (h << 7) - h + (unsigned char)*s;
        ++ s;
    }
    return h;
}
Run Code Online (Sandbox Code Playgroud)

在我对它进行基准测试的每台机器上,第一台至少与第二台机器一样快.有点令人惊讶的是,它有时更快(例如在Sun Sparc上).当硬件不支持快速乘法(并且大部分时间不支持)时,编译器会将乘法转换为shift和add/sub的适当组合.并且因为它知道了最终目标,所以它有时可以在较少的指令中执行,而不是在您明确编写shift和add/subs时.

请注意,这与15年前相似.希望编译器从那以后只会变得更好,所以你几乎可以指望编译器做正确的事情,可能比你更好.(此外,代码看起来如此C'ish的原因是因为它超过15年前.我显然std::string今天使用和迭代器.)

  • 您可能对以下博客文章感兴趣,其中作者指出,现代优化编译器似乎对工程师可能使用的常见模式进行反向工程,将其更有效地用于数学形式,以便为他们真正生成最有效的指令序列.http://shape-of-code.coding-guidelines.com/2009/06/30/searching-for-the-source-line-implementing-3n1/ (5认同)

Eri*_*ert 64

除了这里所有其他好的答案之外,让我指出在你意味着分裂或乘法时不使用shift的另一个原因.我从来没有见过有人通过忘记乘法和加法的相对优先级来引入错误.我看到介绍的时候维护程序员忘记了通过转变"乘"是错误逻辑乘法而不是语法相同的优先级倍增.x * 2 + z并且x << 1 + z非常不同!

如果你正在处理数字,那么使用算术运算符+ - * / %.如果您正在处理位数组,请使用像bit这样的位运算符& ^ | >>.不要混合它们; 一个既麻烦又算术的表达式是一个等待发生的错误.

  • @Ivan:"(hi << 8)| lo"更清楚.设置位数组的低位是**不添加整数**.它是**设置位**,所以写入设置位的代码. (32认同)
  • @Joel:好的.*如果你还记得你需要它们.*我的观点是很容易忘记你这样做.那些养成阅读"x << 1"的心理习惯的人就好像是"x*2"一样,养成了这样的心理习惯,即认为<<与乘法具有相同的优先权,而不是乘法. (21认同)
  • 用简单的括号可以避免吗? (5认同)
  • 好吧,我发现表达“(hi &lt;&lt; 8) + lo”比“hi*256 + lo”更能揭示意图。可能这是一个品味问题,但有时写位更清晰。在大多数情况下,虽然我完全同意你的观点。 (2认同)

Jen*_*ens 50

这取决于处理器和编译器.有些编译器已经以这种方式优化代码,而其他编译器则没有 因此,每次需要以这种方式优化代码时都需要检查.

除非你迫切需要优化,否则我不会为了保存汇编指令或处理器周期而扰乱我的源代码.

  • 通常,编译器会比您更好地优化代码. (54认同)
  • 只是添加一个粗略的估计:在一个典型的16位处理器(80C166)上增加两个整数来自1-2个周期,10个周期的乘法和20个周期的除法.如果你将i*10优化为多个操作(每个移动另外+1个循环),还有一些移动操作.除非乘法/除法乘以2,否则最常见的编译器(Keil/Tasking)不会优化. (3认同)

Ton*_*roy 38

使用say(i << 3)+(i << 1)乘以10比直接使用i*10实际上更快吗?

它可能在您的机器上,也可能不在您的机器上 - 如果您关心,请测量您的实际使用情况.

案例研究 - 从486到核心i7

基准测试很难有意义,但我们可以看一些事实.来自http://www.penguin.cz/~literakl/intel/s.html#SALhttp://www.penguin.cz/~literakl/intel/i.html#IMUL,我们了解x86时钟周期算术移位和乘法需要.假设我们坚持"486"(最新列出的一个),32位寄存器和immediates,IMUL需要13-42个周期和IDIV 44.每个SAL需要2个,并且加1,所以即使有几个一起移动表面看起来像一个胜利者.

这些天,核心i7:

(来自http://software.intel.com/en-us/forums/showthread.php?t=61481)

整数加法的延迟为1个周期,整数乘法的延迟为3个周期.您可以在http://www.intel.com/products/processor/manuals/上的"英特尔®64和IA-32架构优化参考手册"的附录C中找到延迟和吞吐量.

(来自某些英特尔模糊)

使用SSE,Core i7可以同时发出加法和乘法指令,从而在每个时钟周期产生8个浮点运算(FLOP)的峰值速率

这可以让你了解事情的进展.优化琐事 - 比如位移与*- 即使在90年代也被认真对待,现在已经过时了.比特移位仍然更快,但是当您完成所有轮班并添加结果时,对于非功率为2的mul/div,它会再次变慢.然后,更多的指令意味着更多的缓存故障,更多的流水线问题,更多的临时寄存器的使用可能意味着更多地从堆栈中保存和恢复寄存器内容......它很快就变得过于复杂,无法明确地量化所有影响,但它们是主要是消极的.

源代码与实现中的功能

更一般地,您的问题标记为C和C++.作为第三代语言,它们专门用于隐藏底层CPU指令集的细节.为了满足他们的语言标准,他们必须支持乘法和移位操作(以及许多其他操作),即使底层硬件没有.在这种情况下,他们必须使用许多其他指令合成所需的结果.同样,如果CPU缺少浮点运算并且没有FPU,它们必须为浮点运算提供软件支持.现代CPU都支持*<<,所以这看起来荒谬的理论和历史,但意义的是,选择实现的自由是双向的:即使CPU具有实现在一般情况下,在源代码中所请求的操作指令,编译器可以自由选择它喜欢的其他东西,因为它对于编译器所面临的特定情况更好.

示例(使用假设的汇编语言)

source           literal approach         optimised approach
#define N 0
int x;           .word x                xor registerA, registerA
x *= N;          move x -> registerA
                 move x -> registerB
                 A = B * immediate(0)
                 store registerA -> x
  ...............do something more with x...............
Run Code Online (Sandbox Code Playgroud)

像exclusive或(xor)这样的指令与源代码没有任何关系,但是对任何东西进行清除会清除所有的位,因此它可以用来将某些东西设置为0.暗示内存地址的源代码可能不需要任何使用.

只要计算机出现,就会使用这种黑客攻击.在3GL的早期阶段,为了确保开发人员的注意,编译器输出必须满足现有的核心手动优化汇编语言开发.社区认为生成的代码不是更慢,更冗长或更糟糕.编译器很快就采用了很多很好的优化 - 它们比任何单独的汇编语言程序员都成为一个更好的集中存储器,尽管他们总是有可能错过在特定情况下恰好至关重要的特定优化 - 人类有时可以坚持下去并摸索出更好的东西,而编译器就像他们被告知的那样,直到有人将这种体验反馈给他们.

因此,即使在某些特定硬件上移位和添加仍然更快,编译器编写者可能已经确切地确定了它既安全又有益.

可维护性

如果您的硬件发生了变化,您可以重新编译,它会查看目标CPU并做出另一个最佳选择,而您不太可能想要重新审视您的"优化"或列出哪些编译环境应该使用乘法而哪些应该移位.想想10年前写的所有非幂二位移位"优化",它们现在正在减慢它们在现代处理器上运行时的代码......!

值得庆幸的是,像GCC这样的优秀编译器通常可以在启用任何优化时直接替换一系列位移和算术(即...main(...) { return (argc << 4) + (argc << 2) + argc; }- > imull $21, 8(%ebp), %eax),因此即使不修复代码,重新编译也可能有所帮助,但这并不能保证.

实现乘法或除法的奇怪的位移代码远不如你在概念上试图实现的那样表达,因此其他开发人员会对此感到困惑,并且一个混乱的程序员更有可能引入错误或删除一些必要的东西以恢复看似理智.如果你只是在非常明显有益的情况下做非显而易见的事情,然后很好地记录它们(但不记录其他直观的东西),每个人都会更快乐.

一般解决方案与部分解

如果你有一些额外的知识,比如,你int真的会只存储值x,y并且z,那么你也许能够制定出这些值工作的一些指令,让您在结果之快,超过当编译器没有这种洞察力需要一种适用于所有int价值观的实施.例如,考虑一下你的问题:

使用位运算符可以实现乘法和除法...

你说明乘法,但分裂怎么样?

int x;
x >> 1;   // divide by 2?
Run Code Online (Sandbox Code Playgroud)

根据C++标准5.8:

-3- E1 >> E2的值是E1右移E2位的位置.如果E1具有无符号类型或者E1具有有符号类型和非负值,则结果的值是E1的商除以提升到功率E2的数量2的积分.如果E1具有带符号类型和负值,则结果值是实现定义的.

因此,当x负数时,您的位移具有实现定义结果:它可能在不同的机器上以不同的方式工作.但是,/工作更加可预测. (它可能也不完全一致,因为不同的机器可能具有不同的负数表示,因此即使构成表示的位数相同,也会有不同的范围.)

你可能会说"我不在乎......那int是存储员工的年龄,它永远不会消极".如果您有这种特殊的洞察力,那么是 - 您的>>安全优化可能会被编译器传递,除非您在代码中明确地执行此操作.但是,它很冒险而且很少有用,因为在很多时候你都没有这种洞察力,而其他程序员在使用相同的代码时也不会知道你对这些数据的某些不寻常的期望打赌了.将要处理......对他们来说似乎是一个完全安全的改变可能会因为你的"优化"而适得其反.

是否有任何类型的输入不能以这种方式倍增或分割?

是的......如上所述,当通过位移"划分"时,负数具有实现定义的行为.

  • 非常好的答案.Core i7与486的对比是有启发性的! (2认同)

use*_*016 33

刚尝试在我的机器上编译:

int a = ...;
int b = a * 10;
Run Code Online (Sandbox Code Playgroud)

拆卸时会产生输出:

MOV EAX,DWORD PTR SS:[ESP+1C] ; Move a into EAX
LEA EAX,DWORD PTR DS:[EAX+EAX*4] ; Multiply by 5 without shift !
SHL EAX, 1 ; Multiply by 2 using shift
Run Code Online (Sandbox Code Playgroud)

这个版本比手动优化的代码更快,具有纯粹的移位和添加功能.

你真的不知道编译器会想出什么,所以最好简单地编写一个正常的乘法并让他优化他想要的方式,除非你知道编译器无法优化的非常精确的情况.


Mik*_*wan 21

移位通常比在指令级别乘以快得多,但您可能会浪费时间进行过早优化.编译器可能会在编译时执行这些优化.自己动手会影响可读性,并且可能对性能没有影响.如果您已经分析并发现这是一个瓶颈,那么做这样的事情可能是值得的.

实际上,被称为"魔术师"的分裂技巧实际上可以产生巨大的回报.您应该再次首先查看是否需要它.但是,如果您确实使用它,那么有一些有用的程序可以帮助您找出相同除法语义所需的指令.以下是一个示例:http://www.masm32.com/board/index.php?topic = 12421.0

我从MASM32上的OP线程中解除的一个例子:

include ConstDiv.inc
...
mov eax,9999999
; divide eax by 100000
cdiv 100000
; edx = quotient
Run Code Online (Sandbox Code Playgroud)

会产生:

mov eax,9999999
mov edx,0A7C5AC47h
add eax,1
.if !CARRY?
    mul edx
.endif
shr edx,16
Run Code Online (Sandbox Code Playgroud)

  • 没有关于喜欢数学的随机论坛帖子.任何喜欢数学的人都知道生成一个真正的"随机"论坛帖子是多么困难. (30认同)
  • 链接似乎是一个关于喜欢数学的随机论坛帖子. (15认同)
  • @Drew由于某种原因你的评论让我笑了起来,把我的咖啡洒了.谢谢. (7认同)
  • 关于喜欢数学的那个主题的+1是EPIC! (4认同)
  • *如果您已经分析并发现这是一个瓶颈,那么做这样的事情可能是值得的* **并再次实施替代方案和分析并获得至少 10 倍的性能优势**。 (2认同)

Pau*_*l R 11

移位和整数乘法指​​令在大多数现代CPU上具有相似的性能 - 整数乘法指​​令在20世纪80年代相对较慢,但总的来说这不再是真的.整数乘法指​​令可能具有更高的延迟,因此可能仍然存在优选移位的情况.对于可以让更多执行单元保持忙碌的情况同样如此(尽管这可以同时削减两种方式).

整数除法仍然相对较慢,因此使用转换而不是除以2的幂仍然是一个胜利,并且大多数编译器将实现此作为优化.但请注意,为使此优化有效,红利需要是无符号的,或者必须知道为正.对于负红利,转移和鸿沟并不等同!

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 5; i >= -5; --i)
    {
        printf("%d / 2 = %d, %d >> 1 = %d\n", i, i / 2, i, i >> 1);
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

输出:

5 / 2 = 2, 5 >> 1 = 2
4 / 2 = 2, 4 >> 1 = 2
3 / 2 = 1, 3 >> 1 = 1
2 / 2 = 1, 2 >> 1 = 1
1 / 2 = 0, 1 >> 1 = 0
0 / 2 = 0, 0 >> 1 = 0
-1 / 2 = 0, -1 >> 1 = -1
-2 / 2 = -1, -2 >> 1 = -1
-3 / 2 = -1, -3 >> 1 = -2
-4 / 2 = -2, -4 >> 1 = -2
-5 / 2 = -2, -5 >> 1 = -3
Run Code Online (Sandbox Code Playgroud)

因此,如果您想帮助编译器,那么请确保被除数中的变量或表达式是显式无符号的.

  • 例如,在PlayStation 3的PPU上对整数乘法进行微码编码,并使整个流水线停止.建议在某些平台上避免整数倍数:) (4认同)
  • 许多无符号除法 - 假设编译器知道如何 - 使用无符号乘法实现.一个或两个乘以@几个时钟周期,每个时钟周期可以完成相同的工作@ 40个周期. (2认同)