C ++优化之谜

时间:2015-03-06 16:40:50

标签: c++ optimization bit-shift multiplication

采取以下两个片段:

int main()
{
    unsigned long int start = utime();

    __int128_t n = 128;

    for(__int128_t i=1; i<1000000000; i++)
        n = (n * i);

    unsigned long int end = utime();

    cout<<(unsigned long int) n<<endl;

    cout<<end - start<<endl;
}

int main()
{
    unsigned long int start = utime();

    __int128_t n = 128;

    for(__int128_t i=1; i<1000000000; i++)
        n = (n * i) >> 2;

    unsigned long int end = utime();

    cout<<(unsigned long int) n<<endl;

    cout<<end - start<<endl;
}

我在C ++中对128位整数进行基准测试。当执行第一个(只是乘法)时,一切都在大约运行。 0.95秒当我还添加位移操作(第二个片段)时,执行时间会增加到惊人的2.49秒。

这怎么可能?我认为位移是处理器最轻的操作之一。怎么这么简单的操作会导致如此多的开销呢?我正在编译O3标志激活。

有什么想法吗?

2 个答案:

答案 0 :(得分:6)

在我的4770K上对两者进行基准测试(使用GCC 4.7.3在-O2上生成的汇编)之后,我发现第一个每次迭代需要5个周期,第二个每次迭代需要9个周期。为什么这么大的差异?

事实证明,这是吞吐量和延迟之间的相互作用。主要杀手是shrd,需要3个周期才能进入关键路径。这是它的一张图片(我忽略了i的链条,因为它更快,并且有足够的备用吞吐量可以提前运行,它不会干扰):

dependency chain

这里的边缘是依赖关系,而不是数据流。

仅基于此链中的延迟,预期时间为每次迭代8个周期。但事实并非如此。这里的问题是,要发生8个周期,mul2imul3必须并行执行,整数乘法只有1个/周期的吞吐量。所以它(任何一个)必须等待一个循环,并按周期保持链。我通过将imul更改为add来验证这一点,这将每次迭代的时间缩短为8个周期。将其他imul更改为add没有效果,正如基于此解释所预测的那样(它不依赖shrd,因此可以提前安排,而不会干扰其他乘法)。

这些确切的细节仅适用于Haswell。

我使用的代码是:

section .text

global cmp1
proc_frame cmp1
[endprolog]
    mov r8, rsi
    mov r9, rdi
    mov esi, 1
    xor edi, edi
    mov eax, 128
    xor edx, edx
.L2:
    mov rcx, rdx
    mov rdx, rdi
    imul    rdx, rax
    imul    rcx, rsi
    add rcx, rdx
    mul rsi
    add rdx, rcx
    add rsi, 1
    mov rcx, rsi
    adc rdi, 0
    xor rcx, 10000000
    or  rcx, rdi
    jne .L2
    mov rdi, r9
    mov rsi, r8
    ret
endproc_frame

global cmp2
proc_frame cmp2
[endprolog]
    mov r8, rsi
    mov r9, rdi
    mov esi, 1
    xor edi, edi
    mov eax, 128
    xor edx, edx
.L3:
    mov rcx, rdi
    imul    rcx, rax
    imul    rdx, rsi
    add rcx, rdx
    mul rsi
    add rdx, rcx
    shrd    rax, rdx, 2
    sar rdx, 2
    add rsi, 1
    mov rcx, rsi
    adc rdi, 0
    xor rcx, 10000000
    or  rcx, rdi
    jne .L3
    mov rdi, r9
    mov rsi, r8
    ret
endproc_frame

答案 1 :(得分:1)

除非您的处理器能够支持本机128位操作,否则必须对操作进行软件编码以使用下一个最佳选项。

您的128位操作使用与8位处理器使用16位操作时相同的方案,这需要时间。

例如,使用64位寄存器的128位右移一位需要:
将最高有效寄存器右移为进位。 Carry标志将包含被移出的位 将最低有效寄存器右移,带进位。这些位将向右移位,进位标志位移到最高位位置。

如果不支持本机128位操作,则代码将占用相同64位操作的两倍的操作;有时候更多(比如乘法)。这就是你看到这种糟糕表现的原因。

我强烈建议在非常必要的地方使用128位。