web-dev-qa-db-ja.com

可能であればmod演算子の使用を避ける方が良いですか?

少なくとも単純な算術テスト(数値が配列の長さを超えているかどうかを確認するなど)と比較すると、数値のモジュラスの計算はやや高価な操作であると思います。これが実際に当てはまる場合、たとえば次のコードを置き換える方が効率的ですか?

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

次で? :

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

最初の方が目には簡単ですが、2番目の方が効率的かと思います。もしそうなら、コンパイルされた言語が使用されているときに、最適化コンパイラが最初のスニペットを2番目のスニペットに置き換えることを期待できますか?

もちろん、この「最適化」(実際に最適化である場合)はすべての場合に機能するわけではありません(この場合、i+1lenを超えることはありません。

42
limp_chimp

私の一般的なアドバイスは次のとおりです。目に見えると思うバージョンを使用し、システム全体のプロファイルを作成します。プロファイラーがボトルネックとしてフラグを立てるコードの部分のみを最適化します。モジュロ演算子はその中にはないだろうと、私は一番下のドルを賭けます。

特定の例に関して言えば、特定のコンパイラを使用して特定のアーキテクチャ上でどれが高速であるかを判断できるのはベンチマークのみです。モジュロを branching で置き換える可能性がありますが、それは明らかですが、どちらが速いかは明らかです。

30
NPE

簡単な測定:

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

-O3を使用してgccまたはclangでコンパイルし、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)

つまり、比較バージョンを使用することをお勧めします。

25
dpi

さて、「モジュロ3」巡回カウンタの次の値を取得する2つの方法を見てください。

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

そして、2番目の関数よりもはるかに長いです(そして私は間違いなく遅いです):

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

そのため、「(最新の)コンパイラがとにかくあなたよりも良い仕事をする」ということは必ずしも真実ではありません。

興味深いことに、3の代わりに4を使用した同じ実験は、最初の関数のandマスキングにつながります

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

しかし、それはまだであり、概して、第2バージョンよりも劣っています。

物事を行う適切な方法についてより明確に

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

より良い結果が得られます:

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

(*)まあ、それほど複雑ではありません。相反による乗算。整数定数K =(2 ^ N)/ 3を計算します。これは、Nの十分な大きさの値に対して行われます。右の位置。

3
Michel Billaud

コード内の「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,868秒
  • cond:1,291s

サイズ= 16:

  • モジュロ:1,067s
  • cond:1,599s

-O3最適化を使用してgcc 7.3.0でコンパイル。マシンはi7 920です。

0
J. Doe