65

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

res = array[(i + 1) % len];

与以下?:

res = array[(i + 1 == len) ? 0 : i + 1];

第一个在眼睛上更容易,但我想知道第二个是否更有效。如果是这样,我是否希望优化编译器在使用编译语言时将第一个片段替换为第二个片段?

当然,这种“优化”(如果它确实是一种优化)并非在所有情况下都有效(在这种情况下,它只有在i+1不超过时才有效len)。

4

8 回答 8

47

我的一般建议如下。使用您认为更容易使用的任何版本,然后配置您的整个系统。仅优化分析器标记为瓶颈的那些代码部分。我敢打赌,模运算符不会在其中。

就具体示例而言,只有基准测试才能判断使用您的特定编译器在您的特定架构上哪个更快。您可能会用branching替换 modulo ,而且速度更快是显而易见的。

于 2013-03-24T07:57:06.660 回答
30

一些简单的测量:

#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);
}

使用 gcc 或 clang with 编译-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)

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

于 2016-01-31T13:33:47.973 回答
6

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

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

int next2(int n) {
    return n == 2 ? 0 : n + 1;
}

我已经用 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

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

leal    1(%rdi), %eax
cmpl    $2, %edi
movl    $0, %edx
cmove   %edx, %eax
ret

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

有趣的是,使用 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

但它仍然,而且在很大程度上,不如第二个版本。

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

int next3(int n) {
    return (n + 1) & 3;;
}

产生更好的结果:

leal    1(%rdi), %eax
andl    $3, %eax
ret

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

于 2018-09-24T15:05:17.483 回答
2

这是一些额外的基准。请注意,我还添加了一个无分支版本:

#include <iostream>
#include <array>
#include <algorithm>
#include <random>
#include <chrono>
using namespace std::chrono;

constexpr size_t iter = 1e8;

int main() {
  std::minstd_rand rnd_engine{1234};
  std::uniform_int_distribution<int> dist {-1000, 1000};
  auto gen = [&]() { return dist(rnd_engine); };

  std::array<int, 10> a;
  std::generate( a.begin(), a.end(), gen);

  for (size_t size = 2; size < 10; size++) {
    std::cout << "Modulus size = " << size << '\n';
  
    {
      std::cout << "operator%  ";
      long sum = 0;
      size_t x = 0;
      auto start = high_resolution_clock::now();
      for (size_t i = 0; i < iter; ++i) {
        sum += a[x];
        x = (x + 1) % size;
      }
      auto stop = high_resolution_clock::now();
      std::cout << duration_cast<microseconds>(stop - start).count()*0.001
                << "ms\t(sum = " << sum << ")\n";
    }
  
    {
      std::cout << "ternary    ";
      long sum = 0;
      size_t x = 0;
      auto start = high_resolution_clock::now();
      for (size_t i = 0; i < iter; ++i) {
        sum += a[x];
        x = ((x + 1) == size) ? 0 : x + 1;
      }
      auto stop = high_resolution_clock::now();
      std::cout << duration_cast<microseconds>(stop - start).count()*0.001
                << "ms\t(sum = " << sum << ")\n";
    }
    
    {
      std::cout << "branchless ";
      long sum = 0;
      size_t x = 1;
      auto start = high_resolution_clock::now();
      for (size_t i = 0; i < iter; ++i) {
        sum += a[x-1];
        x = ( x != size ) * x + 1;
      }
      auto stop = high_resolution_clock::now();
      std::cout << duration_cast<microseconds>(stop - start).count()*0.001
                << "ms\t(sum = " << sum << ")\n";
    }

  }
  return 0;
}

这是我的 i7-4870HQ 上的输出

