% Operatörüne göre daha hızlı bölünebilirlik testi?


23

Bilgisayarımda ilginç bir şey fark ettim. * El yazısı bölünebilirlik testi %operatörden önemli ölçüde daha hızlıdır . Minimum örneği düşünün:

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

Örnek tek ave ile sınırlıdır m > 0. Ancak, herkes için kolayca genelleştirilebilir ave m. Kod, bölümü sadece bir dizi eklemeye dönüştürür.

Şimdi aşağıdakilerle derlenen test programını düşünün -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
        }
    }

... ve bilgisayarımdaki sonuçları:

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

Bu nedenle 2 kat daha hızlı.

Soru: Kodun makinenizde nasıl davrandığını söyleyebilir misiniz? GCC'de kaçırılan optimizasyon fırsatı var mı? Bu testi daha da hızlı yapabilir misin?


GÜNCELLEME: İstendiği gibi, burada tekrarlanabilir minimum bir örnek verilmiştir:

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

ile gcc -std=c99 -march=native -O3 -DNDEBUGAMD Ryzen Threadripper 2990WX ile derlendi

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

GÜNCELLEME2: İstendiği gibi, herhangi birini işleyebilen sürüm ave m(ayrıca tamsayı taşmasını önlemek istiyorsanız, test, girdi tamsayılarının iki katı kadar tamsayı türüyle uygulanmalıdır):

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

Yorumlar uzun tartışmalar için değildir; bu görüşme sohbete taşındı .
Samuel Liew

Ayrıca r, hesapladığınız iki s'nin gerçekten birbirine eşit olduğunu iddia ettiğiniz bir test de görmek istiyorum .
Mike Nakis

@MikeNakis Bunu ekledim.
DaBler

2
Çoğu gerçek hayattaki kullanımları a % bvar bçok daha küçük a. Test durumunuzdaki çoğu yinelemeyle, benzer boyutta veya bdaha büyüktür ve sürümünüz bu durumlarda birçok CPU'da daha hızlı olabilir.
Matt Timmermans

Yanıtlar:


11

Yaptığınız şeye güç azaltma denir: pahalı bir işlemi bir dizi ucuz olanla değiştirmek.

Birçok CPU'daki mod talimatı yavaştır, çünkü tarihsel olarak birkaç ortak ölçütte test edilmemiştir ve bu nedenle tasarımcılar bunun yerine diğer talimatları optimize etmiştir. Bu algoritma birçok yineleme yapmak zorundaysa daha kötü performans gösterir ve %yalnızca iki saat döngüsüne ihtiyaç duyduğu bir CPU'da daha iyi performans gösterir.

Son olarak, belirli sabitler tarafından bölünmenin geri kalanını alacak birçok kısayol olduğunu unutmayın. (Derleyiciler genellikle bununla ilgilenecektir.)


tarihsel olarak birçok ortak ölçütte test edilmedi - Ayrıca bölünme doğası gereği yinelemeli ve hızlı hale getirilmesi zor olduğu için! x86, en azından Intel Penryn, Broadwell ve IceLake'de (daha yüksek sayı tabanı donanım bölücüler) biraz aşk kazanmış div/ parçası olarak kaldıidiv
Peter Cordes

1
"Güç azaltma" anlayışım, bir döngüdeki ağır bir işlemi tek bir daha hafif işlemle değiştirmektir, örneğin x = i * consther yinelemede yaptığınız her yineleme yerine x += const. Ben bir çarpma / ekleme döngü ile tek bir çarpma yerine güç azaltma denir düşünmüyorum. tr.wikipedia.org/wiki/… , terimin bu şekilde kullanılabileceğini, ancak "Bu materyal tartışmalı. Gözetleme deliği optimizasyonu ve talimat ataması olarak daha iyi tanımlanır."
Peter Cordes

9

Soruma kendim cevap vereceğim. Görünüşe göre şube tahmininin kurbanı oldum. İşlenenlerin karşılıklı büyüklüğü önemli değil, sadece sıraları.

Aşağıdaki uygulamayı düşünün

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

ve diziler

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

karıştırma işlevi kullanılarak karıştırılmayan / karıştırılmayan .

Karıştırma olmadan, sonuçlar hala

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

Ancak, bu dizileri karıştırdığımda sonuçlar farklı

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |   31.34user |
| builtin % operator |   17.53user |
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.