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

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

i*2 = i<<1 i*3 = (i<<1) + i; i*10 = (i<<3) + (i<<1) 

等等。

使用say (i<<3)+(i<<1)乘以10比直接使用i*10快吗? 有什么样的input不能以这种方式相乘或分割吗?

简短的回答:不太可能。

长的答案:你的编译器有一个优化器,它知道如何以你的目标处理器体系结构能够快速地进行乘法运算。 你最好的select是清楚地告诉编译器你的意图(即i * 2而不是i << 1),并决定最快的汇编/机器代码序列是什么。 处理器本身甚至有可能已经将乘法指令实现为微码中的一系列移位和相加。

底线 – 不要花太多时间担心这一点。 如果你的意思是转移,转移。 如果你的意思是乘,乘。 做语义上最清楚的事 – 稍后你的同事会感谢你。 或者,更可能的是,如果你不这样做,你会后悔的。

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

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

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

在我testing过的每台机器上,第一台机器至less和第二台机器一样快。 有点令人惊讶的是,它有时更快(例如在Sun Sparc上)。 当硬件不支持快速乘法(当时大多数还没有支持),编译器会将乘法转换成适当的移位和加/减的组合。 而且因为它知道最终的目标,所以有时候可以用比明确写出class次和添加/子目录更less的指令来完成。

请注意,这是15年前的事情。 希望编译器从那以后才变得更好,所以你可以指望编译器做正确的事情,可能比你做得更好。 (另外,代码看起来如此C'ish的原因是因为它已经超过15年了,我当然明显使用std::string和iterators。)

除了这里所有其他的好的答案之外,让我指出另一个理由,当你的意思是分而治之时,不要使用移位。 我从来没有见过有人通过忘记乘法和加法的相对优先级来引入一个bug。 当维护程序员忘记了通过移位来“乘”的时候,我看到了一些错误,这在逻辑上是一个乘法,但在语法 与乘法没有相同的优先级。 x * 2 + zx << 1 + z是非常不同的!

如果你正在使用数字,那么使用算术运算符,如+ - * / % 。 如果你正在处理比特数组,可以使用像& ^ | >>这样的位操作符 & ^ | >> 。 不要混在一起; 一个既有点混淆又有算术的expression式是一个等待发生的错误。

这取决于处理器和编译器。 一些编译器已经用这种方式优化了代码,而另一些则没有。 所以你需要检查每次你的代码需要这样优化。

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

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

它可能会或可能不在你的机器上 – 如果你在意,衡量你的实际使用情况。

一个案例研究 – 从486到核心i7

标杆pipe理很难做到有意义,但我们可以看一些事实。 从http://www.penguin.cz/~literakl/intel/s.html#SAL和http://www.penguin.cz/~literakl/intel/i.html#IMUL我们了解了x86时钟周期算术移位和乘法所需要的。; 说我们坚持“486”(最新列出的),32位寄存器和立即数,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中find延迟时间。;

(从一些英特尔blurb)

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

这让你知道事情到底有多远。 这种优化的琐事 – 比如转移到* – 甚至到了九十年代都被认真考虑过了,现在已经过时了。 位移速度还是比较快,但是当你完成所有的class次并添加结果后,速度会再次变慢。 然后,更多的指令意味着更多的caching故障,stream水线中更多的潜在问题,更多地使用临时寄存器可能意味着更多的保存和从栈中恢复寄存器内容…它很快地变得太复杂,以确定的量化所有影响,但他们主要是负面的。

源代码vs实现中的function

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

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

 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............... 

诸如exclusive或( xor )之类的指令与源代码没有任何关系,但是与自己的任何东西都可以清除所有的位,所以它可以被用来将某些东西设置为0.隐含内存地址的源代码可能不需要被使用。

只要计算机已经存在,这些黑客就已经被使用了。 在3GLs的早期,为了确保开发者的使用,编译器输出必须满足现有的硬核手动优化汇编语言开发。 所产生的代码并不慢,更冗长或者更糟。 编译器很快就采用了很多优化 – 它们比任何单独的汇编语言程序员都可能成为一个更好的集中式存储器,尽pipe总是有机会错过在特定情况下碰巧至关重要的特定优化 – 人类有时可能坚持下来,摸索一些更好的东西,而编译器只是按照他们的说法去做,直到有人把这些经验带回到他们的身上。

所以,即使在某些特定的硬件上移位和添加速度仍然较快,那么编译器作者可能已经准确地确定了它的安全性和有效性。

可维护性

如果你的硬件改变了,你可以重新编译,它会查看目标CPU并做出另一个最好的select,而你不可能想重温你的“优化”或列出哪些编译环境应该使用乘法,哪些应该改变。 想想在10年前写的所有非功率位移的“优化”,现在正在放慢它们在现代处理器上运行的代码。

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

执行乘法或除法的奇怪的移位代码远远不能expression你想要实现的东西,所以其他开发者会被这个困惑,而一个混淆的程序员更可能引入错误或删除一些重要的东西来恢复看起来的理智。 如果你只是在真正有益的时候做非显而易见的事情,然后logging下来(但是不要logging其他直观的东西),那么每个人都会更快乐。

一般解决scheme还是部分解决

如果你有一些额外的知识,比如你的int只能存储xyz ,那么你可以编写一些适合这些值的指令,并且比编译器没有这种洞察力,需要一个适用于所有int值的实现。 例如,考虑你的问题:

乘法和除法可以使用位操作符来实现…

你说明乘法,但是除法呢?

 int x; x >> 1; // divide by 2? 

根据C ++标准5.8:

-3- E1 >> E2的值是E1右移E2位的位置。 如果E1具有无符号types,或者如果E1具有带符号types和非负值,则结果的值是E1的商除以提高到功率E2的数量2的整数部分。 如果E1有签名types和负值,则结果值是实现定义的。

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

你可能会说:“我不在乎…… int是存储员工的年龄,它永远不会是负面的”。 如果你有这种特殊的洞察力,那么是的 – 你的安全优化可能会被编译器传递,除非你明确地在你的代码中进行。 但是,很多时候你都不会有这种洞察力, 所以这是有风险的 ,很less有用的,而其他的编程人员也不会知道你已经下定决心,我们正在处理……对他们来说,看起来完全安全的变化可能会因为你的“优化”而倒闭。

有什么样的input不能以这种方式相乘或分割吗?

是的……如上所述,负数在按位移“划分”时具有实现定义的行为。

刚试过在我的机器上编译这个:

 int a = ...; int b = a * 10; 

拆卸时产生输出:

 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 

这个版本比纯手工优化的代码更快,更纯净。

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

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

事实上,这种被称为“魔法师”的分裂技巧实际上可以产生巨大的回报。 再次,您应该首先查看是否需要。 但是如果你使用它,那么有一些有用的程序可以帮助你找出相同分割语义所需要的指令。 这是一个例子: 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 

会生成:

 mov eax,9999999 mov edx,0A7C5AC47h add eax,1 .if !CARRY? mul edx .endif shr edx,16 

Shift和整数乘法指​​令在大多数现代CPU上具有相似的性能 – 整数乘法指​​令在20世纪80年代相对较慢,但总的来说这不再是事实。 整数乘法指​​令可能具有更高的延迟 ,所以可能仍然存在优先移位的情况。 同样的情况下,你可以保持更多的执行单位繁忙(虽然这可以削减两种方式)。

整数划分虽然还是比较慢的,所以使用2的幂乘而不是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; } 

