visual-c++ telecharger - Pourquoi les compilateurs C++modernes n'optimisent-ils pas les boucles simples comme celle-ci?(Clang, MSVC)




gcc (5)

Lorsque je compile et exécute ce code avec Clang ( -O3 ) ou MSVC ( /O2 ) ...

#include <stdio.h>
#include <time.h>

static int const N = 0x8000;

int main()
{
    clock_t const start = clock();
    for (int i = 0; i < N; ++i)
    {
        int a[N];    // Never used outside of this block, but not optimized away
        for (int j = 0; j < N; ++j)
        {
            ++a[j];  // This is undefined behavior (due to possible
                     // signed integer overflow), but Clang doesn't see it
        }
    }
    clock_t const finish = clock();
    fprintf(stderr, "%u ms\n",
        static_cast<unsigned int>((finish - start) * 1000 / CLOCKS_PER_SEC));
    return 0;
}

... la boucle n'est pas optimisée.

De plus, ni Clang 3.6, ni Visual C ++ 2013, ni GCC 4.8.1 ne m'indiquent que la variable n'est pas initialisée!

Maintenant, je me rends compte que l'absence d'une optimisation n'est pas un bug en soi, mais je trouve cela étonnant étant donné que les compilateurs sont censés être assez intelligents de nos jours. Cela semble être un morceau de code si simple que même les techniques d’analyse d’activité il ya dix ans devraient permettre d’optimiser la variable a et donc toute la boucle - sans oublier le fait que l’incrémentation de la variable est déjà un comportement indéfini.

Pourtant, seul GCC est capable de déterminer qu'il s'agit d'une opération négative, et aucun des compilateurs ne me dit qu'il s'agit d'une variable non initialisée.

Pourquoi est-ce? Qu'est-ce qui empêche une analyse simple de l'activité physique de dire au compilateur qu'un fichier est inutilisé? De plus, pourquoi le compilateur ne détecte-t-il pas a[j] n'est pas initialisé en premier lieu? Pourquoi les détecteurs de variables non initialisées existants dans tous ces compilateurs ne peuvent-ils pas détecter cette erreur évidente?


Answers

C'est en effet très intéressant. J'ai essayé votre exemple avec MSVC 2013. Ma première idée était que le fait que ++ a [j] soit quelque peu indéfini est la raison pour laquelle la boucle n'est pas supprimée, car sa suppression changerait définitivement la signification du programme à partir d'un non défini / sémantique incorrecte à quelque chose de significatif, alors j'ai essayé d'initialiser les valeurs avant, mais les boucles ne disparaissent toujours pas.

Après j'ai remplacé le ++ a [j]; avec a [j] = 0; qui produisait alors une sortie sans aucune boucle, de sorte que tout entre les deux appels à clock () était supprimé. Je ne peux que deviner la raison. Peut-être que l'optimiseur n'est pas en mesure de prouver que l'opérateur ++ n'a aucun effet secondaire pour une raison quelconque.


++a[j];  // This is undefined behavior too, but Clang doesn't see it

Voulez-vous dire que ce comportement est indéfini car les éléments du tableau ne sont pas initialisés?

Si oui, bien que ce soit une interprétation commune de la clause 4.1 / 1 de la norme, je pense qu’elle est incorrecte. Les éléments sont «non initialisés» en ce sens que les programmeurs utilisent généralement ce terme, mais je ne pense pas que cela corresponde exactement à l'utilisation du terme par la spécification C ++.

En particulier, C ++ 11 8.5 / 11 indique que ces objets sont en fait initialisés par défaut, ce qui me semble mutuellement exclusif d'être non initialisé. La norme stipule également que l'initialisation de certains objets par défaut signifie que «aucune initialisation n'est effectuée». Certains pourraient penser que cela signifie qu'ils ne sont pas initialisés, mais cela n'est pas spécifié et je pense simplement que cela signifie qu'aucune performance de ce type n'est requise.

