最好避免在可能的情况下使用mod运算符吗?

lim*_*imp 45 c optimization performance modulo

我假设计算数字的模数是一个稍微昂贵的操作,至少与简单的算术测试(例如,查看数字是否超过数组的长度)相比.如果确实如此,替换更有效,例如,以下代码:

res = array[(i + 1) % len];
Run Code Online (Sandbox Code Playgroud)

以下是什么?:

res = array[(i + 1 == len) ? 0 : i + 1];
Run Code Online (Sandbox Code Playgroud)

第一个更容易在眼睛上,但我想知道第二个可能更有效.如果是这样,当使用编译语言时,我是否可以期望优化编译器将第一个代码段替换为第二个代码段?

当然,这种"优化"(如果它确实是一种优化)在所有情况下都不起作用(在这种情况下,它只有在i+1永远不会超过的情况下才有效len).

NPE*_*NPE 33

我的一般建议如下.使用您认为更容易看到的版本,然后分析整个系统.只优化探查器标记为代码瓶颈的代码部分.我敢打赌我的底价是模数运算符不会在其中.

就具体示例而言,只有基准测试可以告诉您使用特定编译器在特定体系结构上哪个更快.你可能会用分支替换模数,而且显而易见的是更快的东西.

  • @BasileStarynkevitch:嗯,两个代码片段之间的缓存行为是相同的.重要的是版本#2是否使用分支,如果是,则分支预测器将要执行的工作有多好. (3认同)
  • 在最近的机器上整数运算几乎是免费的;更重要的是缓存未命中..... 成本更高。L1 缓存未命中会使处理器停顿数百个周期,在此期间处理器可以进行数十次除法或取模;所以模数的最终成本是噪声 (2认同)

dpi*_*dpi 25

一些简单的测量:

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int test = atoi(argv[1]);
    int divisor = atoi(argv[2]);
    int iterations = atoi(argv[3]);

    int a = 0;

    if (test == 0) {
        for (int i = 0; i < iterations; i++)
            a = (a + 1) % divisor;
    } else if (test == 1) {
        for (int i = 0; i < iterations; i++)
            a = a + 1 == divisor ? 0 : a + 1;
    }

    printf("%d\n", a);
}
Run Code Online (Sandbox Code Playgroud)

使用gcc或clang编译-O3,并运行time ./a.out 0 42 1000000000(模数版本)或time ./a.out 1 42 1000000000(比较版本)会导致

  • 模数版本的用户运行时间为6.25秒,
  • 比较版本为1.03秒.

(使用gcc 5.2.1或clang 3.6.2; Intel Core i5-4690K @ 3.50GHz; 64位Linux)

这意味着使用比较版本可能是个好主意.

  • 比较版本只是更快,因为if语句的结果每次都相同,所以分支预测器每次都正确.如果您将输入随机化,则比较版本甚至可能比mod差 (12认同)
  • 在更现实的数据上(例如,如果数字是随机的)那么差异就不那么大了 (3认同)

Mic*_*aud 6

好吧,看一下获取“模 3”循环计数器的下一个值的 2 种方法。

int next1(int n) {
    return (n + 1) % 3;
}

int next2(int n) {
    return n == 2 ? 0 : n + 1;
}
Run Code Online (Sandbox Code Playgroud)

我已经用 gcc -O3 选项(对于常见的 x64 架构)和 -s 编译它以获取汇编代码。

第一个函数的代码做了一些无法解释的魔法 (*) 来避免除法,无论如何都使用乘法:

addl    $1, %edi
movl    $1431655766, %edx
movl    %edi, %eax
imull   %edx
movl    %edi, %eax
sarl    $31, %eax
subl    %eax, %edx
leal    (%rdx,%rdx,2), %eax
subl    %eax, %edi
movl    %edi, %eax
ret
Run Code Online (Sandbox Code Playgroud)

并且比第二个函数长得多(我敢打赌更慢):

leal    1(%rdi), %eax
cmpl    $2, %edi
movl    $0, %edx
cmove   %edx, %eax
ret
Run Code Online (Sandbox Code Playgroud)

因此,“(现代)编译器无论如何都比您做得更好”并不总是正确的。

有趣的是,同样的实验用 4 而不是 3 导致第一个函数的 and-masking

addl    $1, %edi
movl    %edi, %edx
sarl    $31, %edx
shrl    $30, %edx
leal    (%rdi,%rdx), %eax
andl    $3, %eax
subl    %edx, %eax
ret
Run Code Online (Sandbox Code Playgroud)

但总的来说,它仍然不如第二个版本。

更明确地说明做事的正确方法

int next3(int n) {
    return (n + 1) & 3;;
}
Run Code Online (Sandbox Code Playgroud)

产生更好的结果:

leal    1(%rdi), %eax
andl    $3, %eax
ret
Run Code Online (Sandbox Code Playgroud)

(*) 好吧,没那么复杂。乘以倒数。计算整数常数 K = (2^N)/3,对于一些足够大的 N 值。 现在,当你想要 X/3 的值时,而不是除以 3,计算 X*K,并将其移位 N位置在右边。

  • `%4` 代码更复杂,因为您正在执行_signed_算术。根据C99,模数的符号必须与被除数的符号匹配,因此它不仅仅是直接按位AND。将类型更改为“unsigned int”,您将得到与“&amp;3”代码相同的结果。 (3认同)
  • -1,因为答案强烈建议根据代码大小进行判断,这是一种不错的启发式方法,但在涉及本问题中的优化时,这是一个错误。并非所有指令都是相同的。即使在 RISC 架构上,某些操作也可能比其他操作花费更长的时间,而在流水线 CPU 上,执行错误预测的分支所花费的时间(比分支本身更长,但在分支之后继续,直到在更深层次中找到分支条件的结果)。管道)可能比具有更多指令的无条件代码花费的时间更长。 (2认同)