哪个更快,乘法或减法?

时间:2012-11-22 13:35:37

标签: c performance optimization

我目前正在做一个大学项目,该项目在很大程度上取决于我的解决方案的速度和效率。我对代码所做的微小改动产生了巨大的影响,因为我写的特定功能被称为成千上万次。

我现在已经编写了我项目的主要功能,目前我正在优化我可能做的所有事情。我正在质疑的代码的一个特定部分是这样的:

array[i] *= -1;

我正在考虑优化:

array[i] = 0 - array[i];

更改此代码会不会影响速度?减法运算比乘法运算更快吗?或者这类问题已成为过去?

6 个答案:

答案 0 :(得分:18)

忽略你应该使用它的事实:

array[i] = -array[i];

因为IMO直接表明了意图,所以它更加清晰,让我们检查编译器对此程序的作用(x86-64上的GCC 4.7.2):

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

int main(void)
{
    time_t t = time(NULL);
    t *= -1;
    return 0;
}
gcc -S mult.c -o 1.s

为此:

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

int main(void)
{
    time_t t = time(NULL);
    t = 0 - t;
    return 0;
}
gcc -S sub.c -o 2.s

现在比较两个汇编输出:

diff 1.s 2.s

什么都没打印出来。编译器为两个版本生成了相同的确切代码。所以答案是:你使用什么并不重要。编译器会选择最快的。这是一个非常容易的优化(如果你甚至可以称之为优化),所以我们可以假设几乎每个编译器都会选择以最快的方式为给定的CPU架构做这件事。

作为参考,生成的代码是:

int main()
{
    time_t t = time(NULL);
       mov    edi,0x0
       call   12 
       mov    QWORD PTR [rbp-0x8],rax

    t *= -1;
       neg    QWORD PTR [rbp-0x8]

    t = 0 - t;
       neg    QWORD PTR [rbp-0x8]

    return 0;
       mov    eax,0x0
}

在这两种情况下,它都使用NEG来否定该值。 t *= -1t = 0 - t都生成:

neg QWORD PTR [rbp-0x8]

答案 1 :(得分:13)

只有一种明智的优化方法,那就是测量应用程序的性能。一个好的剖析器可以告诉你很多,但只是计划程序的执行时间和各种修改也可以提供很大的帮助。我会首先使用探查器,但要找出瓶颈所在。

至于你的具体问题,正如其他人所指出的那样,这将高度依赖于架构。

答案 2 :(得分:5)

编译器非常聪明,可以将其转换为高效的操作。例如

C源

void f()
{
    int a = 7, b = 7;
    a *= -1;
    b = -b;
}

使用gcc -S a.c

    .file    "a.c"
    .text
    .globl    f
    .type    f, @function
f:
.LFB0:
    .cfi_startproc
    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $7, -8(%rbp) ; assign 7
    movl    $7, -4(%rbp) ; assign 7
    negl    -8(%rbp)     ; negate variable
    negl    -4(%rbp)     ; negate variable
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size    f, .-f
    .ident    "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
    .section    .note.GNU-stack,"",@progbits

这是在使用Ubuntu 12.04和gcc 4.6.3的PC上。您的架构可能会有所不同。

答案 3 :(得分:3)

几乎每台设备的乘法速度都会变慢。这是一个更复杂的操作。

但是你的编译器可能足够聪明,可以自己进行转换。 并且在现代CPU上,操作可以以这样的方式重叠:指令的额外时间不会导致执行时间增加,因为它与其他工作重叠。 而且,除非你做了数百万次,否则如果没有大量的努力就会无法衡量,这种差异很小。

通常首先编写清除代码,如果以后需要优化代码。 如果你想要一个变量的负值写“-value”而不是“-1 * value”,因为它更准确地反映了你的意图,而不仅仅是一种计算方法。

答案 4 :(得分:2)

这是gcc 4.6.1对-O:

