web-dev-qa-db-fra.com

Pourquoi GCC n'optimise-t-il pas un * a * a * a * a * a à (a * a * a) * (a * a * a)?

Je suis en train d’optimiser numériquement une application scientifique. Une chose que j’ai remarquée est que GCC optimisera l’appel pow(a,2) en le compilant dans a*a, mais l’appel pow(a,6) n’est pas optimisé et appellera en fait la fonction de bibliothèque pow, ce qui ralentit considérablement la performance. (En revanche, compilateur Intel C++ , exécutable icc, éliminera l'appel de la bibliothèque pour pow(a,6).)

Ce qui est curieux, c’est que lorsque j’ai remplacé pow(a,6) par a*a*a*a*a*a avec GCC 4.5.1 et les options "-O3 -lm -funroll-loops -msse4", il utilise les instructions 5 mulsd:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

alors que si j'écris (a*a*a)*(a*a*a), cela produira

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

ce qui réduit le nombre d'instructions de multiplication à 3. icc a un comportement similaire.

Pourquoi les compilateurs ne reconnaissent-ils pas cette astuce d'optimisation?

2071
xis

Parce que le calcul en virgule flottante n'est pas associatif . La façon dont vous regroupez les opérandes en multiplication à virgule flottante a une incidence sur la précision numérique de la réponse.

En conséquence, la plupart des compilateurs sont très prudents quant à la réorganisation des calculs en virgule flottante, sauf s’ils peuvent être certains que la réponse restera la même ou si vous ne leur dites pas que vous ne vous souciez pas de la précision numérique. Par exemple: l'option -fassociative-math] de gcc qui permet à gcc de réassocier des opérations en virgule flottante, ou même l'option -ffast-math qui permet des compromis encore plus agressifs entre précision et vitesse.

2677
Lambdageek

Lambdageek souligne correctement que, comme l'associativité ne s'applique pas aux nombres à virgule flottante, "l'optimisation" de a*a*a*a*a*a à (a*a*a)*(a*a*a) peut changer la valeur. C'est pourquoi il est interdit par C99 (sauf autorisation expresse de l'utilisateur, via un indicateur de compilation ou un pragma). En règle générale, l'hypothèse est que la programmeuse a écrit ce qu'elle a fait pour une raison, et le compilateur devrait respecter cela. Si vous voulez (a*a*a)*(a*a*a), écrivez cela.

Cela peut être pénible à écrire, cependant; pourquoi le compilateur ne peut-il pas simplement faire [ce que vous considérez être] la bonne chose lorsque vous utilisez pow(a,6)? Parce que ce serait la mauvaise chose à faire. Sur une plate-forme dotée d'une bonne bibliothèque mathématique, pow(a,6) est nettement plus précis que a*a*a*a*a*a ou (a*a*a)*(a*a*a). Juste pour fournir des données, j'ai effectué une petite expérience sur mon Mac Pro, mesurant la pire erreur lors de l'évaluation de ^ 6 pour tous les nombres flottants à simple précision compris entre [1,2):

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

L'utilisation de pow au lieu d'un arbre de multiplication réduit l'erreur liée à un facteur de 4 . Les compilateurs ne devraient pas (et ne font généralement pas) faire des "optimisations" qui augmentent l'erreur sauf si l'utilisateur le permet (par exemple via -ffast-math).

Notez que GCC fournit __builtin_powi(x,n) comme alternative à pow( ), qui devrait générer un arbre de multiplication en ligne. Utilisez-le si vous voulez échanger précision et performance, mais ne souhaitez pas activer le calcul rapide.

642
Stephen Canon

