%演算子よりも速い分割可能性テスト?


23

コンピューターに不思議なことに気づきました。*手書きの分割可能性テストは、%オペレーターよりも大幅に高速です。最小限の例を考えてみましょう:

* AMD Ryzen Threadripper 2990WX、GCC 9.2.0

static int divisible_ui_p(unsigned int m, unsigned int a)
{
    if (m <= a) {
        if (m == a) {
            return 1;
        }

        return 0;
    }

    m += a;

    m >>= __builtin_ctz(m);

    return divisible_ui_p(m, a);
}

例は奇数aとによって制限されm > 0ます。ただし、すべてのaおよびに簡単に一般化できますm。コードは除算を一連の追加に変換するだけです。

でコンパイルされたテストプログラムを考えてみましょう-std=c99 -march=native -O3

    for (unsigned int a = 1; a < 100000; a += 2) {
        for (unsigned int m = 1; m < 100000; m += 1) {
#if 1
            volatile int r = divisible_ui_p(m, a);
#else
            volatile int r = (m % a == 0);
#endif
        }
    }

...そして私のコンピューター上の結果:

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.52user |
| builtin % operator |   17.61user |

したがって、2倍以上速くなります。

質問:コードがマシンでどのように動作するか教えていただけますか?GCCで最適化の機会を逃していませんか?このテストをもっと速く行うことができますか?


更新: 要求されたとおり、これは最小限の再現可能な例です:

#include <assert.h>

static int divisible_ui_p(unsigned int m, unsigned int a)
{
    if (m <= a) {
        if (m == a) {
            return 1;
        }

        return 0;
    }

    m += a;

    m >>= __builtin_ctz(m);

    return divisible_ui_p(m, a);
}

int main()
{
    for (unsigned int a = 1; a < 100000; a += 2) {
        for (unsigned int m = 1; m < 100000; m += 1) {
            assert(divisible_ui_p(m, a) == (m % a == 0));
#if 1
            volatile int r = divisible_ui_p(m, a);
#else
            volatile int r = (m % a == 0);
#endif
        }
    }

    return 0;
}

gcc -std=c99 -march=native -O3 -DNDEBUGAMD Ryzen Threadripper 2990WXでコンパイル

gcc --version
gcc (Gentoo 9.2.0-r2 p3) 9.2.0

UPDATE2:要求に応じて、すべてaを処理できるバージョンm(整数オーバーフローも回避したい場合は、入力整数の2倍の整数型でテストを実装する必要があります):

int divisible_ui_p(unsigned int m, unsigned int a)
{
#if 1
    /* handles even a */
    int alpha = __builtin_ctz(a);

    if (alpha) {
        if (__builtin_ctz(m) < alpha) {
            return 0;
        }

        a >>= alpha;
    }
#endif

    while (m > a) {
        m += a;
        m >>= __builtin_ctz(m);
    }

    if (m == a) {
        return 1;
    }

#if 1
    /* ensures that 0 is divisible by anything */
    if (m == 0) {
        return 1;
    }
#endif

    return 0;
}

コメントは詳細な議論のためのものではありません。この会話はチャットに移動しました
サミュエルLiew

また、r計算した2つのが実際に互いに等しいと実際に主張するテストも見たいと思います。
Mike Nakis

@MikeNakisたった今追加しました。
DaBler

2
ほとんどの実際の用途はa % b持っているbよりもはるかに小さいですa。テストケースのほとんどの反復では、それらは同じようなサイズか、それbよりも大きく、そのような状況では、多くのCPUでバージョンが高速になる可能性があります。
Matt Timmermans

回答:


11

あなたがしていることは強度低下と呼ばれます:高価な操作を一連の安価な操作に置き換えます。

多くのCPUのmod命令は、これまでいくつかの一般的なベンチマークでテストされておらず、設計者が代わりに他の命令を最適化したため、低速です。このアルゴリズムは、多くの反復を実行する必要がある場合はパフォーマンスが低下%し、2クロックサイクルしか必要としないCPUでパフォーマンスが向上します。

最後に、残りの部分を特定の定数で除算するための多くのショートカットがあることに注意してください。(ただし、通常はコンパイラーがこれを処理します。)


歴史的には、いくつかの一般的なベンチマークでテストされていません -また、除算は本質的に反復的であり、高速にするのが難しいためです!x86は少なくとも残りの一部としてdiv/ idivIntel Penryn、Broadwell、IceLake(上位基数のハードウェアディバイダー)で愛されてきた
Peter Cordes

1
「強度低下」についての私の理解は、ループ内の重い操作を単一の軽い操作に置き換えることです。たとえば、x = i * constすべての反復の代わりに、すべての反復を行いx += constます。単一の乗算をシフト/加算ループで置き換えることは、強度低下と呼ばれるとは思いません。en.wikipedia.org/wiki/…は、この用語はこのように使用できる可能性があると述べていますが、「この資料には異議があります。のぞき穴の最適化と命令の割り当てとしてよりよく説明されています。」
Peter Cordes

9

自分で質問に答えます。分岐予測の犠牲になったようです。オペランドの相互のサイズは重要ではないようで、順序だけが重要です。

次の実装を検討してください

int divisible_ui_p(unsigned int m, unsigned int a)
{
    while (m > a) {
        m += a;
        m >>= __builtin_ctz(m);
    }

    if (m == a) {
        return 1;
    }

    return 0;
}

とアレイ

unsigned int A[100000/2];
unsigned int M[100000-1];

for (unsigned int a = 1; a < 100000; a += 2) {
    A[a/2] = a;
}
for (unsigned int m = 1; m < 100000; m += 1) {
    M[m-1] = m;
}

シャッフル機能を使用してシャッフルされる/されない。

シャッフルせずに、結果はまだです

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |    8.56user |
| builtin % operator |   17.59user |

ただし、これらの配列をシャッフルすると、結果が異なります

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |   31.34user |
| builtin % operator |   17.53user |
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.