2299 votes

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

Je fais de l'optimisation numérique sur une application scientifique. Une chose que j'ai remarquée est que GCC optimise l'appel pow(a,2) en le compilant en a*a mais l'appel pow(a,6) n'est pas optimisé et appellera en fait la fonction de la bibliothèque pow ce qui ralentit considérablement les performances. (En revanche, Compilateur Intel C++ , exécutable icc éliminera l'appel à la bibliothèque pour pow(a,6) .)

Ce qui m'intrigue, c'est que lorsque j'ai remplacé pow(a,6) con a*a*a*a*a*a en utilisant GCC 4.5.1 et les options " -O3 -lm -funroll-loops -msse4 ", il utilise 5 mulsd des instructions :

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) il 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 ?

16 votes

Que signifie "reconnaître pow(a,6)" ?

1 votes

Je suis surpris gcc fait no optimiser cela. Le compilateur FORTRAN des années 1970 que j'ai utilisé sur CDC Cyber faisait ce genre de transformation, même sans sélectionner d'optimisation. Je pense que la V6 d'Unix (c. 1978) C le compilateur le faisait lorsque l'optimisation était activée, bien que la plupart des optimisations qu'il effectuait visaient à économiser de l'espace dans le code, une denrée précieuse à l'époque.

707 votes

Um... vous savez qu'un a a a a a et (a a a)*(a a*a) ne sont pas les mêmes avec les nombres à virgule flottante, n'est-ce pas ? Vous devrez utiliser -funsafe-math ou -ffast-math ou autre pour cela.

2908voto

Lambdageek Points 10928

Parce que Les mathématiques à virgule flottante ne sont pas associatives . La façon dont vous regroupez les opérandes dans une multiplication en virgule flottante a un effet sur la précision numérique de la réponse.

Par conséquent, la plupart des compilateurs sont très prudents lorsqu'il s'agit de réorganiser des calculs en virgule flottante, sauf s'ils sont sûrs que la réponse restera la même ou si vous leur dites que vous ne vous souciez pas de la précision numérique. Par exemple : le site -fassociative-math option de gcc qui permet à gcc de réassocier les opérations en virgule flottante, ou encore la fonction -ffast-math qui permet des compromis encore plus agressifs entre la précision et la vitesse.

16 votes

Oui. Avec -ffast-math il fait une telle optimisation. Bonne idée ! Mais comme notre code concerne plus la précision que la vitesse, il est préférable de ne pas le passer.

25 votes

IIRC C99 permet au compilateur de faire de telles optimisations FP "non sûres", mais GCC (sur tout autre système que le x87) fait une tentative raisonnable de suivre IEEE 754 - ce n'est pas "error bounds" ; il n'y a qu'une seule réponse correcte .

16 votes

Les détails de la mise en œuvre de pow ne sont ni ici ni là ; cette réponse ne fait même pas référence à pow .

709voto

Stephen Canon Points 58003

Lambdageek souligne à juste titre qu'étant donné que l'associativité n'est pas valable pour les nombres à virgule flottante, l'"optimisation" de l'option a*a*a*a*a*a a (a*a*a)*(a*a*a) peut modifier la valeur. C'est pourquoi il est interdit par C99 (sauf si l'utilisateur l'autorise spécifiquement, via un drapeau de compilation ou un pragma). En général, l'hypothèse est que le programmeur a écrit ce qu'il a fait pour une raison, et le compilateur devrait respecter cela. Si vous voulez (a*a*a)*(a*a*a) écrivez cela.

Mais cela peut être pénible à écrire ; pourquoi le compilateur ne peut-il pas simplement faire [ce que vous considérez comme] la bonne chose quand vous utilisez pow(a,6) ? Parce que ce serait le mauvais à faire. Sur une plate-forme avec une bonne bibliothèque de mathématiques, pow(a,6) est significativement plus précis que l'un ou l'autre a*a*a*a*a*a o (a*a*a)*(a*a*a) . Juste pour fournir quelques données, j'ai fait une petite expérience sur mon Mac Pro, en mesurant la pire erreur dans l'évaluation de a^6 pour tous les nombres flottants de simple précision 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

