mathématiques associatives avec GCC

J’ai créé un type de données double-double en C. J’ai essayé -Ofast avec GCC et découvert qu’il est considérablement plus rapide (par exemple 1,5 s avec -O3 et 0,3 s avec -Ofast ), mais les résultats sont faux. J’ai pourchassé ceci jusqu’à -fassociative-math . Je suis surpris que cela ne fonctionne pas car je définis explicitement l’associativité de mes opérations lorsque c’est important. Par exemple, dans le code suivant, je mets entre parenthèses le cas échéant.

 static inline doublefloat two_sum(const float a, const float b) { float s = a + b; float v = s - a; float e = (a - (s - v)) + (b - v); return (doublefloat){s, e}; } 

Donc, je ne m’attends pas à ce que GCC change, par exemple, (a - (s - v)) en ((a + v) - s) même avec -fassociative-math . Alors, pourquoi les résultats sont-ils si faux en utilisant -fassociative-math (et tellement plus rapidement)?

J’ai essayé /fp:fast avec MSVC (après avoir converti mon code en C ++) et les résultats sont corrects mais ce n’est pas plus rapide que /fp:precise .

Dans le manuel de GCC en ce qui concerne -fassociative-math il est indiqué

Autoriser la réassociation d’opérandes dans une série d’opérations en virgule flottante. Cela enfreint les normes de langage ISO C et C ++ en modifiant éventuellement le résultat du calcul. REMARQUE: la réorganisation peut changer le signe de zéro, ignorer les NaN et inhiber ou créer un dépassement inférieur ou supérieur (elle ne peut donc pas être utilisée avec du code reposant sur un comportement d’arrondi tel que “(x + 2 ^ 52) – 2 ^ 52” Peut également réorganiser les comparaisons en virgule flottante et ne peut donc pas être utilisé lorsque des comparaisons ordonnées sont requirejses.Cette option nécessite que les options -fno-signed-zeros et -fno-trapping-math soient en vigueur. sens avec -frounding-math.

Modifier:

J’ai fait quelques tests avec des entiers (signé et non signé) et float pour vérifier si GCC simplifie les opérations associatives. Voici le code que j’ai testé

 //test1.c unsigned foosu(unsigned a, unsigned b, unsigned c) { return (a + c) - b; } signed fooss(signed a, signed b, signed c) { return (a + c) - b; } float foosf(float a, float b, float c) { return (a + c) - b; } unsigned foomu(unsigned a, unsigned b, unsigned c) { return a*a*a*a*a*a; } signed fooms(signed a, signed b, signed c) { return a*a*a*a*a*a; } float foomf(float a, float b, float c) { return a*a*a*a*a*a; } 

et

 //test2.c unsigned foosu(unsigned a, unsigned b, unsigned c) { return a - (b - c); } signed fooss(signed a, signed b, signed c) { return a - (b - c); } float foosf(float a, float b, float c) { return a - (b - c); } unsigned foomu(unsigned a, unsigned b, unsigned c) { return (a*a*a)*(a*a*a); } signed fooms(signed a, signed b, signed c) { return (a*a*a)*(a*a*a); } float foomf(float a, float b, float c) { return (a*a*a)*(a*a*a); } 

Je me suis conformé à -O3 et -Ofast et j’ai regardé l’assemblage généré et c’est ce que j’ai observé

  • unsigned: le code était identique pour l’addition et la multiplication (réduit à trois multiplications)
  • signé: le code n’était pas identique pour l’addition, mais pour la multiplication (réduit à trois multiplications)
  • float: le code n’était pas identique pour l’addition ou la multiplication avec -O3 cependant avec -Ofast l’addition était identique et la multiplication était presque la même avec trois multiplications seulement.

J’en conclus que

  • si une opération est associative, alors GCC la simplifiera comme elle le souhaite pour que a - (b - c) puisse devenir (a + c) - b .
  • l’addition et la multiplication non signées sont associatives
  • l’ajout signé n’est pas associatif
  • la multiplication signée est associative
  • a*a*a*a*a*a est simplifié à trois multiplications seulement pour les entiers et pour les nombres à virgule flottante avec -fassociative-math .
  • -fassociative-math amène l’addition et la multiplication à virgule flottante à être associatives.

En d’autres termes, GCC a fait exactement ce que je ne m’attendais pas à voir avec -fassociative-math . Il convertit (a - (s - v)) en ((a + v) - s) .

On peut penser que cela est évident avec -fassociative-math mais il -fassociative-math parfois qu’un programmeur veuille que le point flottant soit associatif dans un cas et non associatif dans un autre cas. Par exemple, la vectorisation automatique et la réduction d’un tableau à virgule flottante nécessitent l’ -fassociative-math mais si cela est fait, le double flottant ne peut pas être utilisé dans le même module. Ainsi, la seule option est de placer les fonctions de virgule flottante associatives dans un module et les fonctions de virgule flottante non associatives dans un autre module et de les comstackr dans des fichiers object distincts.

Je suis surpris que cela ne fonctionne pas car je définis explicitement l’associativité de mes opérations lorsque c’est important. Par exemple, dans le code suivant, je mets entre parenthèses le cas échéant.

C’est exactement ce que fait -fassociative-math : il ignore l’ordre défini par votre programme (qui est tout aussi défini sans les parenthèses) et fait ce qui permet plutôt des simplifications. En règle générale, pour une double addition double, le terme d’erreur est calculé à 0, car il correspond à ce qu’il serait égal à si les opérations à virgule flottante étaient associatives. e = 0; est beaucoup plus rapide que e = (a - …; mais bien sûr, c’est tout simplement faux.

Dans la norme C99, la règle de grammaire suivante dans 6.5.6: 1 implique que x + y + z ne peuvent être analysés que comme (x + y) + z :

 expression additive:
          expression multiplicative
          expression additive + expression multiplicative
          expression additive - expression multiplicative

Les parenthèses explicites et les assignations à des valeurs intermédiaires n’empêchent pas -fassociative-math de faire son travail. L’ordre était défini même sans eux (de gauche à droite dans le cas d’une séquence d’additions et de soustractions) et vous avez dit au compilateur d’ignorer l’ordre défini. En fait, sur la représentation intermédiaire à laquelle l’optimisation est appliquée, je doute que l’information rest, que l’ordre soit imposé par des assignations intermédiaires, des parenthèses ou la grammaire.

Vous pouvez essayer de placer toutes les fonctions que vous souhaitez comstackr avec l’ordre imposé par le standard C dans la même unité de compilation que vous comstackriez sans -fassociative-math , ou d’éviter complètement cet indicateur pour l’ensemble du programme. Si vous insistez pour laisser le double-double addition dans une unité de compilation compilée avec -fassociative-math , vous pouvez essayer de jouer avec volatile variables volatile , mais le qualificateur de type volatile ne fait que rendre l’access à lvalue un événement observable. le calcul doit avoir lieu.