输出:

 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 

所以,如果你想帮助编译器,那么确保被除数的variables或expression式是明确无符号的。

它完全取决于目标设备,语言,目的等

像素在video卡驱动程序? 很可能,是的!

您的部门的.NET业务应用程序? 绝对没有理由甚至没有看到它。

对于移动设备的高性能游戏来说,这可能是值得研究的,但只有在进行了更简单的优化之后。

除非你绝对需要,你的代码意图需要移位而不是乘法/除法。

在典型的日子里 – 你可以节省很less的机器周期(或者是松散的,因为编译器知道怎样优化),但是代价是不值得的 – 你花时间在小细节而不是实际工作上,维护代码变得更加困难,你的同事会诅咒你

您可能需要执行高负载计算,每个保存周期意味着运行时间的分钟。 但是,您应该一次优化一个地方,每次都进行性能testing,看看您是否真的做得更快或破坏了编译器的逻辑。

据我所知,在某些机器上乘法可能需要高达16到32个机器周期。 所以是的 ,根据机器types的不同,移位运算符比乘法/除法更快。

但是某些机器确实有他们的math处理器,其中包含特殊的乘法/除法指令。

我同意德鲁·霍尔的标记。 答案可以使用一些额外的笔记。

对于绝大多数软件开发人员来说,处理器和编译器不再与这个问题有关。 我们大多数人远远超过了8088和MS-DOS。 这可能只与那些仍在开发embedded式处理器的人有关。

在我的软件公司Math(add / sub / mul / div)应该用于所有math。 在转换数据types时应该使用Shift,例如。 ushort字节为n >> 8而不是 n / 256。

