web-dev-qa-db-fra.com

vaut-il mieux éviter d'utiliser l'opérateur mod quand c'est possible?

Je suppose que le calcul du module d'un nombre est une opération quelque peu coûteuse, du moins par rapport aux tests arithmétiques simples (comme voir si un nombre dépasse la longueur d'un tableau). Si tel est effectivement le cas, est-il plus efficace de remplacer, par exemple, le code suivant:

res = array[(i + 1) % len];

avec ce qui suit? :

res = array[(i + 1 == len) ? 0 : i + 1];

Le premier est plus agréable pour les yeux, mais je me demande si le second pourrait être plus efficace. Dans l'affirmative, est-ce que je pourrais m'attendre à ce qu'un compilateur d'optimisation remplace le premier extrait par le second, lorsqu'un langage compilé est utilisé?

Bien sûr, cette "optimisation" (s'il s'agit bien d'une optimisation) ne fonctionne pas dans tous les cas (dans ce cas, elle ne fonctionne que si i+1 n'est jamais plus que len).

42
limp_chimp

Mon conseil général est le suivant. Utilisez la version qui vous semble la plus agréable à l'œil, puis profilez l'ensemble de votre système. Optimisez uniquement les parties du code que le profileur signale comme goulots d'étranglement. Je parie que mon dernier dollar est que l'opérateur modulo ne sera pas parmi eux.

En ce qui concerne l'exemple spécifique, seul l'analyse comparative peut déterminer ce qui est le plus rapide sur votre architecture spécifique à l'aide de votre compilateur spécifique. Vous remplacez potentiellement modulo par branchement , et c'est tout sauf évident qui serait plus rapide.

30
NPE

Quelques mesures simples:

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

Compilation avec gcc ou clang avec -O3, et en cours d'exécution time ./a.out 0 42 1000000000 (version modulo) ou time ./a.out 1 42 1000000000 (version de comparaison) entraîne

  • 6,25 secondes runtime utilisateur pour la version modulo,
  • 1,03 secondes pour la version de comparaison.

(en utilisant gcc 5.2.1 ou clang 3.6.2; Intel Core i5-4690K à 3,50 GHz; Linux 64 bits)

Cela signifie que c'est probablement une bonne idée d'utiliser la version de comparaison.

25
dpi

Eh bien, jetez un œil à 2 façons d'obtenir la valeur suivante d'un compteur cyclique "modulo 3".

int next1(int n) {
    return (n + 1) % 3;
}

int next2(int n) {
    return n == 2 ? 0 : n + 1;
}

Je l'ai compilé avec l'option gcc -O3 (pour l'architecture x64 commune) et -s pour obtenir le code d'assemblage.

Le code de la première fonction fait de la magie inexplicable (*) pour éviter une division, en utilisant quand même une multiplication:

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

Et est beaucoup plus longue (et je parie plus lentement) que la deuxième fonction:

leal    1(%rdi), %eax
cmpl    $2, %edi
movl    $0, %edx
cmove   %edx, %eax
ret

Il n'est donc pas toujours vrai que "le compilateur (moderne) fait un meilleur travail que vous de toute façon".

Fait intéressant, la même expérience avec 4 au lieu de 3 conduit à un masquage et pour la première fonction

addl    $1, %edi
movl    %edi, %edx
sarl    $31, %edx
shrl    $30, %edx
leal    (%rdi,%rdx), %eax
andl    $3, %eax
subl    %edx, %eax
ret

mais il est encore, et dans l'ensemble, inférieur à la deuxième version.

Être plus explicite sur les bonnes façons de faire les choses

int next3(int n) {
    return (n + 1) & 3;;
}

donne de bien meilleurs résultats:

leal    1(%rdi), %eax
andl    $3, %eax
ret

(*) bien, pas si compliqué. Multiplication par réciproque. Calculez la constante entière K = (2 ^ N)/3, pour une valeur suffisamment grande de N. Maintenant, lorsque vous voulez la valeur de X/3, au lieu d'une division par 3, calculez X * K et déplacez-la N positions à droite.

3
Michel Billaud

Si "len" dans votre code est suffisamment grand, le conditionnel sera plus rapide, car les prédicteurs de branche devineront presque toujours correctement.

Sinon, je pense que cela est étroitement lié aux files d'attente circulaires, où il arrive souvent que la longueur soit une puissance de 2. Cela permettra au compilateur de remplacer modulo par un simple ET.

Le code est le suivant:

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

taille = 15:

  • modulo: 4,868s
  • cond: 1,291s

taille = 16:

  • modulo: 1,067s
  • cond: 1,599s

Compilé dans gcc 7.3.0, avec optimisation -O3. La machine est une i7 920.

0
J. Doe