当模量为特殊形式时,通过模块化添加进行性能优化

时间:2012-08-30 14:14:57

标签: c++ c performance math assembly

我正在编写一个执行数百万模块化添加的程序。为了提高效率,我开始考虑如何使用机器级指令来实现模块化添加。

设w为机器的字大小(通常为32或64位)。如果将模数设为2 ^ w,则可以非常快速地执行模块化添加:只需添加加数就足够了,并丢弃进位。

我使用以下C代码测试了我的想法:

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

int main()
{
    unsigned int x, y, z, i;
    clock_t t1, t2;

    x = y = 0x90000000;

    t1 = clock();

    for(i = 0; i <20000000 ; i++)
        z = (x + y) % 0x100000000ULL;

    t2 = clock();

    printf("%x\n", z);
    printf("%u\n", (int)(t2-t1));

    return 0;
}

使用GCC使用以下选项进行编译(我使用-O0来阻止GCC展开循环):

-S -masm=intel -O0

生成的汇编代码的相关部分是:

    mov DWORD PTR [esp+36], -1879048192
    mov eax, DWORD PTR [esp+36]
    mov DWORD PTR [esp+32], eax
    call    _clock
    mov DWORD PTR [esp+28], eax
    mov DWORD PTR [esp+40], 0
    jmp L2
L3:
    mov eax, DWORD PTR [esp+36]
    mov edx, DWORD PTR [esp+32]
    add eax, edx
    mov DWORD PTR [esp+44], eax
    inc DWORD PTR [esp+40]
L2:
    cmp DWORD PTR [esp+40], 19999999
    jbe L3
    call    _clock

很明显,不涉及任何模块化算法。

现在,如果我们将C代码的模块化添加行更改为:

z = (x + y) % 0x0F0000000ULL;

汇编代码更改为(仅显示相关部分):

    mov DWORD PTR [esp+36], -1879048192
    mov eax, DWORD PTR [esp+36]
    mov DWORD PTR [esp+32], eax
    call    _clock
    mov DWORD PTR [esp+28], eax
    mov DWORD PTR [esp+40], 0
    jmp L2
L3:
    mov eax, DWORD PTR [esp+36]
    mov edx, DWORD PTR [esp+32]
    add edx, eax
    cmp edx, -268435456
    setae   al
    movzx   eax, al
    mov DWORD PTR [esp+44], eax
    mov ecx, DWORD PTR [esp+44]
    mov eax, 0
    sub eax, ecx
    sal eax, 28
    mov ecx, edx
    sub ecx, eax
    mov eax, ecx
    mov DWORD PTR [esp+44], eax
    inc DWORD PTR [esp+40]
L2:
    cmp DWORD PTR [esp+40], 19999999
    jbe L3
    call    _clock

显然,在对_clock的两次调用之间添加了大量指令。

考虑到装配说明的数量增加, 我期望通过适当选择模量至少100%来获得性能增益。但是,运行输出时,我注意到速度仅提高了10%。我怀疑操作系统正在使用多核CPU并行运行代码,但即使将进程的CPU亲和性设置为1也没有任何改变。

你可以请我解释一下吗?

编辑:使用VC ++ 2010运行示例,我得到了我的预期:第二个代码比第一个代码慢大约12倍!

2 个答案:

答案 0 :(得分:2)

Art nailed it

对于2次幂模数,用-O0-O3生成的计算代码是相同的,不同之处在于循环控制代码,运行时间因子而异3。

对于其他模数,循环控制代码的差异是相同的,但计算的代码并不完全相同(优化的代码看起来应该更快一点,但我不知道关于装配或我的处理器,以确保)。未经优化和优化的代码之间的运行时间差异约为2倍。

两个模数的运行时间与未优化的代码相似。与没有任何模数的运行时间差不多。与通过从生成的程序集中删除计算而获得的可执行文件的运行时间大致相同。

因此,运行时间完全由循环控制代码

决定
    mov DWORD PTR [esp+40], 0
    jmp L2
L3:
    # snip
    inc DWORD PTR [esp+40]
L2:
    cmp DWORD PTR [esp+40], 19999999
    jbe L3

启用优化后,循环计数器保存在寄存器(此处)并递减,然后跳转指令为jne。该循环控制速度快得多,模数计算现在占用了大部分运行时间,从生成的程序集中删除计算现在将运行时间减少了3倍。 2。

因此,当使用-O0编译时,您不是测量计算的速度,而是测量循环控制代码的速度,因此差异很小。通过优化,您可以同时测量计算和循环控制,并且计算速度的差异可以清楚地显示出来。

答案 1 :(得分:1)

两者之间的区别归结为在逻辑指令中可以轻易转换2的幂除法这一事实。

a/n其中n是2的幂等于a >> log2 n 对于模数,它是一样的 a mod n可以呈现a & (n-1)

但在你的情况下,它甚至比这更进一步: 你的值0x100000000ULL是2 ^ 32。这意味着任何无符号的32位变量将自动为模2 ^ 32值。 编译器足够聪明,可以删除操作,因为它是对32位变量的不必要操作。 ULL说明符 并没有改变这一事实。

对于适合32位变量的值0x0F0000000,编译器不能忽略该操作。它使用了转换 似乎比分裂操作更快。