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

时间:2013-03-24 07:54:44

标签: c performance optimization modulo

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

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

以下? :

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

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

当然,这种“优化”(如果它确实是一种优化)在所有情况下都不起作用(在这种情况下,只有在i+1永远不超过len时才有效。) / p>

7 个答案:

答案 0 :(得分:29)

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

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

答案 1 :(得分: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);
}

使用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)

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

答案 2 :(得分:3)

好,看看两种获取“模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个相同的实验导致第一个函数的和掩码

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

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

答案 3 :(得分:1)

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

#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)是 通常做聪明的事情,经常避免使用模数机器 目标处理器的指令。Basile Starynkevitch

例如,通过删除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上会是怎样的:)

答案 4 :(得分: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
  • cond:1,291秒

size = 16:

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

在gcc 7.3.0中进行了编译,并带有-O3优化。 该机器是i7 920。

答案 5 :(得分:0)

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

答案 6 :(得分:-3)

模块可以在大多数体系结构上使用单处理器指令完成(例如x86上的DIV)。但是,它可能是您所需要的过早优化。