Test de divisibilité plus rapide que l'opérateur%?

23

J'ai remarqué une chose curieuse sur mon ordinateur. * Le test de divisibilité manuscrit est beaucoup plus rapide que l' %opérateur. Prenons l'exemple minimal:

* 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'exemple est limité par impair aet m > 0. Cependant, il peut être facilement généralisé à tous aet m. Le code convertit simplement la division en une série d'ajouts.

Considérons maintenant le programme de test compilé avec -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
        }
    }

... et les résultats sur mon ordinateur:

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

Donc plus de 2 fois plus rapide.

La question: pouvez-vous me dire comment se comporte le code sur votre machine? Est-ce une opportunité d'optimisation manquée dans GCC? Pouvez-vous faire ce test encore plus rapidement?


MISE À JOUR: Comme demandé, voici un exemple reproductible minimal:

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

compilé avec gcc -std=c99 -march=native -O3 -DNDEBUGsur AMD Ryzen Threadripper 2990WX avec

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

UPDATE2: Comme demandé, la version qui peut gérer tout aet m(si vous voulez également éviter le débordement d'entier, le test doit être implémenté avec un type entier deux fois plus long que les entiers d'entrée):

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;
}
DaBler
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Samuel Liew
Je voudrais également voir un test où vous affirmez réellement que ces deux rs que vous calculez sont effectivement égaux l'un à l'autre.
Mike Nakis
@MikeNakis Je viens d'ajouter ça.
DaBler
2
La plupart des utilisations réelles de a % bsont bbeaucoup plus petites que a. Dans la plupart des itérations de votre scénario de test, elles sont de taille similaire ou bplus grande, et votre version peut être plus rapide sur de nombreux processeurs dans ces situations.
Matt Timmermans

Réponses:

11

Ce que vous faites s'appelle la réduction de la force: remplacer une opération coûteuse par une série d'opérations bon marché.

L'instruction mod sur de nombreux processeurs est lente, car historiquement elle n'a pas été testée dans plusieurs benchmarks courants et les concepteurs ont donc optimisé d'autres instructions à la place. Cet algorithme fonctionnera moins bien s'il doit effectuer plusieurs itérations, et %fonctionnera mieux sur un processeur où il n'a besoin que de deux cycles d'horloge.

Enfin, sachez qu'il existe de nombreux raccourcis pour prendre le reste de la division par des constantes spécifiques. (Bien que les compilateurs s'en occupent généralement pour vous.)

Davislor
la source
historiquement n'a pas été testé dans plusieurs repères communs - également parce que la division est intrinsèquement itérative et difficile à faire rapidement! x86 au moins fait le reste dans le cadre de div/ idivqui ont obtenu un certain amour dans Intel Penryn, Broadwell et IceLake (diviseurs matériels radix supérieurs)
Peter Cordes
1
Ma compréhension de la "réduction de la force" est que vous remplacez une opération lourde dans une boucle par une seule opération plus légère, par exemple, au lieu de x = i * constchaque itération, vous effectuez x += constchaque itération. Je ne pense pas que remplacer une seule multiplication par une boucle shift / add serait appelé réduction de force. en.wikipedia.org/wiki/… dit que le terme peut peut-être être utilisé de cette façon, mais avec une note "Ce matériel est contesté. Il est mieux décrit comme l'optimisation de judas et l'affectation d'instructions."
Peter Cordes
9

Je répondrai moi-même à ma question. Il semble que je suis devenu victime de la prédiction de branche. La taille mutuelle des opérandes ne semble pas avoir d'importance, seulement leur ordre.

Considérez l'implémentation suivante

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

et les tableaux

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

qui sont / ne sont pas mélangés à l'aide de la fonction de lecture aléatoire .

Sans mélange, les résultats sont toujours

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

Cependant, une fois que je mélange ces tableaux, les résultats sont différents

| implementation     | time [secs] |
|--------------------|-------------|
| divisible_ui_p     |   31.34user |
| builtin % operator |   17.53user |
DaBler
la source