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


287

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

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

等等。

使用说(i<<3)+(i<<1)乘以10 真的比i*10直接使用要快吗?是否存在无法以这种方式相乘或除法的输入?


8
实际上,可以用除以2的幂以外的常数进行廉价除法,但这是一个棘手的子喷气式飞机,您在问题中未对“ / Division…/ divided”进行公正对待。参见例如hackersdelight.org/divcMore.pdf(或者,如果可以的话,拿这本书“ Hacker's delight”)。
Pascal Cuoq

46
听起来好像很容易测试。
juanchopanza 2011年

25
和往常一样-这取决于。曾几何时,我在Intel 8088(IBM PC / XT)上的汇编器中尝试过此操作,乘法运算耗费了数十亿个时钟。移位和添加执行的速度要快得多,所以这似乎是个好主意。但是,在乘法时,总线单元可以自由地填充指令队列,然后可以立即开始下一条指令。经过一系列移位和添加后,指令队列将为空,CPU将必须等待从内存中提取下一条指令(一次一个字节!)。测量,测量,测量!
Bo Persson

19
另外,请注意,仅对无符号整数定义了右移。如果您有符号整数,则不确定是从左开始填充0还是最高位。(并且不要忘记一年后其他人(甚至是您自己)阅读代码所花费的时间!)
Kerrek SB 2011年

29
实际上,一个好的优化编译器会在速度更快时通过移位实现乘法和除法。
Peter G.

Answers:


486

简短答案:不太可能。

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

最重要的是-不要花很多时间担心这个问题。如果您要转移,那就转移。如果要乘,就乘。做语义上最清晰的事情-您的同事稍后会感谢您。或者,更可能的是,如果以后否则诅咒您。


31
是的,正如所说的,几乎每个应用程序可能获得的收益将完全超过引入的模糊性。不必过早担心这种优化。建立明显清晰的内容,确定瓶颈并从那里进行优化...
Dave

4
商定,针对可读性和可维护性进行优化可能会给您更多的时间,使您可以花更多的时间来实际优化探查器所说的热代码路径。
doug65536

5
这些注释使您听起来好像放弃了告诉编译器如何完成其​​工作的潜在性能。这是不是这种情况。实际上,从x86上获得的代码比从shift版本获得的代码更好。作为一个经常查看编译器输出的人(请参阅我的许多asm /优化答案),我并不感到惊讶。有时可以帮助将编译器固定为一种处理方式,但这不是其中一种。gcc很擅长整数数学,因为它很重要。gcc -O3return i*10
彼得·科德斯

刚刚下载了具有的arduino草图millis() >> 2; 要求划分是否会太多?
Paul Wieland

1
我用优化-O3在gcc上对i / 32vs i >> 5i / 4vs i >> 2在gcc上测试了cortex-a9(没有硬件划分),并且得到的组件完全相同。我不喜欢先使用除法,但是它描述了我的意图,并且输出是相同的。
robsn

91

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

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;
}

在我对其进行基准测试的每台机器上,第一台至少和第二台一样快。令人惊讶的是,有时速度更快(例如在Sun Sparc上)。当硬件不支持快速乘法(大多数都不支持快速乘法)时,编译器会将乘法转换为移位和加/减的适当组合。而且由于它知道最终目标,所以有时可以用比您明确地编写班次和加/减时更少的指令来做到。

请注意,这就像15年前。希望从那以后编译器只会变得更好,因此您可以指望编译器做正确的事情,可能比您做的更好。(此外,代码看起来如此“ C”的原因是因为它已经超过15年了。我std::string今天显然会使用和迭代器。)


5
您可能对以下博客文章感兴趣,其中作者指出,现代优化编译器似乎对通用模式进行了逆向工程,程序员可能会使用这种模式将效率更高的数学形式转换为数学形式,从而真正为他们生成最有效的指令序列。 。shape-of-code.coding-guidelines.com/2009/06/30/...
帕斯卡尔Cuoq

@PascalCuoq对此没有什么新鲜的。我在20年前就对Sun CC发现了几乎相同的东西。
詹姆斯·坎泽

66

