【问题标题】:Performance optimization with modular addition when the modulus is of special form模数为特殊形式时通过模加法优化性能
【发布时间】:2012-08-30 14:14:57
【问题描述】:

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

设 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 倍!

【问题讨论】:

  • 操作系统不会自动并行化。
  • 某些模数比其他模数更难。仅此而已。
  • @ThomSmith:是的。正如我在上面指出的,我的测试也说了同样的话。
  • @SadeqDousti 如果系统足够空闲,上下文切换非常罕见,以至于更大的循环计数会增加计时的意义(我系统上的clock() 的分辨率为 10000 滴答声,所以简称-运行基准,小的差异不会可靠地显示出来)。此外,您应该使 x、y、z 易变并进行优化编译。我对此有很大的不同(令人惊讶的是,不是 -O0,我不明白为什么)。
  • @SadeqDousti 因此我将 x、y 和 z 设为易失性,因此编译器无法消除循环,必须在每次迭代中执行计算。如果没有 volatile,则完全删除循环,因此两者都产生 0 次计时。问题是,使用 -O0,两个循环之间的时间差很小,而通过优化,它大约是 50%(如果我让 x,y,z unsigned long 获得 64 位,则超过 100%,所以功率-of-2 模数不能完全去除)。毫不奇怪 -O1 使代码更快,它使差异更大(在这种情况下)。

标签: c++ c performance math assembly


【解决方案1】:

Artnailed 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 编译时,您测量的不是计算速度,而是循环控制代码的速度,因此差异很小。通过优化,您可以同时测量计算和回路控制,并且计算中的速度差异可以清楚地显示出来。

【讨论】:

  • 太棒了。从上面的 cmets 和这里的回答中,我了解到我应该使用关键字 volatile,并使用 O1 进行编译,以获得正确的结果。
【解决方案2】:

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

a/n 其中 n 是 2 的幂等于 a &gt;&gt; log2 n 模数是一样的 a mod n 可以被a &amp; (n-1) 渲染

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

对于适合 32 位变量的值 0x0F0000000,编译器无法省略操作。它使用了一个转换 似乎比除法运算要快。

【讨论】:

  • 或者在模数为 2^bitsize 的情况下,它是空操作。但有趣的是,OP 并没有问为什么它变慢了,他问为什么它没有他预期的那么慢。
  • +1。很好的解释。只剩下一点:由于在第二个示例中两次_clock 调用之间的指令数几乎翻了一番,我自然希望它慢100%。为什么它只慢了 10%? (你可以运行结果并检查一下)
  • @SadeqDousti 您不能“自然地期望”将汇编指令加倍会使执行时间加倍。一方面,不同的指令需要不同的时间。另一方面,现代 CPU 通常可以并行执行不同的指令。
  • 您可以查看 Agner Fog 的博客,他有一个表格,其中包含所有当前 x86 处理器的延迟、吞吐量和管道。 agner.org/optimize/instruction_tables.pdf
  • @SadeqDousti:IIRC vtune 和 codeanalyst 可以为 intel/amd cpus 进行流水线模拟,尽管我不是 100% 确定。但是,我不知道知道确切的日程安排对您有什么帮助。为了理解它,我也会推荐 Agner Fog 的优化手册。
猜你喜欢
  • 2012-03-08
  • 2016-04-06
  • 2015-03-04
  • 1970-01-01
  • 1970-01-01
  • 2013-06-16
  • 1970-01-01
  • 2016-09-25
  • 1970-01-01
相关资源
最近更新 更多