La spécification montre clairement que les éléments du tableau auront des valeurs indéterminées. C ++ spécifie, par référence au standard C, que les valeurs indéterminées peuvent être soit des représentations valides, légales pour accéder normalement, soit des représentations de pièges. Si les valeurs indéterminées particulières des éléments du tableau sont toutes des représentations valides (et si aucune n'est INT_MAX, en évitant le débordement), la ligne ci-dessus ne déclenche aucun comportement indéfini en C ++ 11.

Étant donné que ces éléments de tableau pourraient être des représentations de pièges, il serait parfaitement conforme que clang agisse comme s'il s'agissait de représentations de pièges, choisissant effectivement de créer le code UB afin de créer une opportunité d'optimisation.

Même si Clang ne le fait pas, il pourrait toujours choisir d'optimiser en fonction du flux de données. Clang sait comment faire, comme le montre le fait que si la boucle interne est légèrement modifiée, les boucles sont supprimées.

Alors, pourquoi la présence (optionnelle) d'UB semble-t-elle contrecarrer l'optimisation, lorsque UB est généralement considéré comme une opportunité d'optimisation?

Ce qui se passe, c'est que Clang a décidé que les utilisateurs veulent un int piégeage basé sur le comportement du matériel. Ainsi, plutôt que de prendre des pièges comme une opportunité d'optimisation, Clang doit générer du code qui reproduit fidèlement le comportement du programme dans le matériel. Cela signifie que les boucles ne peuvent pas être éliminées en fonction du flux de données, car cela pourrait éliminer les pièges matériels.

C ++ 14 met à jour le comportement tel que l'accès à des valeurs indéterminées produit un comportement indéfini, indépendamment du fait que l'on considère la variable non initialisée ou non: https://.com/a/23415662/365496


Le comportement non défini n'est pas pertinent ici. Remplacer la boucle interne par:

    for (int j = 1; j < N; ++j)
    {
        a[j-1] = a[j];
        a[j] = j;
    }

... a le même effet, du moins avec Clang.

Le problème est que la boucle interne se charge à la fois de a[j] (pour certains j ) et stocke à a[j] (pour certains j ). Aucun des magasins ne peut être supprimé, car le compilateur pense qu'ils peuvent être visibles aux chargements ultérieurs, et aucun des chargements ne peut être supprimé, car leurs valeurs sont utilisées (en entrée des magasins ultérieurs). En conséquence, la boucle a toujours des effets secondaires sur la mémoire, de sorte que le compilateur ne voit pas qu'il peut être supprimé.

Contrairement à la réponse de nm, remplacer int par unsigned ne fait pas disparaître le problème. Le code généré par Clang 3.4.1 utilisant int et using unsigned int est identique.


C'est une question intéressante en ce qui concerne l'optimisation. Je m'attendrais à ce que dans la plupart des cas, le compilateur traite chaque élément du tableau comme une variable individuelle lors de l'analyse du code mort. Ans 0x8000 fait trop de variables individuelles à suivre, donc le compilateur n'essaie pas. Le fait qu'un a[j] n'accède pas toujours au même objet peut également poser problème à l'optimiseur.

De toute évidence, différents compilateurs utilisent des heuristiques différentes; un compilateur pourrait traiter le tableau comme un objet unique et détecter qu'il n'a jamais affecté la sortie (comportement observable). Certains compilateurs peuvent choisir de ne pas, cependant, parce que, en général, cela demande beaucoup de travail pour très peu de gain: à quelle fréquence ces optimisations seraient-elles applicables au code réel?


Cela peut avoir un impact négatif sur les performances du programme en raison des considérations de cohérence de la mémoire cache . Ecrire pour flag chaque fois que func() est appelé salirait la ligne de cache contenant. Cela se produit indépendamment du fait que la valeur en cours d'écriture correspond exactement aux bits trouvés à l'adresse de destination avant l'écriture.

MODIFIER

hvd a fourni une autre bonne raison qui empêche une telle optimisation. C'est un argument plus convaincant contre l'optimisation proposée, car il peut en résulter un comportement indéfini, alors que ma réponse (originale) ne traitait que des aspects de performance.

Après un peu plus de réflexion, je peux proposer un autre exemple de pourquoi les compilateurs devraient être fortement bannis - à moins qu'ils ne puissent prouver que la transformation est sûre pour un contexte particulier - d'introduire l'écriture inconditionnelle. Considérez ce code:

const bool foo = true;

int main()
{
    func(const_cast<bool&>(foo));
}

Avec une écriture inconditionnelle dans func() cela déclenche définitivement un comportement indéfini (l'écriture dans la mémoire en lecture seule terminera le programme, même si l'effet de l'écriture serait autrement une opération non-op).





c++ visual-c++ gcc clang compiler-optimization