除了这里所有其他好的答案之外,让我指出在表示除法或乘法时不使用移位的另一个原因。我从未见过有人通过忘记乘法和加法的相对优先级来引入错误。当维护程序员忘记通过移位进行“乘法”在逻辑上是乘法,但在语法却没有与乘法相同的优先级时,我已经看到了引入的错误。x * 2 + z而且x << 1 + z有很大的不同!

如果您正在处理数字,请使用算术运算符,例如+ - * / %。如果您正在处理位数组,请使用位旋转运算符,例如& ^ | >>。不要混在一起;一个既有点摇摆又有算术的表达式是一个等待发生的错误。


5
用简单的括号可以避免吗?
Joel B

21
@Joel:好的。如果您记得自己需要它们。我的观点是,很容易忘记自己。养成阅读“ x << 1”就像是“ x * 2”的心理习惯的人养成一种思维习惯,即<< <<与乘法相同,但不是。
埃里克·利珀特

1
好吧,我发现表达式“(hi << 8)+ lo”比“ hi * 256 + lo”更具意图。也许这是一个品味问题,但有时更清楚地写位纠结。尽管在大多数情况下,我完全同意您的观点。
伊万·丹尼洛夫

32
@Ivan:“(hi << 8)| lo”更加清晰。设置位数组的低位不是整数的加法运算。是设置位,因此编写设置位的代码。
埃里克·利珀特

1
哇。以前没有这样想过。谢谢。
伊万·丹尼洛夫

50

这取决于处理器和编译器。一些编译器已经以这种方式优化代码,而另一些则没有。因此,您需要每次检查需要以这种方式优化代码的时间。

除非您迫切需要优化,否则我不会为了保存汇编指令或处理器周期而对源代码进行加扰。


3
仅作一个粗略的估计:在典型的16位处理器(80C166)上,在1-2个周期内添加两个整数,在10个周期内相乘,在20个周期内相除。如果将i * 10优化为多个操作,则还要进行一些移动操作(每个操作再移动+1个周期)。除非对乘法/除法乘以2的幂,否则最常见的编译器(Keil / Tasking)不会优化。–
Jens

55
通常,编译器比您更好地优化代码。
user703016 2011年

我同意,在将“数量”相乘时,乘法运算符通常会更好,但是当将有符号值除以2的幂时,该>>运算符会比快,/并且,如果有符号值可以为负数,则它在语义上也通常比较优越。如果需要一个x>>4将产生的值,那将比清晰得多x < 0 ? -((-1-x)/16)-1 : x/16;,而且我无法想象编译器如何将后一种表达式优化为更好的形式。
2013年

38