Utilisation de pow au lieu d'un arbre de multiplication réduit la limite d'erreur d'une facteur 4 . Les compilateurs ne devraient pas (et ne le font généralement pas) faire des "optimisations" qui augmentent le taux d'erreur, à moins que l'utilisateur n'y soit autorisé par l'utilisateur (par exemple, par l'intermédiaire de -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 cette méthode si vous voulez sacrifier la précision aux performances, mais que vous ne voulez pas activer fast-math.

33 votes

Notez également que Visual C++ fournit une version "améliorée" de pow(). En appelant _set_SSE2_enable(<flag>) con flag=1 il utilisera SSE2 si possible. Cela réduit un peu la précision, mais améliore la vitesse (dans certains cas). MSDN : _set_SSE2_enable() y pow()

0 votes

@xis19 : Avec une bonne bibliothèque mathématique, la même chose vaut pour le double (et en fait, pour tout type de virgule flottante supporté).

0 votes

@TkTech : l'utilisation de SSE2 ne doit pas nécessairement réduire la précision, même. La plupart des bibliothèques mathématiques modernes sur x86 utilisent SSE quand il est disponible, et beaucoup d'entre elles fournissent des résultats très précis.

193voto

SCombinator Points 1347

Un autre cas similaire : la plupart des compilateurs n'optimisent pas a + b + c + d a (a + b) + (c + d) (il s'agit d'une optimisation puisque la deuxième expression peut être mieux pipelinée) et l'évaluer comme indiqué (c'est-à-dire comme (((a + b) + c) + d) ). Cela aussi est dû à des cas particuliers :

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

Ces sorties 1.000000e-05 0.000000e+00

14 votes

Ce n'est pas exactement la même chose. Changer l'ordre des multiplications/divisions (à l'exception de la division par 0) est plus sûr que changer l'ordre des sommes/soustractions. A mon humble avis, le compilateur devrait essayer d'associer les multiplications/divisions car cela réduit le nombre total d'opérations et en plus du gain de performance, il y a aussi un gain de précision.

10 votes

@DarioOO : Ce n'est pas plus sûr. Multiplier et diviser sont les mêmes que l'addition et la soustraction de l'exposant, et changer l'ordre peut facilement faire dépasser aux temporaires la plage possible de l'exposant. (Pas exactement la même chose, parce que l'exposant ne souffre pas de perte de précision... mais la représentation est encore assez limitée, et le réordonnancement peut conduire à des valeurs non représentables).

13 votes

Je pense qu'il vous manque des connaissances en calcul. La multiplication et la division de deux nombres introduisent la même quantité d'erreur. Alors que la soustraction ou l'addition de deux nombres peut introduire une erreur plus importante, en particulier lorsque les deux nombres sont d'un ordre de grandeur différent. Il est donc plus sûr de réorganiser la division et la division que l'addition et la soustraction, car cela n'introduit qu'un changement mineur dans l'erreur finale.

89voto

Szabolcs Points 12411

Fortran (conçu pour le calcul scientifique) possède un opérateur de puissance intégré et, pour autant que je sache, les compilateurs Fortran optimisent généralement l'élévation à la puissance des entiers d'une manière similaire à celle que vous décrivez. Malheureusement, le C/C++ ne dispose pas d'opérateur de puissance, mais seulement de la fonction de bibliothèque pow() . Cela n'empêche pas des compilateurs intelligents de traiter pow spécialement et le calculer de manière plus rapide pour des cas particuliers, mais il semble qu'ils le fassent moins souvent ...

Il y a quelques années, j'ai essayé de rendre plus pratique le calcul des puissances d'entiers de manière optimale, et j'ai trouvé ce qui suit. C'est du C++, pas du C cependant, et cela dépend toujours de l'intelligence du compilateur quant à l'optimisation/inline des choses. Quoi qu'il en soit, j'espère que vous le trouverez 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);
}

<strong>Clarification pour les curieux : </strong>cela ne permet pas de trouver la manière optimale de calculer les puissances, mais puisque <a href="http://en.wikipedia.org/wiki/Addition-chain_exponentiation">trouver la solution optimale est un problème NP-complet </a>et cela ne vaut la peine d'être fait que pour les petites puissances de toute façon (contrairement à l'utilisation des <code>pow</code> ), il n'y a aucune raison de s'attarder sur les détails.

Ensuite, il suffit de l'utiliser comme power<6>(a) .

