Les normes C++11 / C++14 comme écrit permettent aux trois magasins d'être repliés/coalisés en un seul magasin de la valeur finale. Même dans un cas comme celui-ci :
y.store(1, order);
y.store(2, order);
y.store(3, order); // inlining + constant-folding could produce this in real code
La norme ne no garantir qu'un observateur tournant sur y
(avec une charge atomique ou un CAS) verra jamais y == 2
. Un programme qui dépendrait de cela aurait un bogue de course de données, mais seulement le type de course de données le plus courant, et non le type de course de données du C++ Undefined Behaviour. (Il s'agit d'un comportement indéfini uniquement avec les variables non atomiques). Un programme qui s'attend à parfois voir qu'il n'est même pas nécessairement bogué. (Voir ci-dessous au sujet des barres de progression).
Tout ordre qui est possible sur la machine abstraite C++ peut être choisi (au moment de la compilation) comme l'ordre qui va toujours se produire . C'est la règle du "si" en action. Dans ce cas, c'est comme si les trois stockages se sont produits dos à dos dans l'ordre global, sans qu'aucun chargement ou stockage d'autres threads ne se produise entre les y=1
y y=3
.
Il ne dépend pas de l'architecture ou du matériel cible, tout comme l'a fait l'agent de sécurité. réorganisation au moment de la compilation d'opérations atomiques relaxées sont autorisées même en ciblant le x86 fortement ordonné. Le compilateur n'a pas à préserver tout ce que l'on pourrait attendre en pensant au matériel pour lequel on compile, donc on a besoin de barrières. Les barrières peuvent être compilées en instructions asm zéro.
Alors pourquoi les compilateurs ne font-ils pas cette optimisation ?
Il s'agit d'un problème de qualité d'implémentation, qui peut modifier les performances et le comportement observés sur le matériel réel.
Le cas le plus évident où cela pose problème est celui de la barre de progression. . Si l'on sort les magasins d'une boucle (qui ne contient aucune autre opération atomique) et qu'on les replie tous en un seul, la barre de progression restera à 0 et atteindra 100 % à la fin.
Il n'y a pas de C++11 std::atomic
moyen de arrêter Ainsi, pour l'instant, les compilateurs choisissent simplement de ne jamais fusionner plusieurs opérations atomiques en une seule. (Les coalescer toutes en une seule opération ne change pas leur ordre les unes par rapport aux autres).
Les rédacteurs de compilateurs ont remarqué à juste titre que les programmeurs s'attendent à ce qu'un stockage atomique se produise effectivement en mémoire chaque fois que la source fait y.store()
. (Voir la plupart des autres réponses à cette question, qui affirment que les magasins doivent se produire séparément en raison des lecteurs possibles qui attendent de voir une valeur intermédiaire) ; c'est-à-dire qu'il viole la règle du le principe de la moindre surprise .
Toutefois, il existe des cas où elle serait très utile, par exemple pour éviter les inutiles shared_ptr
réf compte inc/déc dans une boucle.
Il est évident que toute réorganisation ou fusion ne doit pas violer d'autres règles d'ordonnancement. Par exemple, num++; num--;
devrait toujours être une barrière complète au réordonnancement à l'exécution et à la compilation, même s'il ne touche plus la mémoire à l'adresse num
.
Des discussions sont en cours pour étendre le std::atomic
API donner aux programmeurs le contrôle de ces optimisations, ce qui permettra aux compilateurs d'optimiser lorsque cela est utile, ce qui peut arriver même dans un code soigneusement écrit qui n'est pas intentionnellement inefficace. Quelques exemples de cas utiles pour l'optimisation sont mentionnés dans les liens suivants de discussion / proposition de groupe de travail :
Voir également la discussion sur ce même sujet dans la réponse de Richard Hodges à la question suivante Est-ce que num++ peut être atomique pour 'int num' ? (voir les commentaires). Voir aussi la dernière section de ma réponse à la même question, où j'argumente plus en détail que cette optimisation est autorisée. (Je serai bref ici, car les liens du groupe de travail C++ reconnaissent déjà que la norme actuelle telle qu'elle est écrite l'autorise, et que les compilateurs actuels ne l'optimisent tout simplement pas exprès).
Dans le cadre de la norme actuelle, volatile atomic<int> y
serait un moyen de s'assurer que les magasins qui s'y trouvent ne sont pas autorisés à être optimisés. (Comme Herb Sutter fait remarquer dans une réponse SO , volatile
y atomic
partagent déjà certaines exigences, mais elles sont différentes). Voir aussi std::memory_order
La relation de l'entreprise avec volatile
sur cppreference.
Accès à volatile
ne sont pas autorisés à être optimisés (parce qu'ils pourraient être des registres d'entrée-sortie mappés en mémoire, par exemple).
Utilisation de volatile atomic<T>
résout en grande partie le problème de la barre de progression, mais c'est plutôt laid et pourrait sembler idiot dans quelques années si/quand le C++ décide d'une syntaxe différente pour contrôler l'optimisation afin que les compilateurs puissent commencer à le faire en pratique.
Je pense que nous pouvons être sûrs que les compilateurs ne commenceront pas à faire cette optimisation tant qu'il n'y aura pas un moyen de la contrôler. Espérons qu'il s'agira d'une sorte d'option de participation (comme une option d'achat). memory_order_release_coalesce
) qui ne change pas le comportement du code C++11/14 existant lorsqu'il est compilé en C++quelque chose. Mais cela pourrait être comme la proposition dans wg21/p0062 : marquer les cas de non-optimisation avec [[brittle_atomic]]
.
wg21/p0062 avertisse que même si volatile atomic
ne résout pas tout, et décourage son utilisation à cette fin . Il donne cet exemple :
if(x) {
foo();
y.store(0);
} else {
bar();
y.store(0); // release a lock before a long-running loop
for() {...} // loop contains no atomics or volatiles
}
// A compiler can merge the stores into a y.store(0) here.
Même avec volatile atomic<int> y
un compilateur est autorisé à évincer le y.store()
de la if/else
et de ne le faire qu'une seule fois, parce qu'il s'agit toujours de faire exactement un stockage avec la même valeur. (Ce qui serait après la longue boucle dans la branche else). Surtout si le stockage est seulement relaxed
o release
au lieu de seq_cst
.
volatile
arrête le coalescencement discuté dans la question, mais cela souligne que d'autres optimisations sur les atomic<>
peut également être problématique pour les performances réelles.
Parmi les autres raisons de ne pas optimiser, citons : personne n'a écrit le code compliqué qui permettrait au compilateur d'effectuer ces optimisations en toute sécurité (sans jamais se tromper). Ce n'est pas suffisant, car N4455 dit que LLVM implémente déjà ou pourrait facilement implémenter plusieurs des optimisations qu'il a mentionnées.
La raison de la confusion pour les programmeurs est certainement plausible, cependant. Le code sans verrou est déjà assez difficile à écrire correctement en premier lieu.
Ne soyez pas désinvolte dans votre utilisation des armes atomiques : elles ne sont pas bon marché et n'optimisent pas beaucoup (actuellement pas du tout). Il n'est pas toujours facile d'éviter les opérations atomiques redondantes avec std::shared_ptr<T>
cependant, puisqu'il n'existe pas de version non-atomique de cette dernière (bien que une des réponses ici offre un moyen simple de définir un shared_ptr_unsynchronized<T>
pour gcc).
0 votes
Cette étape d'optimisation n'entraîne probablement pas une grande accélération dans l'application réelle par rapport au coût d'exécution de l'étape d'optimisation, surtout lorsque le code n'est pas trivial. Cette discussion est quelque peu lié.
21 votes
Et si
f
n'est qu'un fil parmi d'autres écrivant ày
tandis que d'autres lisent à partir dey
? Si le compilateur fusionne les écritures en une seule, alors le comportement du programme peut changer de manière inattendue.20 votes
@Someprogrammerdude Ce comportement n'était pas garanti auparavant, donc cela ne rendrait pas l'optimisation invalide.
4 votes
@Someprogrammerdude Je suppose cette situation, et je ne comprends toujours pas. 'f() tourne très vite' est toujours une possibilité d'ordonnancement, donc aucun programme valide ne pourrait supposer qu'il pourrait voir chacune de ces écritures distinctes.
1 votes
Mais vous je ne sais pas et c'est l'un des problèmes. Si nous ne connaissons pas tous les cas d'utilisation possibles, comment le compilateur pourrait-il le faire ?
8 votes
Un argument très pratique est le suivant : pour un compilateur, il serait difficile de raisonner sur la redondance des magasins dans le cas général, alors que pour celui qui écrit le code, il devrait être trivial d'éviter ces écritures redondantes, alors pourquoi les auteurs de compilateurs se donneraient-ils la peine d'ajouter une telle optimisation ?
1 votes
On dirait que la réponse aquí pourrait le couvrir.
3 votes
@NathanOliver En quoi cela est-il lié ? Une optimisation du compilateur qui ajoute une écriture introduisant potentiellement une course aux données n'est pas du tout la même chose qu'une optimisation qui supprime les écritures thread-safe redondantes.
3 votes
@NathanOliver Merci, mais la suppression des deux magasins redondants n'introduirait pas "des affectations à un emplacement de mémoire potentiellement partagé qui ne serait pas modifié par la machine abstraite", donc je ne pense pas que cette partie de la norme aide.
3 votes
Le problème ici est qu'il est impossible de prouver que les magasins sont redondants. Supposons qu'un autre thread soit en cours d'exécution et qu'il définisse
y
a42
entre le 2ème et le 3ème magasin,y
serait toujours1
à la fin def
. Si les magasins "redondants" étaient supprimésy
serait2
à la fin def
.17 votes
@RichardCritten Il n'y a aucun moyen d'écrire un programme C++ qui définit
y
a42
entre le 2ème et le 3ème magasin. Vous pouvez écrire un programme qui ne fait que le stockage et peut-être que vous aurez de la chance, mais il n'y a aucun moyen de le garantir. Il est impossible de dire si cela ne s'est jamais produit parce que les écritures redondantes ont été supprimées ou parce que vous avez juste eu un timing malchanceux, donc l'optimisation est valide. Même si cela hace il arrive que vous n'ayez aucun moyen de le savoir, car cela pourrait être avant le premier, le deuxième ou le troisième.4 votes
Je suis en fait soulagé d'entendre qu'aucun compilateur n'optimise cela.
6 votes
Le comité de normalisation n'est pas sûr d'être toujours d'accord avec les optimisations atomiques agressives, donc les compilateurs les évitent probablement. Voir P0062 pour une discussion sur les atomiques et les optimisations agressives. Les articles expliquent que le type d'optimisation que vous attendez est en effet parfaitement autorisé, mais pas toujours ce que l'utilisateur attend.
21 votes
La réponse prosaïque est qu'il n'y a probablement jamais eu assez de code qui ressemble à cela pour qu'un optimiseur-écrivain décide de s'embêter à écrire une optimisation pour lui.
0 votes
@Morwenn lecture intéressante, merci. (Bon candidat pour une réponse !)
0 votes
@TripeHound : Oui. Dispositif de Duff est si obscure que personne ne l'a jamais vue.
0 votes
@TripeHound Il n'y a pas beaucoup de code qui crée des redondances.
shared_ptr
dans les fonctions en ligne ?0 votes
@Morwenn Le comité std n'est pas sûr qu'il soit correct de déplacer des opérations atomiques sur des boucles très longues. Il ne semble pas avoir réfléchi sérieusement à la question car on pourrait dire la même chose des opérations volatiles ou même des E/S régulières. (Ou même pour déplacer du code autour des opérations de synchronisation).