使用(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位寄存器和立即数,IMUL占用13-42个周期,IDIV占用44。每个SAL占用2,并加1,所以即使其中的一些一起在表面上转移像赢家一样

这些天,随着核心i7:

(来自http://software.intel.com/zh-cn/forums/showthread.php?t=61481

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

(摘自一些Intel blurb)

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

这使您了解事情的进展。*甚至在90年代就被认真对待的优化琐事-像位移与- 相比现在已经过时了。位移仍然更快,但是对于非2乘方/ 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...............

诸如Exclusive或(xor)之类的指令与源代码没有关系,但是对其进行异或运算会清除所有位,因此可以将其设置为0。隐含内存地址的源代码可能不需要使用任何代码。

只要计算机存在,这类黑客就一直被使用。在3GL的早期,为了确保开发人员的使用,编译器的输出必须满足现有的核心手动优化汇编语言开发人员的要求。社区认为所生成的代码不会变慢,更冗长或更糟。编译器迅速采用了许多重大优化措施-它们成为比任何单个汇编语言程序员都可能更好的集中存储方式,尽管他们总是有机会错过在特定情况下至关重要的特定优化措施-人们有时可以总结一下,摸索出更好的东西,而编译器则按照被告知的方式工作,直到有人将经验反馈给他们。

因此,即使在某些特定的硬件上转移和添加仍然更快,那么编译器编写者可能会在安全和有益的情况下准确地进行计算。

可维护性

如果更改了硬件,则可以重新编译,并且它将查看目标CPU并做出另一个最佳选择,而您不太可能希望重新访问“优化”或列出哪些编译环境应该使用乘法,哪些应该移位。想想10年前写的所有非2幂次移位的“优化”,它们现在在现代处理器上运行时正在放慢它们的代码...!

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

实现乘法或除法的奇怪的移位代码无法充分表达您在概念上想要实现的目标,因此其他开发人员会对此感到困惑,并且困惑的程序员更可能会引入错误或删除一些必要的东西,以恢复看似理智的状态。如果您仅在非显而易见的事情真正受益时才做,然后很好地记录下来(但无论如何也不要记录其他直观的事情),那么每个人都会更加快乐。

一般解决方案与部分解决方案

如果你有一些额外的知识,比如,你int真的会只存储值xy并且z,然后你可以制定出一些指令,对于那些价值的工作,你更快地得到您的结果比当编译器没有这种洞察力,并需要适用于所有int价值的实施方案。例如,考虑您的问题:

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

您说明了乘法,但是除法又如何呢?

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

根据C ++标准5.8:

-3- E1 >> E2的值是E1右移E2位的位置。如果E1具有无符号类型,或者E1具有带符号类型和非负值,则结果的值是E1商的整数部分除以加到幂E2的数量2。如果E1具有带符号的类型和负值,则结果值是实现定义的。

因此,当x为负数时,您的移位具有一个实现定义的结果:在不同的机器上,它的工作方式可能不同。但是,/工作远比预期的要好。 (这也可能不是完全一致的,因为不同的机器可能具有不同的负数表示形式,因此即使在表示位数的位数相同的情况下,范围也不同。)

您可能会说:“我不在乎...那int是在存储员工的年龄,它永远不会是负面的”。如果您有这种特殊的见识,那么可以-您的>>安全优化可能会被编译器传递,除非您在代码中明确进行了优化。但是,这是冒险的,并且很少会用到,因为在很多时候您不会有这种洞察力,并且其他从事同一代码的程序员也不知道您是否对数据的某些非常规期望下了赌注将会处理...由于您的“优化”,对他们来说似乎完全安全的更改可能适得其反。

是否存在无法以这种方式相乘或除法的输入?

是的...如上所述,当通过移位将“负数”分开时,负数具有实现定义的行为。


2
很好的答案。酷睿i7与486的比较很有启发性!
德鲁·霍尔

在所有常见的体系结构上,intVal>>1它们具有相同的语义,但intVal/2有时会有所不同。如果需要以可移植的方式计算通用架构所能产生的价值intVal >> 1,则表达式将变得更加复杂且难于阅读,并且可能会生成劣于为其生成的代码intVal >> 1
超级猫

35

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

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

该版本比纯手工移位和加法的手动优化代码要快。

您实际上永远都不知道编译器将要做什么,因此最好编写一个普通的乘法并让他优化他想要的方式,除非在非常精确的情况下,您知道编译器无法优化。


1
如果您跳过了有关向量的部分,那么您将为此大赞。如果编译器可以解决乘法问题,那么它也可以看到向量不变。
Bo Persson

如果不作一些非常危险的假设,编译器如何知道向量大小不会改变?还是您从未听说过并发...
Charles Goodwin

1
好的,所以您遍历没有锁的全局向量吗?而且,我遍历了一个未使用其地址的本地向量,并仅调用const成员函数。至少我的编译器意识到矢量大小不会改变。(很快就会有人标记我们聊天:-)。
Bo Persson

1
@BoPersson最后,在所有这些时间之后,我删除了关于编译器无法进行优化的声明vector<T>::size()。我的编译器很古老!:)
user703016

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

会产生:

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

7
由于某种原因,@ Drew的评论使我发笑并洒了咖啡。谢谢。
asawyer 2011年

30
没有关于喜欢数学的随机论坛线程。任何喜欢数学的人都知道生成真正的“随机”论坛线程有多么困难。
Joel B

1
如果您已经剖析并发现这是一个瓶颈 并再次实施替代方案和剖析并获得至少10倍的性能优势,那么做这样的事情可能只是值得的
Lie Ryan

12

移位和整数乘法指​​令在大多数现代CPU上具有相似的性能-整数乘法指​​令在1980年代相对较慢,但总的来说,这不再成立。整数乘法指​​令可能具有更高的延迟,因此在某些情况下,最好还是使用移位。同理,可以让更多的执行单元处于繁忙状态(尽管这可能会双向中断)。

不过,整数除法仍然相对较慢,因此使用移位而不是除以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

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


4
整数乘法会在PlayStation 3的PPU上进行微编码,从而使整个流水线停滞。建议仍在某些平台上避免整数乘法:)
Maister

