Test di divisibilità più veloce di% operatore?


23

Ho notato una cosa curiosa sul mio computer. * Il test di divisibilità scritto a mano è significativamente più veloce %dell'operatore. Considera l'esempio minimo:

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

L'esempio è limitato da dispari ae m > 0. Tuttavia, può essere facilmente generalizzato a tutti ae m. Il codice converte la divisione in una serie di aggiunte.

Consideriamo ora il programma di test compilato con -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
        }
    }

... e i risultati sul mio computer:

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

Pertanto più di 2 volte più veloce.

La domanda: puoi dirmi come si comporta il codice sul tuo computer? Ha perso l'opportunità di ottimizzazione in GCC? Puoi fare questo test ancora più velocemente?


AGGIORNAMENTO: Come richiesto, ecco un esempio riproducibile minimo:

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

compilato con gcc -std=c99 -march=native -O3 -DNDEBUGAMD Ryzen Threadripper 2990WX con

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

UPDATE2: come richiesto, la versione in grado di gestire qualsiasi ae m(se si desidera anche evitare l'overflow di numeri interi, il test deve essere implementato con il tipo intero due volte più lungo degli interi di input):

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

I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Samuel Liew

Vorrei anche vedere un test in cui affermi effettivamente che quei due rs che calcoli sono effettivamente uguali tra loro.
Mike Nakis,

@MikeNakis L'ho appena aggiunto.
DaBler

2
La maggior parte degli usi nella vita reale a % bha bmolto più piccoli di a. Attraverso la maggior parte delle iterazioni nel tuo test case, sono di dimensioni simili o bsono più grandi e la tua versione può essere più veloce su molte CPU in quelle situazioni.
Matt Timmermans,

Risposte:


11

Quello che stai facendo si chiama riduzione della forza: sostituire un'operazione costosa con una serie di operazioni economiche.

L'istruzione mod su molte CPU è lenta, perché storicamente non è stata testata in diversi benchmark comuni e quindi i progettisti hanno ottimizzato altre istruzioni. Questo algoritmo funzionerà peggio se deve fare molte iterazioni e% funzionerà meglio su una CPU dove ha bisogno solo di due cicli di clock.

Infine, tieni presente che ci sono molte scorciatoie per prendere il resto della divisione per costanti specifiche. (Anche se i compilatori generalmente si prenderanno cura di questo per te.)


storicamente non è stato testato in diversi benchmark comuni - Anche perché la divisione è intrinsecamente iterativa e difficile da velocizzare! x86 fa almeno il resto come parte div/ idivche ha ottenuto un po 'di amore in Intel Penryn, Broadwell e IceLake (divisori hardware radix superiori)
Peter Cordes

1
La mia comprensione della "riduzione della forza" è che sostituisci un'operazione pesante in un ciclo con una singola operazione più leggera, ad esempio invece di x = i * constogni iterazione che fai x += constogni iterazione. Non penso che sostituire un singolo moltiplicatore con un ciclo shift / add sarebbe chiamato riduzione della forza. en.wikipedia.org/wiki/… dice che il termine può forse essere usato in questo modo, ma con una nota "Questo materiale è contestato. È meglio descritto come ottimizzazione dello spioncino e assegnazione delle istruzioni."
Peter Cordes,

9

Risponderò alla mia domanda da solo. Sembra che sono diventato vittima della previsione del ramo. La dimensione reciproca degli operandi non sembra avere importanza, ma solo il loro ordine.

Considera la seguente implementazione

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

e gli array

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

che sono / non sono mescolati usando la funzione shuffle .

Senza mescolare, i risultati sono ancora

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

Tuttavia, una volta mischiato questi array, i risultati sono diversi

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |   31.34user |
| builtin % operator |   17.53user |
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.