可能であればmod演算子の使用を避ける方が良いですか?
少なくとも単純な算術テスト(数値が配列の長さを超えているかどうかを確認するなど)と比較すると、数値のモジュラスの計算はやや高価な操作であると思います。これが実際に当てはまる場合、たとえば次のコードを置き換える方が効率的ですか?
res = array[(i + 1) % len];
次で? :
res = array[(i + 1 == len) ? 0 : i + 1];
最初の方が目には簡単ですが、2番目の方が効率的かと思います。もしそうなら、コンパイルされた言語が使用されているときに、最適化コンパイラが最初のスニペットを2番目のスニペットに置き換えることを期待できますか?
もちろん、この「最適化」(実際に最適化である場合)はすべての場合に機能するわけではありません(この場合、i+1
がlen
を超えることはありません。
私の一般的なアドバイスは次のとおりです。目に見えると思うバージョンを使用し、システム全体のプロファイルを作成します。プロファイラーがボトルネックとしてフラグを立てるコードの部分のみを最適化します。モジュロ演算子はその中にはないだろうと、私は一番下のドルを賭けます。
特定の例に関して言えば、特定のコンパイラを使用して特定のアーキテクチャ上でどれが高速であるかを判断できるのはベンチマークのみです。モジュロを branching で置き換える可能性がありますが、それは明らかですが、どちらが速いかは明らかです。
簡単な測定:
#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)
つまり、比較バージョンを使用することをお勧めします。
さて、「モジュロ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の十分な大きさの値に対して行われます。右の位置。
コード内の「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です。