$ g++ -Ofast test.cpp && ./a.out
Modulus size = 2
operator%  904.249ms    (sum = -4200000000)
ternary    137.04ms     (sum = -4200000000)
branchless 169.182ms    (sum = -4200000000)
Modulus size = 3
operator%  914.911ms    (sum = -31533333963)
ternary    113.384ms    (sum = -31533333963)
branchless 167.614ms    (sum = -31533333963)
Modulus size = 4
operator%  877.3ms      (sum = -36250000000)
ternary    97.265ms     (sum = -36250000000)
branchless 167.215ms    (sum = -36250000000)
Modulus size = 5
operator%  891.295ms    (sum = -30700000000)
ternary    88.562ms     (sum = -30700000000)
branchless 167.087ms    (sum = -30700000000)
Modulus size = 6
operator%  903.644ms    (sum = -39683333196)
ternary    83.433ms     (sum = -39683333196)
branchless 167.778ms    (sum = -39683333196)
Modulus size = 7
operator%  908.096ms    (sum = -34585713678)
ternary    79.703ms     (sum = -34585713678)
branchless 166.849ms    (sum = -34585713678)
Modulus size = 8
operator%  869ms        (sum = -39212500000)
ternary    76.972ms     (sum = -39212500000)
branchless 167.29ms     (sum = -39212500000)
Modulus size = 9
operator%  875.003ms    (sum = -36500000580)
ternary    75.011ms     (sum = -36500000580)
branchless 172.356ms    (sum = -36500000580)

在这种特殊情况下,三元运算符看起来要好得多,当分支预测器上升时,它变得更加如此。但是请注意,这是一个非常特殊的情况:如果我们不按非常量值递增索引,则使用更通用operator%的方法会很简单,而其他两种方法可能会变得非常复杂。

我想强调这个被低估的评论:

如果 len 是编译时常量,则最近的 GCC 编译器(带有 -02) 通常会做一些聪明的事情,通常会避免目标处理器的模数机器 指令。——巴西尔·斯塔林克维奇

例如,通过删除size变量上的循环并在const size_t size = 4;我得到时声明它:

g++ -Ofast test.cpp && ./a.out
Modulus size = 4
operator%  62.103ms     (sum = -36250000000)
ternary    93.674ms     (sum = -36250000000)
branchless 166.774ms    (sum = -36250000000)

结论

无分支版本的执行时间在各种场景中都相当稳定。在考虑的情况下,三进制始终优于无分支,尤其是在分支预测器启动时。最后,operator%虽然更通用且速度明显较慢,但在特定 const 值的情况下,有机会优化以成为最快的的右手边。

当然,这完全取决于平台,谁知道这将如何在 Arduino 上进行 :)

于 2020-08-20T19:36:19.890 回答
0

如果代码中的“len”足够大,那么条件会更快,因为分支预测器几乎总是会正确猜测。

如果不是,那么我相信这与循环队列密切相关,在这种情况下,长度通常是 2 的幂。这将使编译器能够用简单的 AND 替换模。

代码如下:

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

#define modulo

int main()
{
    int iterations = 1000000000;
    int size = 16;
    int a[size];
    unsigned long long res = 0;
    int i, j;

    for (i=0;i<size;i++)
        a[i] = i;

    for (i=0,j=0;i<iterations;i++)
    {
        j++;
        #ifdef modulo
            j %= size;
        #else
            if (j >= size)
                j = 0;
        #endif
        res += a[j];
    }

    printf("%llu\n", res);
}

大小=15:

  • 模数:4,868s
  • 条件:1,291s

大小=16:

  • 模数:1,067s
  • 条件:1,599s

在 gcc 7.3.0 中编译,带有 -O3 优化。机器是i7 920。

于 2019-04-17T14:12:29.853 回答
0

我阅读了有关制作快速哈希图的文章。瓶颈可以是模算子来找到哈希桶。他们建议将您的存储桶数设为 2 的幂。显然,通过 2 的幂进行取模就像查看最后 n 位一样。

于 2020-09-05T23:08:41.613 回答
0

模运算符很昂贵,但除法也很昂贵。因此,将您的代码从使用模运算符转换为除法不会优化您的代码。

  (i + 1) % len

优化上面的代码

if ((i+1)==len){
   return 0
} else {
   return i+1
}
于 2022-01-31T02:21:36.870 回答
-3

在大多数架构(例如 x86 上的 DIV)上,可以使用单个处理器指令来完成取模。但是,这可能是您需要的过早优化。

于 2013-03-24T08:02:33.513 回答