2
假设编译器知道如何,许多无符号除法都是使用无符号乘法实现的。一个或两个乘法@每个时钟周期可以完成与除法@每个40个周期以上的工作。
Olof Forshell

1
@Olof:正确,但仅对除以编译时常数有效
Paul R

4

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

显卡驱动程序中的像素失真?很有可能,是的!

您部门的.NET业务应用程序?绝对没有理由对此进行调查。

对于用于移动设备的高性能游戏,可能值得研究,但是只有在执行了更轻松的优化之后。


2

除非您绝对需要并且代码意图需要移位而不是乘法/除法,否则不要这样做。

在通常的一天中,您可能会节省几个机器周期(或宽松,因为编译器知道如何优化),但成本却不值得-您花时间在次要细节上,而不是在实际工作上,维护代码变得更加困难,您的同事会诅咒您。

对于高负载计算,您可能需要这样做,其中每个保存的周期意味着几分钟的运行时间。但是,您应该一次优化一个位置,并且每次都要进行性能测试,以查看是否确实使速度更快或破坏了编译器逻辑。


1

据我所知,在某些机器中,乘法可能需要多达16到32个机器周期。所以,是的,这取决于机器的类型,位移操作符比乘法/除法快。

但是,某些机器确实具有其数学处理器,其中包含用于乘法/除法的特殊指令。


7
为那些机器编写编译器的人也可能阅读了Hackers Delight并进行了相应的优化。
Bo Persson

1

我同意德鲁·霍尔的明确回答。答案可能会使用一些其他注释。

对于绝大多数软件开发人员而言,处理器和编译器不再与此问题相关。我们大多数人都远远超出了8088和MS-DOS。它可能仅与那些仍在为嵌入式处理器开发的人相关。

在我的软件公司,所有数学都应使用Math(add / sub / mul / div)。在数据类型之间进行转换时,应使用Shift键。ushort字节为n >> 8,而不是 n / 256。


我也同意你的看法 我下意识地遵循了相同的准则,尽管我从来没有正式的要求。
德鲁·霍尔

0

在带符号整数和右移与除法的情况下,可以有所作为。对于负数,移位四舍五入为负无穷大,而除法四舍五入为零。当然,编译器会将除法更改为更便宜的方法,但是通常会将其更改为与除法具有相同舍入行为的方法,因为它要么无法证明变量不会为负数,要么不会关心。因此,如果您可以证明数字不会为负数,或者不关心数字的取整方式,则可以采用更有可能产生变化的方式进行优化。


或将号码投给unsigned
Lie Ryan

4
您确定换档行为是标准化的吗?我的印象是,负整数的右移是实现定义的。
Kerrek SB 2011年

1
尽管您可能应该提到依赖于任何特定行为的负向右移代码都应记录该要求,但是在自然产生正确值且除法运算符会产生浪费代码的情况下,向右移的好处是巨大的时间计算一个不想要的值,那么用户代码将不得不浪费额外的时间进行调整,以产生最初应该给出的移位。实际上,如果我有德鲁特,编译器可以选择尝试执行有符号除法,因为...
supercat

1
...知道操作数为正的代码,如果在除法之前将其强制转换为无符号(可能在之后转换为有符号),则可以改善优化,而知道操作数为负的代码通常应该以任何方式显式处理该情况(在这种情况下)人们不妨假设他们是积极的)。
2013年

0

Python测试针对相同的随机数执行了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中进行移位而不是乘以2的乘积/除法时,会略有改善(除法约为10%;乘法约为1%)。如果其非2的幂,则可能会出现相当大的减速。

同样,这些#号将根据您的处理器,您的编译器(或解释器-为简单起见在python中执行)而更改。

