我认为对于提问者来说,做出更具差异性的答案会更有帮助,因为我在问题以及某些答案或评论中看到了一些未经审查的假设。
移位和乘法所产生的相对运行时间与C无关。当我说C时,我并不是说特定实现的实例,例如那个或那个版本的GCC,而是语言。我的意思不是荒谬,而是要使用一个极端的例子进行说明:您可以实现一个完全符合标准的C编译器,并且乘法需要一个小时,而移位则需要毫秒(或者相反)。我不了解C或C ++中的任何此类性能限制。
您可能不关心论证中的这种技术性。您的意图可能只是测试移位与乘法的相对性能,然后选择C,因为C通常被认为是一种低级编程语言,因此人们可能希望其源代码能够更直接地转换为相应的指令。这样的问题非常普遍,我认为一个很好的答案应该指出,即使在C语言中,您的源代码也不能像您在给定实例中所想象的那样直接转换为指令。我在下面给出了一些可能的编译结果。
在这里,提出了质疑在现实世界软件中替代这种等效性的有用性的评论。您可以在对问题的评论中看到一些评论,例如Eric Lippert的评论。这与您通常会从经验丰富的工程师那里得到的响应相吻合。如果您将生产代码中的二进制移位用作乘法和除法的总括方式,人们很可能会对您的代码感到畏缩,并会产生某种程度的情感反应(“为了天堂,我听说过有关JavaScript的荒谬说法”)。除非对新手程序员有更好的了解,否则可能对新手程序员没有意义。
这些原因主要是这种优化的可读性和无效性的综合体现,您可能已经在比较它们的相对性能时发现了它们。但是,我不认为如果将shift替换为乘法是这种优化的唯一示例,那么人们不会有那么强烈的反应。像您这样的问题经常以各种形式和背景出现。我认为,至少在某些时候,更高级的工程师实际上会如此强烈地做出反应,这是当人们在整个代码库中自由地采用此类微优化时,有可能造成更大范围的危害。如果您在像Microsoft这样的大型代码公司工作,您将花费大量时间阅读其他工程师的源代码,或尝试在其中找到某些代码。甚至可能是您自己的代码,几年后,尤其是在某些最不适当的时候,您将试图弄清这些代码,例如,当您接到传呼机上的电话后必须修复生产中断时周五晚上值班,准备和朋友一起度过一个快乐的夜晚……如果您花大量时间阅读代码,您将欣赏它尽可能可读。想像一下您最喜欢的小说,但是出版商决定发行使用abbrv的新版本。所有ovr th plc bcs和svs spc。这类似于其他工程师可能会对您的代码产生的反应,如果您对它们进行了这样的优化。正如其他答案所指出的那样,最好清楚地说明您的意思,
即使在这些环境中,您也可能会发现自己正在解决一个面试问题,希望您知道这一点或其他等效问题。知道它们并不坏,一个好的工程师会意识到二进制移位的算术效果。请注意,我并不是说这会造就一个好的工程师,但是我认为一个好的工程师会知道。特别是,您可能仍会找到一些经理,通常在面试循环快要结束时,他会大笑您,以期希望在编码问题中向您透露这个聪明的工程“技巧”,并证明他/她也曾经是或曾经是精明的工程师之一,而不是“仅仅是”一位经理。在这种情况下,只要让自己印象深刻并感谢他/她的启发性采访。
为什么看不到C的速度差异?最可能的答案是,它们都导致了相同的汇编代码:
int shift(int i) { return i << 2; }
int multiply(int i) { return i * 2; }
都可以编译成
shift(int):
lea eax, [0+rdi*4]
ret
在没有优化的GCC上,即使用标志“ -O0”,您可能会得到以下信息:
shift(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
sal eax, 2
pop rbp
ret
multiply(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov eax, DWORD PTR [rbp-4]
add eax, eax
pop rbp
ret
如您所见,将“ -O0”传递给GCC并不意味着它不会产生什么样的代码。特别要注意的是,即使在这种情况下,编译器也避免了使用乘法指令。您可以重复相同的实验,但要移位其他数字,甚至要乘以不是2的幂的数字。在您的平台上,您可能会看到移位和加法的组合,但没有乘法。如果乘法和移位确实具有相同的代价,那么在所有这些情况下,编译器显然避免使用乘法似乎有点巧合,不是吗?但是我并不是要提供假设作为证据,所以让我们继续前进。
您可以使用上面的代码重新运行测试,看看现在是否注意到速度差异。即便如此,您仍未测试移位与乘法的关系,正如您通过没有乘法所看到的那样,而是在特定实例中由GCC针对移位和乘法的C运算使用特定标志集生成的代码。因此,在另一个测试中,您可以手动编辑汇编代码,而在代码中使用“ imul”指令执行“ multiply”方法。
如果您想击败一些编译器的聪明才智,则可以定义一个更通用的shift和乘法方法,并最终得到以下结果:
int shift(int i, int j) { return i << j; }
int multiply(int i, int j) { return i * j; }
这可能会产生以下汇编代码:
shift(int, int):
mov eax, edi
mov ecx, esi
sal eax, cl
ret
multiply(int, int):
mov eax, edi
imul eax, esi
ret
即使在GCC 4.9的最高优化级别上,我们最终也终于有了汇编指令中最初开始进行测试时可能期望的表达式。我认为它本身可以成为性能优化中的重要一课。我们可以看到在编译器能够应用的智能方面,用变量代替代码中的具体常量所产生的差异。像移位乘法替换这样的微优化是编译器通常可以很容易地自己完成的一些非常低级的优化。其他对性能影响更大的优化则需要了解代码意图这通常是编译器无法访问的,或者只能通过某种启发式方法来猜测。那就是您作为软件工程师加入的地方,并且通常不涉及使用移位代替乘法。它涉及一些因素,例如避免对产生I / O并可能阻塞进程的服务进行冗余调用。如果您进入硬盘或进入远程数据库以获取一些其他数据,这些数据可能是从内存中已有的数据中获取的,那么等待所花费的时间将超过一百万条指令的执行量。现在,我认为我们与您的原始问题相去甚远,但我想将其指出给发问者,特别是如果我们假设有人刚刚开始掌握代码的翻译和执行,
那么,哪一个会更快?我认为这是您选择实际测试性能差异的一种好方法。通常,某些代码更改的运行时性能很容易使您感到惊讶。现代处理器采用了许多技术,软件之间的交互也可能很复杂。即使您应该在一种情况下进行某些更改而获得有益的绩效结果,但我认为得出这样的变化总是可以带来绩效收益的结论还是很危险的。我认为一次进行这样的测试很危险,说“好吧,现在我知道哪个更快!” 然后不加选择地将相同的优化应用于生产代码,而无需重复进行测量。
那么,如果移位比乘法快怎么办?当然有迹象表明这是正确的。正如您在上面看到的那样,GCC似乎认为(即使没有优化)避免直接乘法而采用其他指令也是一个好主意。在英特尔64和IA-32架构优化参考手册会给你的CPU指令的相对成本的想法。http://www.agner.org/optimize/instruction_tables.pdf是另一个更注重指令延迟和吞吐量的资源。。请注意,它们不是绝对运行时的良好预测器,而是指令相对于彼此的性能。在一个紧密的循环中,正如您的测试在模拟一样,“吞吐量”的度量标准应该是最相关的。它是执行给定指令时通常会占用执行单元的周期数。
那么,如果移位没有比乘法快呢?如前所述,现代架构可能非常复杂,诸如分支预测,缓存,流水线和并行执行单元之类的事情可能使得有时很难预测两个逻辑上等效的代码的相对性能。我真的很想强调这一点,因为在这里我对大多数此类问题的答案不满意,并且人们阵营直截了当地说,移位快于乘法是不正确的(不再)。
不,据我所知,我们不是在1970年代发明的,也没有在任何时候突然消除乘法器和移位器的成本差异的时候发明的。在许多架构上,就逻辑门而言,当然就逻辑运算而言,通用乘法仍然比采用桶形移位器的移位复杂得多。如何将其转换为台式计算机上的整体运行时可能有点不清楚。我不确定如何在特定处理器中实现它们,但是这里是对乘法的解释:整数乘法的速度真的与现代CPU上的加法速度相同吗?
这里是桶形移位器的解释。我在上一段中引用的文档通过代理CPU指令对操作的相对成本提供了另一种观点。英特尔的工程师经常会遇到类似的问题:英特尔开发人员专区论坛为核心2双核处理器中的整数乘法和加法提供时钟周期
是的,在大多数现实场景中,几乎可以肯定在JavaScript中,为性能而利用这种等效性可能是徒劳的。但是,即使我们强制使用乘法指令,然后看到运行时也没有差异,这更多是由于我们使用的成本指标的性质(准确地说),而不是因为没有成本差异。端到端运行时是一个指标,如果这是我们关心的唯一指标,那么一切都很好。但这并不意味着乘法和移位之间的所有成本差异都已消失。而且我认为以暗示或其他方式将其传达给提问者肯定不是一个好主意,因为提问者显然才刚刚开始了解现代代码的运行时间和成本所涉及的因素。工程总是要权衡取舍。对现代处理器进行了哪些折衷以显示出我们最终用户看到的执行时间的询问和解释,可能会得出更加不同的答案。而且我认为,如果我们希望看到更少的工程师检入经过微优化的代码,从而消除可读性,则需要一个比“根本不再是事实”更具差异性的答案,因为它需要对这种“优化”的性质有更全面的了解,发现它的各种变身,而不是简单地将某些特定实例称为过时的。