我最近遇到了以下面试问题:
如何以高效优化的方式将数字乘以7?
我知道我可以乘以8(或左移三位)然后减去原始值:
num = (num << 3) - num;
但还有其他解决方案。
答案 0 :(得分:16)
对于有限范围,您可以使用查找表:
static unsigned int mult7[] = {0, 7, 14, 21, ...};
unsigned int three = 3;
unsigned int twenty_one = mult7[three];
这可能声音愚蠢(并且它可能适用于这种特定情况),但对于计算真实成本的事情来说,它通常很方便。我不确定将七个计数乘以其中一个案例。
首先,将x
乘以7(或向左移动x
三位,然后减去x
)是一项完全可以在CPU内部完成的操作。使用表查找,您可能会看到乘以4(向左移两位),然后是添加以获取正确的地址,但是您必须访问内存才能进行实际查找 - 即使使用缓存和所有其他当前CPU有能力的奇妙技巧,这可能会减慢速度。
您的编译器也很有可能已经知道有关如何快速乘法的所有技巧。如果你的七是一个常数(或const int
或等价的),编译器可能已经选择了最快的方式,编译器编写者很有可能比凡人更了解这类东西:-) (a)中
但是对于计算成本 相对较高的情况,计算一次值并将它们作为查找表嵌入代码中是标准优化策略之一(空间权衡时间)。
(a)检查以下代码:
#include <stdio.h>
static int mult7 (int num) {
return num * 7;
}
int main (int argc, char *argv[]) {
printf ("%d\n", mult7 (atoi (argv[1])));
return 0;
}
通过gcc
的正常编译,mult7
出现,因为左移3并减去技巧:
_mult7:
pushl %ebp ; stack frame setup.
movl %esp, %ebp
movl 8(%ebp), %edx ; get value to edx
movl %edx, %eax ; and eax.
sall $3, %eax ; eax <- eax * 8.
subl %edx, %eax ; eax <- eax - edx.
popl %ebp ; stack frame teardown and return.
ret
在-O3
(我称之为疯狂的优化级别),整个内容被内联到main
中:
call _atoi
movl $LC0, (%esp)
leal 0(,%eax,8), %edx ; these two are the relevant instructions.
subl %eax, %edx
movl %edx, 4(%esp)
call _printf
请注意,此内联操作只能由于函数的静态特性 - 如果链接器可见,则必须将其维护为单独的函数,以防另一个目标文件需要调用它。
如果你取消static
,它确实保持非内联所有堆栈帧设置和拆解,但它至少仍然使用下面提到的(可能)更有效的技巧。如果您使用gcc
,可以删除-fomit-frame-pointer
中的堆栈帧代码,前提是这不会对代码产生负面影响,但这会开始深入研究黑暗面a小: - )
这个技巧是使用LEA
指令将edx
设置为eax * 8
,然后从中减去eax
。与正常优化时的sall/subl
理论相同,只是略有不同的机制。
底线,信任您的编译器。如果您想将num
乘以7,请使用以下命令:
num *= 7;
无论您从这种尝试过的微优化中获得什么改进,您都可以通过查看宏观级别(算法和数据结构选择等)获得更好的改进。
答案 1 :(得分:15)
以有效的方式获得7的倍数:
7
7是7的倍数。这回答了你问的问题,但我确定它没有回答你的问题。
编辑:以上是基于问题的原始标题,我刚刚更正了。
要有效地将乘以 7,只需编写,例如:
x * 7
并使用优化调用您的编译器。让编译器确定单个MUL指令或(x<<3) - x
之类的东西对于当前机器是否更有效。
这里还有一个隐含的问题:面试官在寻找什么答案?我希望“让编译器担心它”将是一个可以接受的答案。 (x<<3) - x
可能是最明显的微优化 - 但如果x<<3
溢出,它会产生错误的答案,并且取决于系统,它可能比MUL指令慢。
(如果我是面试官,对问题的解释和理解比任何具体答案都要好得多。)
修改强>
进一步思考,如果您比编译器更了解x
的可能值,那么此处讨论的微优化类型可能非常有用。如果您知道,由于程序逻辑的性质,x
将始终在0..10范围内,那么查找表可能比乘法运算更快。或者,如果您知道x
在99%的时间内处于该范围内,那么回溯到实际乘法的查找表可能就是这样。
但是,如果编译器对您的程序流程的分析不允许它证明 x
总是在该范围内,那么它就无法执行这种优化。< / p>
但这种情况非常罕见。当您的代码在x
可以为11的新环境中运行时(可能它在具有更大显示的设备上运行), kaboom 。而且性能改善很可能首先并不显着。
有时微观优化是合适的,但开发和测试时间会有相当大的成本。只有当实际的测量值表明它是值得的时才这样做。
答案 2 :(得分:6)
我这样做的方式就像
num = (num << 3) - num;
即。 2 ^ 3 = 8,然后减去乘以的数字得到7的倍数。
我刚用gcc编译了以下代码:
int mul(int num)
{
return num * 7;
}
这是一个gdb转储,它编译成:
Dump of assembler code for function mul:
0x00000000004004c4 <+0>: push rbp
0x00000000004004c5 <+1>: mov rbp,rsp
0x00000000004004c8 <+4>: mov DWORD PTR [rbp-0x4],0xa
0x00000000004004cf <+11>: mov edx,DWORD PTR [rbp-0x4]
0x00000000004004d2 <+14>: mov eax,edx
0x00000000004004d4 <+16>: shl eax,0x3
0x00000000004004d7 <+19>: sub eax,edx
0x00000000004004d9 <+21>: mov DWORD PTR [rbp-0x4],eax
0x00000000004004dc <+24>: pop rbp
0x00000000004004dd <+25>: ret
End of assembler dump.
所以看起来我的机器左移3次然后减去乘以的数字是gcc认为可能是最佳的。
编辑:结果优化级别至少为1(-O1
),gcc使用lea
技巧:
Dump of assembler code for function mul:
0x00000000004004e0 <+0>: lea eax,[rdi*8+0x0]
0x00000000004004e7 <+7>: sub eax,edi
0x00000000004004e9 <+9>: ret
End of assembler dump.
答案 3 :(得分:4)
实际上,乘以7 的最有效方法可能是使用乘法运算符。它取决于目标平台上相应指令的相对速度。
IMO,对这样一个采访问题的完整答案还应该提到以下内容:这种优化通常最好留给编译器/编译器编写器。 (的确,从另一个答案来看,似乎gcc 优化了这种情况。)
你(作为程序员)应该只花时间在这上面1)有一个真实的(可测量的)性能问题,2)你的探查器告诉你,你正在看的语句是性能关键的。
在他的回答中。奥拉夫写道:
“当他告诉你应该(或不应该)做什么时,我不同意斯蒂芬C.如果每个人都这样做,那么软件行业就没有创新。”
奥拉夫似乎不相信以下一项或多项:
确实如果每个人总是对他们收到的建议采取行动,那么创新就会减少。但另一方面,手头的工作通常并不需要太多创新。 (它很少需要手工优化......)
此外,如果忽视建议(最佳实践)是一种美德,那么75%的软件工程师会花时间维护“goto spaghetti”,汇编代码或1990年代设计方法论的结果。
所以你至少应该理解这些建议,并权衡忽视它的潜在后果。就像老板对他的“创新”(或者更确切地说,浪费时间)的模糊观点一样。
答案 4 :(得分:2)
正如Stephen C所说,“乘以7的最有效方法可能是乘法运算符。”
在本文中 - Instruction latencies and throughput for AMD and Intel x86 processors - 斯德哥尔摩皇家理工学院的TorbjörnGranlund表明,无符号乘法需要在K10架构上的32/64位模式下3/5个时钟周期和4/4开启珊迪大桥。如果需要背对背地执行多次乘法,K10可以在32/64位模式下每隔一个时钟周期发出一次乘法。这意味着它可以在64位同时(3/1)和2.5(5/2)的不同阶段中进行三次乘法运算。 Sandy Bridge在32/64中每隔一个/每个时钟周期发出一个。这意味着同时有两(4/2)或四(4/1)条指令。
就个人而言,我相信你很难通过多班制顺序来改善这一点。当他告诉你应该(或不应该)做什么时,我不同意斯蒂芬C.如果每个人都这样做,那么软件行业就没有创新。
所以:去吧!