与其他所有人一样,请勿过早优化。编写易读的代码,如果速度不够快,则进行概要分析,然后尝试优化慢速部分。请记住,您的编译器在优化方面比您要好得多。


0

编译器无法进行某些优化,因为它们仅适用于减少的输入集。

下面是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;
}

0

我认为,在一种情况下,您想要乘以2或乘以除法,即使使用编译器将它们转换为MUL / DIV,使用位移位运算符也不会出错,因为某些处理器使用了微代码(实际上是处理器宏),因此对于这些情况,您将获得改进,尤其是当移位大于1时。或更明确地讲,如果CPU没有位移位运算符,则无论如何它都将是MUL / DIV,但是如果CPU具有位移位运算符,您可以避免微代码分支,这少了几条指令。

我现在正在编写一些代码,由于它正在密集的二叉树上工作,因此需要大量加倍/减半运算,而且我怀疑还有一个比加法更优化的运算-左(二乘幂) )加班。如果移位比您要添加的位数大,则可以用左移和xor代替,简单的例子是(i << 1)^ 1,它将加一。这当然不适用于向右移位(二分的幂),因为只有向左移位(小尾数)填充了零的间隙。

在我的代码中,这两个乘/除法和两个运算的幂的使用非常密集,并且由于公式已经很短了,因此可以消除的每条指令都可以带来可观的收益。如果处理器不支持这些移位运算符,则不会发生任何增益,但也不会造成任何损失。

另外,在我正在编写的算法中,它们直观地表示发生的运动,因此从某种意义上说它们实际上更加清晰。二叉树的左侧较大,右侧较小。除此之外,在我的代码中,奇数和偶数具有特殊的意义,树中的所有左手子代都是奇数,而所有右手子代和根都是偶数。在某些情况下,我还没有遇到过,但是,哦,实际上,我什至没有想到这一点,与x%2相比,x&1可能是更理想的操作。偶数的x&1将产生零,而奇数将产生1。

不仅仅只是奇数/偶数标识,如果x&3得到零,我知道4是我们数字的因数,x%7等于8。我知道这些情况的实用程序可能很有限,但是很高兴知道您可以避免模运算,而可以使用按位逻辑运算,因为按位运算几乎总是最快的,并且对于编译器来说模棱两可。

我几乎是在发明密集的二叉树领域,所以我希望人们可能不会理解此注释的价值,因为人们很少只希望仅使用2的幂或仅使用2的乘/除幂来进行因式分解。



0

如果在gcc编译器上比较x + x,x * 2和x << 1语法的输出,则在x86汇编中将得到相同的结果:https : //godbolt.org/z/JLpp0j

        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, eax
        pop     rbp
        ret

因此,您可以将gcc视为足够聪明的人,可以独立于键入的内容确定自己的最佳解决方案。


0

我也想看看我是否能击败众议院。对于任何数乘以任何数的乘法运算,这是更通用的按位运算。我制作的宏比普通*乘法快25%到两倍。正如其他人所说,如果它接近2的倍数或由2的几个倍数组成,则您可能会赢。如由(X << 4)+(X << 2)+(X << 1)+ X组成的X * 23会慢于由(X << 6)+ X组成的X * 65。

#include <stdio.h>
#include <time.h>