Cela permet de taper facilement les pouvoirs (pas besoin d'épeler 6 a avec des parenthèses), et vous permet d'avoir ce type d'optimisation sans -ffast-math au cas où vous auriez quelque chose qui dépende de la précision, comme 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 se compile avec un compilateur C++).

J'espère que cela pourra vous être utile.

EDITAR:

Voici ce que j'obtiens de mon compilateur :

Para a*a*a*a*a*a ,

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

Para (a*a*a)*(a*a*a) ,

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

Para power<6>(a) ,

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

37 votes

Trouver l'arbre de puissance optimal peut être difficile, mais puisque ce n'est intéressant que pour les petites puissances, la réponse évidente est de le précalculer une fois (Knuth fournit une table jusqu'à 100) et d'utiliser cette table codée en dur (c'est ce que gcc fait en interne pour powi).

7 votes

Sur les processeurs modernes, la vitesse est limitée par la latence. Par exemple, le résultat d'une multiplication peut être disponible après cinq cycles. Dans cette situation, trouver le moyen le plus rapide de créer de la puissance peut s'avérer plus délicat.

3 votes

Vous pouvez également essayer de trouver l'arbre de puissance qui donne la limite supérieure la plus basse pour l'erreur d'arrondi relative, ou l'erreur d'arrondi relative moyenne la plus basse.

51voto

Atom Points 8739

Parce qu'un nombre à virgule flottante de 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 représente la multiplication de nombres de précision arbitraire sans qu'il y ait d'erreurs liées à ces nombres. La raison pour laquelle certaines personnes ne s'en rendent pas compte est peut-être liée aux calculs mathématiques qu'elles ont effectués à l'école primaire : elles ne travaillent qu'avec des nombres idéaux sans erreur et croient qu'il est normal d'ignorer simplement "e" lors d'une multiplication. Ils ne voient pas le "e" implicite dans "float a=1.2", "a*a*a" et autres codes C similaires.

Si la majorité des programmeurs reconnaissaient (et étaient capables d'exécuter) l'idée que l'expression C a*a*a*a*a*a*a ne fonctionne pas réellement avec des nombres idéaux, le compilateur GCC serait alors LIBRE d'optimiser "a*a*a*a*a*a" en disant "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. Et donc GCC ne fera que ce à quoi le code source ressemble - parce que c'est ce que GCC voit avec son "œil nu".

... une fois que vous savez quel genre de programmeur usted sont, vous pouvez utiliser le commutateur "-ffast-math" pour dire à GCC que "Hey, GCC, je sais ce que je fais !". Cela permettra au GCC de convertir a*a*a*a*a*a en un texte différent - il a l'air différent de a*a*a*a*a - mais calcule toujours un nombre dans l'intervalle d'erreur de a*a*a*a*a*a. Ce n'est pas grave, puisque vous savez déjà que vous travaillez avec des intervalles et non des nombres idéaux.

66 votes

Les nombres à virgule flottante sont exacts. Ils ne sont simplement pas nécessairement exacts. De plus, la technique de l'epsilon est elle-même une approximation de la manière d'aborder les choses dans la réalité, car la véritable erreur attendue est relative à l'échelle de la mantisse, c'est-à-dire que vous êtes normalement à environ 1 LSB près, mais cela peut augmenter à chaque opération effectuée si vous ne faites pas attention. Utilisez une bibliothèque appropriée si vous le pouvez.

4 votes

@DonalFellows : La norme IEEE exige que les calculs en virgule flottante donnent le résultat qui correspond le plus précisément à ce que serait le résultat si les opérandes sources étaient des valeurs exactes, mais cela ne signifie pas qu'ils soient réellement représenter valeurs exactes. Dans de nombreux cas, il est plus utile de considérer 0,1f comme étant (1 677 722 +/- 0,5)/16 777 216, qui doit être affiché avec le nombre de chiffres décimaux impliqué par cette incertitude, que de le considérer comme la quantité exacte (1 677 722 +/- 0,5)/16 777 216 (qui doit être affichée avec 24 chiffres décimaux).

31 votes

@supercat : IEEE-754 est assez clair sur le fait que les données à virgule flottante hacer représentent des valeurs exactes ; les clauses 3.2 à 3.4 sont les sections pertinentes. Vous pouvez, bien sûr, choisir de les interpréter autrement, tout comme vous pouvez choisir d'interpréter les clauses 3.2 à 3.4. int x = 3 comme signifiant que x est de 3+/-0,5.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X