Un autre cas similaire: la plupart des compilateurs n'optimiseront pas a + b + c + d en (a + b) + (c + d) (il s'agit d'une optimisation puisque la deuxième expression peut être mieux traitée en pipeline) et l'évaluera comme donnée (c'est-à-dire comme (((a + b) + c) + d)). C'est aussi à cause des cas de coin:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

Cette sortie 1.000000e-05 0.000000e+00

164
sanjoyd

Fortran (conçu pour l'informatique scientifique) a un opérateur de puissance intégré et, autant que je sache, les compilateurs Fortran optimisent généralement l'augmentation des puissances de nombres entiers de la même manière que ce que vous décrivez. Malheureusement, C/C++ n’a pas d’opérateur électrique, seulement la fonction de bibliothèque pow(). Cela n'empêche pas les compilateurs intelligents de traiter spécialement pow et de le calculer plus rapidement dans des cas particuliers, mais il semble qu'ils le fassent moins fréquemment ...

Il y a quelques années, j'essayais de rendre plus pratique le calcul optimal des puissances sur un entier. C'est du C++, pas du C cependant, et cela dépend toujours du compilateur qui est assez intelligent pour optimiser les choses/en ligne. Quoi qu'il en soit, j'espère que vous trouverez cela utile dans la pratique:

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

Clarification pour les curieux: ceci ne trouve pas le moyen optimal de calculer les puissances, mais depuis trouver la solution optimale est un problème NP-complet et cela ne vaut de toute façon que pour les petites puissances (par opposition à l’utilisation de pow), il n’ya aucune raison de s’embarrasser du détail.

Ensuite, utilisez-le simplement comme power<6>(a).

Cela facilite la frappe des pouvoirs (pas besoin d’écrire 6 as avec des parens), et vous permet d’avoir ce type d’optimisation sans -ffast-math au cas où vous auriez quelque chose dépendant de la précision, tel que sommation compensée (un exemple où l'ordre des opérations est essentiel).

Vous pouvez probablement aussi oublier qu'il s'agit de C++ et l'utiliser simplement dans le programme C (s'il compile avec un compilateur C++).

J'espère que cela peut être utile.

EDIT:

Voici ce que je tire de mon compilateur:

Pour a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

Pour (a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

Pour power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
79
Szabolcs

GCC optimise effectivement un a a a a a à (a a a) (a a a) quand a est un entier. J'ai essayé avec cette commande:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

Il y a beaucoup de drapeaux gcc mais rien d'extraordinaire. Ils veulent dire: Read from stdin; utiliser le niveau d'optimisation de l'O2; affiche la liste des langues d'assemblage au lieu d'un fichier binaire; la liste doit utiliser la syntaxe du langage d'assemblage d'Intel; l'entrée est en langage C (généralement la langue est déduite de l'extension du fichier d'entrée, mais il n'y a pas d'extension de fichier lors de la lecture de stdin); et écrivez sur stdout.

Voici la partie importante de la sortie. Je l'ai annoté avec quelques commentaires indiquant ce qui se passe dans la langue de l'Assemblée:

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

J'utilise le système GCC sous Linux Mint 16 Petra, un dérivé d'Ubuntu. Voici la version gcc:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

Comme d'autres auteurs l'ont fait remarquer, cette option n'est pas possible en virgule flottante, car l'arithmétique en virgule flottante n'est en réalité pas associative.

58
picomancer

Car un nombre à virgule flottante 32 bits, tel que 1.024, n'est pas 1.024. Dans un ordinateur, 1.024 est un intervalle: de (1.024-e) à (1.024 + e), où "e" représente une erreur. Certaines personnes ne s'en rendent pas compte et croient également que * dans a * a signifie multiplication de nombres de précision arbitraire sans erreurs associées à ces nombres. Certaines personnes échouent à comprendre que c’est peut-être les calculs mathématiques qu’ils ont effectués à l’école élémentaire: ne travailler qu'avec des nombres idéaux sans erreurs, et croire qu’il est acceptable d’ignorer simplement "e" lors de la multiplication. Ils ne voient pas le "e" implicite dans "float a = 1.2", "a * a * a" et les codes C similaires.

Si la majorité des programmeurs reconnaissent (et sont capables d'exécuter) l'idée que l'expression C a * a * a * a * a * a ne fonctionne pas avec des nombres idéaux, le compilateur GCC serait alors libre d'optimiser "a * a * a * a * a * a "en dire" t = (a * a); t * t * t "qui nécessite un plus petit nombre de multiplications. Mais malheureusement, le compilateur GCC ne sait pas si le programmeur qui écrit le code pense que "a" est un nombre avec ou sans erreur. Ainsi, GCC ne fera que reproduire le code source, car c’est ce que GCC voit de son "œil nu".

... une fois que vous savez quel genre de programmeur vous êtes, vous pouvez utiliser le commutateur "-ffast-math" pour dire à GCC que "Hey, GCC, je sais ce que je fais!". Cela permettra à GCC de convertir un * a * a * a * a * a * a en un morceau de texte différent - il est différent de a * a * a * a * a * a - mais calcule tout de même un nombre compris dans l'intervalle d'erreur de a * a * a * a * a * a. C'est bon, car vous savez déjà que vous travaillez avec des intervalles, pas des nombres idéaux.

51
user811773

Aucune affiche n'a encore mentionné la contraction des expressions flottantes (norme ISO C, 6.5p8 et 7.12.2). Si le pragma FP_CONTRACT est défini sur ON, le compilateur est autorisé à considérer une expression telle que a*a*a*a*a*a comme une opération unique, comme si elle était évaluée exactement avec un arrondi unique. Par exemple, un compilateur peut le remplacer par une fonction d'alimentation interne à la fois plus rapide et plus précise. Ceci est particulièrement intéressant car le comportement est partiellement contrôlé par le programmeur directement dans le code source, alors que les options du compilateur fournies par l'utilisateur final peuvent parfois être utilisées de manière incorrecte.

L'état par défaut du pragma FP_CONTRACT est défini par l'implémentation, de sorte qu'un compilateur est autorisé à effectuer ces optimisations par défaut. Ainsi, le code portable qui doit suivre strictement les règles IEEE 754 doit explicitement le définir sur OFF.

Si un compilateur ne supporte pas ce pragma, il doit être prudent en évitant une telle optimisation, au cas où le développeur aurait choisi de le définir sur OFF.

GCC ne supporte pas ce pragma, mais avec les options par défaut, il suppose qu'il s'agit de ON; ainsi pour les cibles avec un FMA matériel, si on veut empêcher la transformation a*b+c en fma (a, b, c), il faut fournir une option telle que -ffp-contract=off (pour définir explicitement le pragma sur OFF) ou -std=c99 (pour indiquer à GCC de se conformer à une version standard C, ici C99, suivez donc le paragraphe ci-dessus). Dans le passé, cette dernière option n’empêchait pas la transformation, ce qui signifie que GCC n’était pas conforme sur ce point: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845

32
vinc17

Je n'aurais pas pensé que ce cas soit optimisé du tout. Il est rare qu’une expression contienne des sous-expressions pouvant être regroupées pour supprimer des opérations entières. Je m'attendrais à ce que les rédacteurs de compilateurs investissent leur temps dans des domaines susceptibles de conduire à des améliorations notables, plutôt que de couvrir un cas Edge rarement rencontré.

J'ai été surpris d'apprendre par les autres réponses que cette expression pouvait effectivement être optimisée avec les commutateurs de compilation appropriés. L’optimisation est soit triviale, soit il s’agit d’une optimisation beaucoup plus courante de Edge, soit les rédacteurs du compilateur ont été extrêmement complets.

Il n'y a rien de mal à fournir des astuces au compilateur, comme vous l'avez fait ici. Il est normal et prévisible que le processus de micro-optimisation permette de réorganiser les instructions afin de déterminer les différences qu’elles apporteront.

Bien que le compilateur puisse justifier que les deux expressions produisent des résultats incohérents (sans les commutateurs appropriés), vous n'êtes pas obligé d'être lié par cette restriction. La différence sera incroyablement minime - à tel point que si la différence vous importe, vous ne devriez pas utiliser d'arithmétique en virgule flottante standard.

28
Mark Ransom

Comme Lambdageek l'a souligné, la multiplication flottante n'est pas associative et vous pouvez obtenir moins de précision, mais lorsque vous obtenez une meilleure précision, vous pouvez vous opposer à l'optimisation, car vous souhaitez une application déterministe. Par exemple, dans la simulation de jeu client/serveur, où chaque client doit simuler le même monde pour lequel les calculs en virgule flottante doivent être déterministes.

28
Bjorn

Les fonctions de bibliothèque telles que "pow" sont généralement soigneusement conçues pour générer le minimum d'erreur possible (en cas générique). Ceci est généralement réalisé en approximant des fonctions avec des splines (selon le commentaire de Pascal, l'implémentation la plus courante semble utiliser algorithme de Remez )

fondamentalement l'opération suivante:

pow(x,y);

a une erreur inhérente d'approximativement la même même ampleur que l'erreur de toute multiplication ou division .

Alors que l'opération suivante:

float a=someValue;
float b=a*a*a*a*a*a;

a une erreur inhérente supérieure à 5 fois l'erreur d'une multiplication simple ou d'une division (parce que vous combinez 5 multiplications).

Le compilateur devrait être très attentif au type d'optimisation qu'il effectue:

  1. si vous optimisez pow(a,6) sur a*a*a*a*a*a, il peut améliorer les performances, mais réduit considérablement la précision des nombres à virgule flottante.
  2. si vous optimisez a*a*a*a*a*a en pow(a,6), la précision peut en être réduite car "a" était une valeur spéciale permettant une multiplication sans erreur (une puissance de 2 ou un petit nombre entier)
  3. si vous optimisez pow(a,6) sur (a*a*a)*(a*a*a) ou (a*a)*(a*a)*(a*a), il peut toujours y avoir une perte de précision par rapport à la fonction pow.

En général, vous savez que pour des valeurs à virgule flottante arbitraires, "pow" a une meilleure précision que toute fonction que vous pourriez éventuellement écrire, mais dans certains cas particuliers, la multiplication multiple peut avoir une précision et des performances meilleures, il appartient au développeur de choisir celle qui convient le mieux éventuellement commenter le code de sorte que personne ne "optimise" ce code.

La seule chose qui ait du sens (opinion personnelle, et apparemment un choix dans GCC sans optimisation particulière ou indicateur de compilation) à optimiser, devrait être de remplacer "pow (a, 2)" par "a * a". Ce serait la seule chose saine qu'un vendeur de compilateur devrait faire.

27
GameDeveloper

Il y a déjà quelques bonnes réponses à cette question, mais pour des raisons d'exhaustivité, je voudrais souligner que la section applicable de la norme C est 5.1.2.2.3/15 (ce qui correspond à la section 1.9/9 de la C++ 11 standard). Cette section indique que les opérateurs ne peuvent être regroupés que s'ils sont réellement associatifs ou commutatifs.

21
Rastaban

gcc peut réellement faire cette optimisation, même pour les nombres à virgule flottante. Par exemple,

double foo(double a) {
  return a*a*a*a*a*a;
}

devient

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

avec -O -funsafe-math-optimizations. Cette réorganisation enfreint IEEE-754, cependant, elle nécessite le drapeau.

Comme Peter Cordes l'a souligné dans un commentaire, les entiers signés peuvent effectuer cette optimisation sans -funsafe-math-optimizations, car il existe exactement quand il n'y a pas de dépassement de capacité et en cas de dépassement, vous obtenez un comportement indéfini. Donc vous obtenez

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

avec juste -O. Pour les entiers non signés, c'est encore plus facile, car ils travaillent avec une puissance de 2 et peuvent donc être réorganisés librement, même en cas de débordement.

12
Charles