#define MULTIPLYINTBYMINUS(X,Y) (-((X >> 30) & 1)&(Y<<30))+(-((X >> 29) & 1)&(Y<<29))+(-((X >> 28) & 1)&(Y<<28))+(-((X >> 27) & 1)&(Y<<27))+(-((X >> 26) & 1)&(Y<<26))+(-((X >> 25) & 1)&(Y<<25))+(-((X >> 24) & 1)&(Y<<24))+(-((X >> 23) & 1)&(Y<<23))+(-((X >> 22) & 1)&(Y<<22))+(-((X >> 21) & 1)&(Y<<21))+(-((X >> 20) & 1)&(Y<<20))+(-((X >> 19) & 1)&(Y<<19))+(-((X >> 18) & 1)&(Y<<18))+(-((X >> 17) & 1)&(Y<<17))+(-((X >> 16) & 1)&(Y<<16))+(-((X >> 15) & 1)&(Y<<15))+(-((X >> 14) & 1)&(Y<<14))+(-((X >> 13) & 1)&(Y<<13))+(-((X >> 12) & 1)&(Y<<12))+(-((X >> 11) & 1)&(Y<<11))+(-((X >> 10) & 1)&(Y<<10))+(-((X >> 9) & 1)&(Y<<9))+(-((X >> 8) & 1)&(Y<<8))+(-((X >> 7) & 1)&(Y<<7))+(-((X >> 6) & 1)&(Y<<6))+(-((X >> 5) & 1)&(Y<<5))+(-((X >> 4) & 1)&(Y<<4))+(-((X >> 3) & 1)&(Y<<3))+(-((X >> 2) & 1)&(Y<<2))+(-((X >> 1) & 1)&(Y<<1))+(-((X >> 0) & 1)&(Y<<0))
#define MULTIPLYINTBYSHIFT(X,Y) (((((X >> 30) & 1)<<31)>>31)&(Y<<30))+(((((X >> 29) & 1)<<31)>>31)&(Y<<29))+(((((X >> 28) & 1)<<31)>>31)&(Y<<28))+(((((X >> 27) & 1)<<31)>>31)&(Y<<27))+(((((X >> 26) & 1)<<31)>>31)&(Y<<26))+(((((X >> 25) & 1)<<31)>>31)&(Y<<25))+(((((X >> 24) & 1)<<31)>>31)&(Y<<24))+(((((X >> 23) & 1)<<31)>>31)&(Y<<23))+(((((X >> 22) & 1)<<31)>>31)&(Y<<22))+(((((X >> 21) & 1)<<31)>>31)&(Y<<21))+(((((X >> 20) & 1)<<31)>>31)&(Y<<20))+(((((X >> 19) & 1)<<31)>>31)&(Y<<19))+(((((X >> 18) & 1)<<31)>>31)&(Y<<18))+(((((X >> 17) & 1)<<31)>>31)&(Y<<17))+(((((X >> 16) & 1)<<31)>>31)&(Y<<16))+(((((X >> 15) & 1)<<31)>>31)&(Y<<15))+(((((X >> 14) & 1)<<31)>>31)&(Y<<14))+(((((X >> 13) & 1)<<31)>>31)&(Y<<13))+(((((X >> 12) & 1)<<31)>>31)&(Y<<12))+(((((X >> 11) & 1)<<31)>>31)&(Y<<11))+(((((X >> 10) & 1)<<31)>>31)&(Y<<10))+(((((X >> 9) & 1)<<31)>>31)&(Y<<9))+(((((X >> 8) & 1)<<31)>>31)&(Y<<8))+(((((X >> 7) & 1)<<31)>>31)&(Y<<7))+(((((X >> 6) & 1)<<31)>>31)&(Y<<6))+(((((X >> 5) & 1)<<31)>>31)&(Y<<5))+(((((X >> 4) & 1)<<31)>>31)&(Y<<4))+(((((X >> 3) & 1)<<31)>>31)&(Y<<3))+(((((X >> 2) & 1)<<31)>>31)&(Y<<2))+(((((X >> 1) & 1)<<31)>>31)&(Y<<1))+(((((X >> 0) & 1)<<31)>>31)&(Y<<0))
int main()
{
    int randomnumber=23;
    int randomnumber2=23;
    int checknum=23;
    clock_t start, diff;
    srand(time(0));
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYMINUS(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    int msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYMINUS Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum=MULTIPLYINTBYSHIFT(randomnumber,randomnumber2);
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("MULTIPLYINTBYSHIFT Time %d milliseconds", msec);
    start = clock();
    for(int i=0;i<1000000;i++)
    {
        randomnumber = rand() % 10000;
        randomnumber2 = rand() % 10000;
        checknum= randomnumber*randomnumber2;
        if (checknum!=randomnumber*randomnumber2)
        {
            printf("s %i and %i and %i",checknum,randomnumber,randomnumber2);
        }
    }
    diff = clock() - start;
    msec = diff * 1000 / CLOCKS_PER_SEC;
    printf("normal * Time %d milliseconds", msec);
    return 0;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.