如果是有符号整数和右移对分,可以有所作为。 对于负数,轮换向负无穷转,而分向零轮。 当然,编译器会将分区更改为更便宜的分区,但通常会将其更改为与分区具有相同舍入行为的分区,因为要么无法certificate该variables不会是负数,要么就不会关心。 所以,如果你能certificate一个数字不会是负数,或者你不关心它将以何种方式进行,你可以通过一种更有可能产生影响的方式进行优化。

Pythontesting对相同的随机数执行1亿次相同的乘法运算。

 >>> from timeit import timeit >>> setup_str = 'import scipy; from scipy import random; scipy.random.seed(0)' >>> N = 10*1000*1000 >>> timeit('x=random.randint(65536);', setup=setup_str, number=N) 1.894096851348877 # Time from generating the random #s and no opperati >>> timeit('x=random.randint(65536); x*2', setup=setup_str, number=N) 2.2799630165100098 >>> timeit('x=random.randint(65536); x << 1', setup=setup_str, number=N) 2.2616429328918457 >>> timeit('x=random.randint(65536); x*10', setup=setup_str, number=N) 2.2799630165100098 >>> timeit('x=random.randint(65536); (x << 3) + (x<<1)', setup=setup_str, number=N) 2.9485139846801758 >>> timeit('x=random.randint(65536); x // 2', setup=setup_str, number=N) 2.490908145904541 >>> timeit('x=random.randint(65536); x / 2', setup=setup_str, number=N) 2.4757170677185059 >>> timeit('x=random.randint(65536); x >> 1', setup=setup_str, number=N) 2.2316000461578369 

因此,在python中,python中的两个幂乘以二乘而非乘法/除法,有轻微的改善(划分〜10%,乘法〜1%)。 如果它是两个非强国,那么可能会有相当大的放缓。

再次,这些#将根据您的处理器,您的编译器(或解释器 – 为了简单在python中)而改变。

和其他人一样,不要过早地优化。 编写非常可读的代码,如果configuration文件不够快,则尝试优化慢速部分。 记住,你的编译器在优化方面比你好得多。

编译器不能做的优化是因为它们只能用于一组缩减的input。

下面是c + +示例代码,可以做一个64位的“相乘乘法”更快的分工。 分子和分母都必须低于一定的阈值。 请注意,它必须被编译为使用64位的指令,实际上比正常的除法更快。

 #include <stdio.h> #include <chrono> static const unsigned s_bc = 32; static const unsigned long long s_p = 1ULL << s_bc; static const unsigned long long s_hp = s_p / 2; static unsigned long long s_f; static unsigned long long s_fr; static void fastDivInitialize(const unsigned d) { s_f = s_p / d; s_fr = s_f * (s_p - (s_f * d)); } static unsigned fastDiv(const unsigned n) { return (s_f * n + ((s_fr * n + s_hp) >> s_bc)) >> s_bc; } static bool fastDivCheck(const unsigned n, const unsigned d) { // 32 to 64 cycles latency on modern cpus const unsigned expected = n / d; // At least 10 cycles latency on modern cpus const unsigned result = fastDiv(n); if (result != expected) { printf("Failed for: %u/%u != %u\n", n, d, expected); return false; } return true; } int main() { unsigned result = 0; // Make sure to verify it works for your expected set of inputs const unsigned MAX_N = 65535; const unsigned MAX_D = 40000; const double ONE_SECOND_COUNT = 1000000000.0; auto t0 = std::chrono::steady_clock::now(); unsigned count = 0; printf("Verifying...\n"); for (unsigned d = 1; d <= MAX_D; ++d) { fastDivInitialize(d); for (unsigned n = 0; n <= MAX_N; ++n) { count += !fastDivCheck(n, d); } } auto t1 = std::chrono::steady_clock::now(); printf("Errors: %u / %u (%.4fs)\n", count, MAX_D * (MAX_N + 1), (t1 - t0).count() / ONE_SECOND_COUNT); t0 = t1; for (unsigned d = 1; d <= MAX_D; ++d) { fastDivInitialize(d); for (unsigned n = 0; n <= MAX_N; ++n) { result += fastDiv(n); } } t1 = std::chrono::steady_clock::now(); printf("Fast division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT); t0 = t1; count = 0; for (unsigned d = 1; d <= MAX_D; ++d) { for (unsigned n = 0; n <= MAX_N; ++n) { result += n / d; } } t1 = std::chrono::steady_clock::now(); printf("Normal division time: %.4fs\n", (t1 - t0).count() / ONE_SECOND_COUNT); getchar(); return result; }