的作用
double a1(double b) { return -b; }       // xors sign bit with constant, 2 instr
                                         // movsd   .LC0(%rip), %xmm1   (instr 1)
                                         // xorpd   %xmm1, %xmm0        (instr 2)
                                         // ret                     (not counted)
double a2(double b) { return -1.0*b; }   // xors sign bit with constant, 2 instr
                                         // same code as above
double a3(double b) { return 0.0-b; }    // substract b from 0,  3 instructions
                                         // xorpd   %xmm1, %xmm1
                                         // subsd   %xmm0, %xmm1
                                         // movapd  %xmm1, %xmm0    (+ret)
int a4(int a){return -a;}                // neg rax   (+ret)  1 instruction
int a5(int a){return a*(-1);}            // neg rax
int a6(int a){return 0-a;}               // neg rax
double a7(double b) { return 0-b;}       // same as a3() -- 3 instructions

因此,建议的优化使得此编译器更糟糕(取决于数组类型)。

然后关于乘法的问题比加法慢。根据经验,如果乘法与加法一样快,我们讨论的是DSP架构或DSP扩展:Texas C64,Arm NEON,Arm VFP,MMX,SSE。对于许多浮点扩展,从Pentium开始也是如此,其中FADD和FMUL都具有3个周期的延迟和每个周期1个指令的吞吐量。 ARM整数核也在1个周期内执行乘法运算。

答案 5 :(得分:1)

好的,试着清理我的烂摊子,把我的愚蠢变成有用的知识,不仅是为了我自己,也是为了其他人。

主要结论和总结:

这种优化是由编译器自动完成的,在这种情况下,两种方法都被编译为x86上的一条ASM指令。(参见上面的帖子)不要让编译器的工作比它必须是,只要做逻辑所暗示的。

几个答案显示,这两种方式都编译为完全相同的指令。

<强> TL; DR

为了解决我在这个问题上所犯的错误,我决定投入一些努力来为自己清除这一点 - 对于那些遭受精神错乱的人,就像我在回答这个问题时所做的那样奇怪的错误答案...... / p>

否定数字取决于体系结构以及数据的表示方式。

符号和幅度表示

不知怎的,我假设使用了这个实现 - 事实并非如此。这表示数字为一个符号位,所有其余值表示该值。因此,它可以表示从-2 n-1 -1到2 n-1 -1的数字,并且也具有负零值。在这种表示中,翻转符号位就足够了:

input ^ -0; // as the negative zero has all bits but the MSB as zero

一个补码表示

A one's complement integer representation表示负数作为正表示的按位否定。然而,这并没有真正使用,从8080开始,使用了两个赞美。这种表示的一个奇怪后果是负零,这可能会带来很多麻烦。此外,所表示的数字范围从-2 n-1 -1到2 n-1 -1,其中n是存储数字的位数。

在这种情况下,否定数字的最快“手动”方式是翻转代表符号的所有位:

input ^ 0xFFFFFFFF; //assuming 32 bits architecture

input ^ -0; //as negative zero is a "full one" binary value

二进制补码

更广泛(总是?)使用的表示是two's complement system。它表示从-2 n-1 到2 n-1 -1的数字,并且只有一个零值。它表示正范围作为它们的普通二进制表示。然而,添加1到2 n-1 -1(由除了MSB之外的所有位中的1表示)将导致-2 n-1 (表示为MSB为1,其他所有位为零。

手动否定一个二进制补码需要否定所有位并加1:

(input ^ -1) + 1 //as -1 is represented by all bits as 1

然而因为负值的范围比正值的范围更广,所以最负数在此表示中没有正对应,这在交易时必须计算在内。这些数字!反转最负值将导致其本身,就像它发生零(为简单起见,8位)

most negative number: -128, represented as 10000000
inverting all bits: 01111111
adding one: 10000000 -> -128 again

但请大家*记住:过早优化是万恶之源! (对于任何资源丰富的架构,优化器都是过去的事情)

*:OP已经通过了这个,所以这适用于所有其他人,比如我。

(注意自我:过早地(过早地)愚蠢是所有(正当的)downvotes的根源。)