以有效的方式乘以7

时间:2011-11-03 07:14:06

标签: c

我最近遇到了以下面试问题:

  

如何以高效优化的方式将数字乘以7?

我知道我可以乘以8(或左移三位)然后减去原始值:

num = (num << 3) - num;

但还有其他解决方案。

5 个答案:

答案 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,对这样一个采访问题的完整答案还应该提到以下内容:

  1. 这种优化通常最好留给编译器/编译器编写器。 (的确,从另一个答案来看,似乎gcc 优化了这种情况。)

  2. 你(作为程序员)应该只花时间在这上面1)有一个真实的(可测量的)性能问题,2)你的探查器告诉你,你正在看的语句是性能关键的。


  3. 在他的回答中。奥拉夫写道:

      

    “当他告诉你应该(或不应该)做什么时,我不同意斯蒂芬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.如果每个人都这样做,那么软件行业就没有创新。

所以:去吧!