Supposons que je dispose d'un certain nombre d'instructions que je souhaite exécuter dans un ordre fixe. Je veux utiliser g ++ avec le niveau d'optimisation 2, donc certaines instructions pourraient être réorganisées. De quels outils dispose-t-on pour imposer un certain ordre des déclarations?
Prenons l'exemple suivant.
using Clock = std::chrono::high_resolution_clock;
auto t1 = Clock::now(); // Statement 1
foo(); // Statement 2
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
Dans cet exemple, il est important que les instructions 1-3 soient exécutées dans l'ordre donné. Cependant, le compilateur ne peut-il pas penser que l'instruction 2 est indépendante de 1 et 3 et exécuter le code comme suit?
using Clock=std::chrono::high_resolution_clock;
foo(); // Statement 2
auto t1 = Clock::now(); // Statement 1
auto t2 = Clock::now(); // Statement 3
auto elapsedTime = t2 - t1;
c++
c++11
operator-precedence
S2108887
la source
la source
__sync_synchronize()
être utile?foo
d'exécution, que le compilateur est autorisé à ignorer lors de la réorganisation, tout comme il est autorisé à ignorer l'observation d'un thread différent.Réponses:
Je voudrais essayer de fournir une réponse un peu plus complète après que cela ait été discuté avec le comité des normes C ++. En plus d'être membre du comité C ++, je suis également développeur sur les compilateurs LLVM et Clang.
Fondamentalement, il n'y a aucun moyen d'utiliser une barrière ou une opération dans la séquence pour réaliser ces transformations. Le problème fondamental est que la sémantique opérationnelle de quelque chose comme une addition d'entiers est totalement connue de l'implémentation. Il peut les simuler, il sait qu'ils ne peuvent pas être observés par des programmes corrects et est toujours libre de les déplacer.
Nous pourrions essayer d'éviter cela, mais cela aurait des résultats extrêmement négatifs et finirait par échouer.
Tout d'abord, la seule façon d'éviter cela dans le compilateur est de lui dire que toutes ces opérations de base sont observables. Le problème est que cela empêcherait alors l'écrasante majorité des optimisations du compilateur. À l'intérieur du compilateur, nous n'avons pratiquement aucun bon mécanisme pour modéliser que le timing est observable mais rien d'autre. Nous n'avons même pas un bon modèle de ce que les opérations prennent du temps . Par exemple, la conversion d'un entier non signé de 32 bits en un entier non signé de 64 bits prend du temps? Cela ne prend aucun temps sur x86-64, mais sur d'autres architectures, cela prend un temps différent de zéro. Il n'y a pas de réponse générique correcte ici.
Mais même si nous réussissons grâce à certains actes héroïques à empêcher le compilateur de réorganiser ces opérations, rien ne garantit que cela suffira. Considérez un moyen valide et conforme d'exécuter votre programme C ++ sur une machine x86: DynamoRIO. Il s'agit d'un système qui évalue dynamiquement le code machine du programme. Une chose qu'il peut faire est des optimisations en ligne, et il est même capable d'exécuter de manière spéculative toute la gamme d'instructions arithmétiques de base en dehors du timing. Et ce comportement n'est pas unique aux évaluateurs dynamiques, le processeur x86 réel spéculera également (un nombre beaucoup plus petit) d'instructions et les réorganisera dynamiquement.
La réalisation essentielle est que le fait que l'arithmétique ne soit pas observable (même au niveau de la synchronisation) est quelque chose qui imprègne les couches de l'ordinateur. C'est vrai pour le compilateur, le runtime et souvent même le matériel. Le forcer à être observable contraindrait à la fois considérablement le compilateur, mais cela contraindrait également considérablement le matériel.
Mais tout cela ne doit pas vous faire perdre espoir. Lorsque vous souhaitez chronométrer l'exécution d'opérations mathématiques de base, nous avons des techniques bien étudiées qui fonctionnent de manière fiable. Ils sont généralement utilisés lors du micro-benchmarking . J'ai donné une conférence à ce sujet à CppCon2015: https://youtu.be/nXaxk27zwlk
Les techniques présentées ici sont également fournies par diverses bibliothèques de micro-benchmark telles que celles de Google: https://github.com/google/benchmark#preventing-optimization
La clé de ces techniques est de se concentrer sur les données. Vous rendez l'entrée du calcul opaque pour l'optimiseur et le résultat du calcul opaque pour l'optimiseur. Une fois que vous avez fait cela, vous pouvez le chronométrer de manière fiable. Regardons une version réaliste de l'exemple dans la question d'origine, mais avec la définition de
foo
entièrement visible pour l'implémentation. J'ai également extrait une version (non portable) deDoNotOptimize
de la bibliothèque Google Benchmark que vous pouvez trouver ici: https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h#L208Ici, nous nous assurons que les données d'entrée et les données de sortie sont marquées comme non optimisables autour du calcul
foo
, et seulement autour de ces marqueurs sont les timings calculés. Parce que vous utilisez des données pour pincer le calcul, il est garanti qu'il restera entre les deux synchronisations et pourtant le calcul lui-même peut être optimisé. L'assembly x86-64 résultant généré par une version récente de Clang / LLVM est:Ici, vous pouvez voir le compilateur optimiser l'appel à
foo(input)
une seule instructionaddl %eax, %eax
, mais sans le déplacer en dehors du timing ni l'éliminer complètement malgré l'entrée constante.J'espère que cela vous aidera, et le comité des normes C ++ étudie la possibilité de standardiser des API similaires à
DoNotOptimize
ici.la source
Clock::now()
être réorganisés par rapport à foo ()? L'optimzer doit-il supposer celaDoNotOptimize
etClock::now()
avoir accès à et pourrait modifier un état global commun qui à son tour les lierait à l'entrée et à la sortie? Ou comptez-vous sur certaines limitations actuelles de l'implémentation de l'optimiseur?DoNotOptimize
dans cet exemple est un événement synthétiquement "observable". C'est comme s'il imprimait théoriquement la sortie visible sur un terminal avec la représentation de l'entrée. Puisque la lecture de l'horloge est également observable (vous observez le temps qui passe), elles ne peuvent pas être réordonnées sans changer le comportement observable du programme.foo
fonction effectue des opérations comme la lecture à partir d'une socket qui peut être bloquée pendant un certain temps, est-ce que cela compte une opération observable? Et puisque l'opérationread
n'est pas "totalement connue" (n'est-ce pas?), Le code restera-t-il en ordre?Résumé:
Il ne semble pas y avoir de moyen garanti d'empêcher la réorganisation, mais tant que l'optimisation du temps de liaison / du programme complet n'est pas activée, localiser la fonction appelée dans une unité de compilation séparée semble un assez bon pari . (Au moins avec GCC, bien que la logique suggère que cela est probable avec d'autres compilateurs aussi.) Cela se fait au prix de l'appel de fonction - le code incorporé est par définition dans la même unité de compilation et ouvert à la réorganisation.
Réponse originale:
GCC réorganise les appels sous l'optimisation -O2:
GCC 5.3.0:
g++ -S --std=c++11 -O0 fred.cpp
:Mais:
g++ -S --std=c++11 -O2 fred.cpp
:Maintenant, avec foo () comme fonction externe:
g++ -S --std=c++11 -O2 fred.cpp
:MAIS, si cela est lié à -flto (optimisation du temps de liaison):
la source
La réorganisation peut être effectuée par le compilateur ou par le processeur.
La plupart des compilateurs proposent une méthode spécifique à la plate-forme pour empêcher la réorganisation des instructions de lecture-écriture. Sur gcc, c'est
( Plus d'informations ici )
Notez que cela n'empêche qu'indirectement de réorganiser les opérations, tant qu'elles dépendent des lectures / écritures.
En pratique je n'ai pas encore vu de système où l'appel système
Clock::now()
a le même effet qu'une telle barrière. Vous pouvez inspecter l'assemblage résultant pour être sûr.Cependant, il n'est pas rare que la fonction testée soit évaluée pendant la compilation. Pour appliquer une exécution «réaliste», vous devrez peut-être dériver des entrées pour
foo()
des E / S ou unvolatile
lecture.Une autre option serait de désactiver l'inlining pour
foo()
- encore une fois, c'est spécifique au compilateur et généralement pas portable, mais aurait le même effet.Sur gcc, ce serait
__attribute__ ((noinline))
@Ruslan soulève une question fondamentale: dans quelle mesure cette mesure est-elle réaliste?
Le temps d'exécution est affecté par de nombreux facteurs: l'un est le matériel réel sur lequel nous fonctionnons, l'autre est l'accès simultané aux ressources partagées comme le cache, la mémoire, le disque et les cœurs de processeur.
Donc, ce que nous faisons habituellement pour obtenir des timings comparables : assurez-vous qu'ils sont reproductibles avec une faible marge d'erreur. Cela les rend quelque peu artificiels.
Les performances d'exécution "hot cache" et "cold cache" peuvent facilement différer d'un ordre de grandeur - mais en réalité, ce sera quelque chose entre les deux ("tiède"?)
la source
asm
affecte le temps d'exécution des instructions entre les appels de minuterie: le code après la mémoire clobber doit recharger toutes les variables de la mémoire.Le langage C ++ définit ce qui est observable de plusieurs manières.
S'il
foo()
ne fait rien d'observable, alors il peut être complètement éliminé. Sifoo()
seulement un calcul qui stocke des valeurs dans un état "local" (que ce soit sur la pile ou dans un objet quelque part) et que le compilateur peut prouver qu'aucun pointeur dérivé en toute sécurité ne peut entrer dans leClock::now()
code, alors il n'y a pas de conséquences observables pour déplacer lesClock::now()
appels.S'il
foo()
interagit avec un fichier ou l'affichage et que le compilateur ne peut pas prouver qu'ilClock::now()
n'interagit pas avec le fichier ou l'affichage, la réorganisation ne peut pas être effectuée, car l'interaction avec un fichier ou un affichage est un comportement observable.Bien que vous puissiez utiliser des hacks spécifiques au compilateur pour forcer le code à ne pas se déplacer (comme l'assemblage en ligne), une autre approche consiste à tenter de déjouer votre compilateur.
Créez une bibliothèque chargée dynamiquement. Chargez-le avant le code en question.
Cette bibliothèque expose une chose:
et l'enveloppe comme ceci:
qui emballe un lambda nul et utilise la bibliothèque dynamique pour l'exécuter dans un contexte que le compilateur ne peut pas comprendre.
À l'intérieur de la bibliothèque dynamique, nous faisons:
ce qui est assez simple.
Maintenant, pour réorganiser les appels
execute
, il doit comprendre la bibliothèque dynamique, ce qu'il ne peut pas lors de la compilation de votre code de test.Il peut toujours éliminer les
foo()
s sans effets secondaires, mais vous en gagnez, vous en perdez.la source
volatile
accès factice ou appeler un code externe.Non, ça ne peut pas. Selon le standard C ++ [intro.execution]:
Une expression complète est essentiellement une instruction terminée par un point-virgule. Comme vous pouvez le voir, la règle ci-dessus stipule que les instructions doivent être exécutées dans l'ordre. C'est à l' intérieur des instructions que le compilateur a plus de liberté (c'est-à-dire qu'il est autorisé dans certaines circonstances à évaluer les expressions qui composent une instruction dans des ordres autres que de gauche à droite ou autre chose spécifique).
Notez que les conditions d'application de la règle du «comme si» ne sont pas remplies ici. Il n'est pas raisonnable de penser qu'un compilateur serait capable de prouver que la réorganisation des appels pour obtenir l'heure système n'affecterait pas le comportement observable du programme. S'il y avait une circonstance dans laquelle deux appels pour obtenir l'heure pouvaient être réorganisés sans changer le comportement observé, il serait extrêmement inefficace de produire en fait un compilateur qui analyse un programme avec suffisamment de compréhension pour pouvoir en déduire avec certitude.
la source
Non.
Parfois, par la règle du "comme si", les instructions peuvent être réordonnées. Ce n'est pas parce qu'ils sont logiquement indépendants les uns des autres, mais parce que cette indépendance permet à un tel réarrangement de se produire sans changer la sémantique du programme.
Déplacer un appel système qui obtient l'heure courante ne satisfait évidemment pas cette condition. Un compilateur qui le fait sciemment ou non est non conforme et vraiment stupide.
En général, je ne m'attendrais pas à ce qu'une expression qui aboutisse à un appel système soit "seconde-devinée" même par un compilateur optimisant de manière agressive. Il n'en sait tout simplement pas assez sur ce que fait cet appel système.
la source
int x = 0; clock(); x = y*2; clock();
il n'y a pas de moyens définis pour leclock()
code d'interagir avec l'état dex
. Sous la norme C ++, il n'a pas besoin de savoir ce queclock()
fait - il pourrait examiner la pile (et remarquer quand le calcul a lieu), mais ce n'est pas le problème de C ++ .t2
et le second àt1
, serait non conforme et idiot si ces valeurs sont utilisées, ce que cette réponse manque, c'est que un compilateur conforme peut parfois réorganiser un autre code à travers un appel système. Dans ce cas, à condition qu'il sache ce quefoo()
fait (par exemple parce qu'il l'a incorporé) et donc que (en gros) c'est une fonction pure, alors il peut le déplacer.y*y
avant l'appel système, juste pour le plaisir. Il n'y a pas non plus de garantie que l'implémentation réelle n'utilisera pas le résultat de ce calcul spéculatif plus tard, quel que soit le pointx
utilisé, ne faisant donc rien entre les appels àclock()
. Il en va de même pour tout ce que fait une fonction intégréefoo
, à condition qu'elle n'ait aucun effet secondaire et ne puisse pas dépendre d'un état susceptible d'être modifié parclock()
.noinline
fonction + boîte noire d'assemblage en ligne + dépendances de données complètesCeci est basé sur https://stackoverflow.com/a/38025837/895245 mais comme je n'ai vu aucune justification claire de la raison pour laquelle le
::now()
ne peut pas être réorganisé là-bas, je préférerais être paranoïaque et le mettre dans une fonction noinline avec le asm.De cette façon, je suis à peu près sûr que la réorganisation ne peut pas se produire, car les
noinline
"liens" entre le::now
et la dépendance des données.main.cpp
GitHub en amont .
Compilez et exécutez:
Le seul inconvénient mineur de cette méthode est que nous ajoutons une
callq
instruction supplémentaire sur uneinline
méthode.objdump -CD
montre quimain
contient:nous voyons donc que
foo
c'était en ligne, maisget_clock
ne l'étaient pas et l'entourons.get_clock
lui-même est cependant extrêmement efficace, consistant en une instruction optimisée pour les appels à une seule feuille qui ne touche même pas la pile:Étant donné que la précision de l'horloge est elle-même limitée, je pense qu'il est peu probable que vous puissiez remarquer les effets de synchronisation d'un supplément
jmpq
. Notez que l'uncall
est nécessaire, car il se::now()
trouve dans une bibliothèque partagée.Appel
::now()
depuis l'assembly en ligne avec une dépendance de donnéesCe serait la solution la plus efficace possible, surmontant même le supplément
jmpq
mentionné ci-dessus.Ceci est malheureusement extrêmement difficile à faire correctement, comme indiqué sur: Appel de printf dans l'ASM en ligne étendu
Si votre mesure du temps peut être effectuée directement dans l'assemblage en ligne sans appel, cette technique peut être utilisée. C'est le cas par exemple pour les instructions d'instrumentation gem5 magic , x86 RDTSC ( ne sais pas si c'est plus représentatif) et éventuellement d'autres compteurs de performance.
Fils associés:
Testé avec GCC 8.3.0, Ubuntu 19.04.
la source
"+m"
, l'utilisation"+r"
est un moyen beaucoup plus efficace pour que le compilateur matérialise une valeur et suppose ensuite que la variable a changé.