En regardant ce code:
static int global_var = 0;
int update_three(int val)
{
global_var = val;
return 3;
}
int main()
{
int arr[5];
arr[global_var] = update_three(2);
}
Quelle entrée de tableau est mise à jour? 0 ou 2?
Y a-t-il une partie dans la spécification de C qui indique la priorité de fonctionnement dans ce cas particulier?
c
language-lawyer
order-of-execution
Jiminion
la source
la source
clang
afin que ce morceau de code déclenche un avertissement à mon humble avis.Réponses:
Ordre des opérandes gauche et droit
Pour effectuer l'affectation dans
arr[global_var] = update_three(2)
, l'implémentation C doit évaluer les opérandes et, comme effet secondaire, mettre à jour la valeur stockée de l'opérande de gauche. Le paragraphe 3 de C 2018 6.5.16 (qui concerne les affectations) nous dit qu'il n'y a pas de séquencement dans les opérandes gauche et droit:Cela signifie que l'implémentation C est libre de calculer la lvalue d'
arr[global_var]
abord (en calculant la lvalue, nous voulons dire à quoi se réfère cette expression), puis d'évaluerupdate_three(2)
, et enfin d'attribuer la valeur de cette dernière à la première; ou pour évaluer d'update_three(2)
abord, puis calculer la valeur l, puis affecter la première à la seconde; ou pour évaluer la valeur l etupdate_three(2)
de façon mélangée, puis attribuer la valeur droite à la valeur l gauche.Dans tous les cas, l'affectation de la valeur à la valeur l doit venir en dernier, car 6.5.16 3 dit également:
Violation de séquençage
Certains pourraient réfléchir sur un comportement indéfini en raison de son utilisation
global_var
et de sa mise à jour séparée en violation de 6.5 2, qui dit:Il est assez familier à de nombreux praticiens C que le comportement d'expressions telles que
x + x++
n'est pas défini par la norme C car ils utilisent à la fois la valeur dex
et la modifient séparément dans la même expression sans séquençage. Cependant, dans ce cas, nous avons un appel de fonction, qui fournit un certain séquençage.global_var
est utiliséarr[global_var]
et mis à jour dans l'appel de fonctionupdate_three(2)
.6.5.2.2 10 nous dit qu'il y a un point de séquence avant que la fonction soit appelée:
À l'intérieur de la fonction, il
global_var = val;
y a une expression complète , tout comme le3
inreturn 3;
, selon 6.8 4:Ensuite, il y a un point de séquence entre ces deux expressions, toujours selon 6.8 4:
Ainsi, l'implémentation C peut évaluer d'
arr[global_var]
abord et ensuite faire l'appel de fonction, auquel cas il y a un point de séquence entre eux car il y en a un avant l'appel de fonction, ou elle peut évaluerglobal_var = val;
dans l'appel de fonction et ensuitearr[global_var]
, auquel cas il y a un point de séquence entre eux car il y en a un après l'expression complète. Le comportement n'est donc pas spécifié - l'une ou l'autre de ces deux choses peut être évaluée en premier - mais il n'est pas indéfini.la source
Le résultat ici n'est pas spécifié .
Bien que l'ordre des opérations dans une expression, qui dicte la façon dont les sous-expressions sont regroupées, soit bien défini, l'ordre d' évaluation n'est pas spécifié. Dans ce cas, cela signifie que l'un ou l'autre
global_var
pourrait être lu en premier ou l'appel àupdate_three
pourrait se produire en premier, mais il n'y a aucun moyen de savoir lequel.Il n'y a pas de comportement indéfini ici car un appel de fonction introduit un point de séquence, comme le fait chaque instruction de la fonction, y compris celle qui la modifie
global_var
.Pour clarifier, la norme C définit le comportement non défini dans la section 3.4.3 comme:
et définit le comportement non spécifié dans la section 3.4.4 comme:
La norme indique que l'ordre d'évaluation des arguments de fonction n'est pas spécifié, ce qui signifie dans ce cas que soit
arr[0]
défini sur 3 ouarr[2]
défini sur 3.la source
J'ai essayé et j'ai mis à jour l'entrée 0.
Cependant, selon cette question: le côté droit d'une expression sera-t-il toujours évalué en premier
L'ordre d'évaluation n'est pas spécifié et n'est pas séquencé. Je pense donc qu'un code comme celui-ci devrait être évité.
la source
Comme il est peu logique d'émettre du code pour une affectation avant d'avoir une valeur à attribuer, la plupart des compilateurs C émettent d'abord du code qui appelle la fonction et enregistrent le résultat quelque part (registre, pile, etc.), puis ils émettent du code qui écrit cette valeur dans sa destination finale et, par conséquent, ils liront la variable globale après sa modification. Appelons cela «l'ordre naturel», défini par aucune norme mais par pure logique.
Pourtant, dans le processus d'optimisation, les compilateurs tenteront d'éliminer l'étape intermédiaire de stockage temporaire de la valeur quelque part et tenteront d'écrire le résultat de la fonction aussi directement que possible vers la destination finale et dans ce cas, ils devront souvent lire l'index en premier , par exemple vers un registre, pour pouvoir déplacer directement le résultat de la fonction vers le tableau. Cela peut entraîner la lecture de la variable globale avant sa modification.
Il s'agit donc essentiellement d'un comportement indéfini avec la très mauvaise propriété qui est très probable que le résultat sera différent, selon que l'optimisation est effectuée et à quel point cette optimisation est agressive. C'est votre tâche en tant que développeur de résoudre ce problème en codant:
ou codage:
En règle générale, à moins que les variables globales ne le soient
const
(ou ne le sont pas, mais vous savez qu'aucun code ne les modifiera jamais comme effet secondaire), vous ne devez jamais les utiliser directement dans le code, comme dans un environnement multi-thread, même cela peut être indéfini:Puisque le compilateur peut le lire deux fois et qu'un autre thread peut changer la valeur entre les deux lectures. Pourtant, encore une fois, l'optimisation obligerait définitivement le code à le lire une seule fois, vous pouvez donc à nouveau avoir des résultats différents qui dépendent désormais également du timing d'un autre thread. Ainsi, vous aurez beaucoup moins de maux de tête si vous stockez des variables globales dans une variable de pile temporaire avant utilisation. Gardez à l'esprit que si le compilateur pense que cela est sûr, il optimisera probablement même cela et utilisera plutôt la variable globale directement, donc au final, cela ne fera aucune différence dans les performances ou l'utilisation de la mémoire.
(Juste au cas où quelqu'un demanderait pourquoi quelqu'un le ferait à la
x + 2 * x
place de3 * x
- sur certains processeurs, l'ajout est ultra-rapide et la multiplication par une puissance deux aussi, car le compilateur les transformera en décalages de bits (2 * x == x << 1
), mais la multiplication avec des nombres arbitraires peut être très lente , ainsi au lieu de multiplier par 3, vous obtenez un code beaucoup plus rapide en décalant les bits x par 1 et en ajoutant x au résultat - et même cette astuce est effectuée par les compilateurs modernes si vous multipliez par 3 et activez l'optimisation agressive à moins que ce ne soit une cible moderne CPU où la multiplication est aussi rapide que l'addition depuis lors, l'astuce ralentirait le calcul.)la source
3 * x
en deux lectures de x. Il pourrait lire x une fois, puis appliquer la méthode x + 2 * x sur le registre danslanguage-lawyer
, où la langue en question a sa propre "signification très spéciale" pour undefined , vous allez seulement semer la confusion en n'utilisant pas la définition de la langue.Edit global: désolé les gars, je suis tout excité et j'ai écrit beaucoup de bêtises. Juste un vieux coup de gueule.
Je voulais croire que C avait été épargné, mais hélas depuis C11, il a été mis au même niveau que C ++. Apparemment, savoir ce que le compilateur fera avec les effets secondaires dans les expressions nécessite maintenant de résoudre une petite énigme mathématique impliquant un ordre partiel des séquences de code basé sur "est situé avant le point de synchronisation de".
Il se trouve que j'ai conçu et mis en œuvre quelques systèmes embarqués critiques en temps réel à l'époque de K&R (y compris le contrôleur d'une voiture électrique qui pourrait envoyer des personnes s'écraser contre le mur le plus proche si le moteur n'était pas contrôlé, un industriel de 10 tonnes). robot qui pourrait écraser les gens en une pâte s'il n'est pas correctement commandé, et une couche système qui, bien qu'inoffensive, aurait quelques dizaines de processeurs sucer leur bus de données à sec avec moins de 1% de frais généraux du système).
Je suis peut-être trop sénile ou stupide pour faire la différence entre indéfini et non spécifié, mais je pense que j'ai encore une assez bonne idée de ce que signifient l'exécution simultanée et l'accès aux données. À mon avis sans doute informé, cette obsession du C ++ et maintenant des gars C avec leurs langages familiers prenant en charge les problèmes de synchronisation est un rêve de pipe coûteux. Soit vous savez ce qu'est une exécution simultanée, et vous n'avez besoin d'aucun de ces gadgets, soit vous ne l'avez pas, et vous rendriez service au monde en général sans essayer de jouer avec.
Tout ce chargement d'abstractions de barrière de mémoire alléchantes est simplement dû à un ensemble temporaire de limitations des systèmes de cache multi-CPU, qui peuvent tous être encapsulés en toute sécurité dans des objets de synchronisation de système d'exploitation communs comme, par exemple, les mutex et les variables de condition C ++ des offres.
Le coût de cette encapsulation n'est qu'une infime baisse des performances par rapport à ce qu'une utilisation d'instructions CPU spécifiques à grain fin pourrait réaliser dans certains cas.
Le
volatile
mot clé (ou un#pragma dont-mess-with-that-variable
malgré tout, en tant que programmeur système, le soin) aurait suffi pour dire au compilateur d'arrêter de réorganiser les accès à la mémoire. Le code optimal peut facilement être produit avec des directives asm directes pour saupoudrer le pilote de bas niveau et le code du système d'exploitation avec des instructions spécifiques au processeur ad hoc. Sans une connaissance approfondie du fonctionnement du matériel sous-jacent (système de cache ou interface de bus), vous êtes de toute façon obligé d'écrire du code inutile, inefficace ou défectueux.Un ajustement minutieux du
volatile
mot - clé et de Bob aurait été tout le monde, sauf l'oncle des programmeurs de bas niveau les plus durs. Au lieu de cela, le gang habituel des monstres mathématiques C ++ a eu une journée de terrain pour concevoir une autre abstraction incompréhensible, cédant à leur tendance typique à concevoir des solutions à la recherche de problèmes inexistants et à confondre la définition d'un langage de programmation avec les spécifications d'un compilateur.Seulement cette fois, le changement nécessaire pour défigurer un aspect fondamental de C aussi, puisque ces "barrières" devaient être générées même en code C de bas niveau pour fonctionner correctement. Cela, entre autres, a fait des ravages dans la définition des expressions, sans aucune explication ni justification que ce soit.
En conclusion, le fait qu'un compilateur puisse produire un code machine cohérent à partir de ce morceau absurde de C n'est qu'une conséquence éloignée de la façon dont les gars C ++ ont fait face aux incohérences potentielles des systèmes de cache de la fin des années 2000.
Cela a fait un terrible gâchis d'un aspect fondamental de C (définition de l'expression), de sorte que la grande majorité des programmeurs C - qui ne se soucient pas des systèmes de cache, et à juste titre - sont maintenant obligés de s'appuyer sur des gourous pour expliquer la différence entre
a = b() + c()
eta = b + c
.Essayer de deviner ce qu'il adviendra de ce tableau malheureux est une perte nette de temps et d'efforts de toute façon. Indépendamment de ce que le compilateur en fera, ce code est pathologiquement incorrect. La seule chose responsable à en faire est de l'envoyer au bac.
Conceptuellement, les effets secondaires peuvent toujours être retirés des expressions, avec l'effort trivial de laisser explicitement la modification se produire avant ou après l'évaluation, dans une déclaration distincte.
Ce type de code merdique aurait pu être justifié dans les années 80, lorsque vous ne pouviez pas vous attendre à ce qu'un compilateur optimise quoi que ce soit. Mais maintenant que les compilateurs sont devenus plus intelligents que la plupart des programmeurs, il ne reste plus qu'un morceau de code merdique.
Je ne comprends pas non plus l’importance de ce débat indéfini / non spécifié. Soit vous pouvez compter sur le compilateur pour générer du code avec un comportement cohérent, soit vous ne le pouvez pas. Que vous appeliez cela non défini ou non spécifié semble être un point discutable.
À mon avis, sans doute éclairé, C est déjà suffisamment dangereux dans son état K&R. Une évolution utile consisterait à ajouter des mesures de sécurité de bon sens. Par exemple, en utilisant cet outil d'analyse de code avancé, les spécifications forcent le compilateur à implémenter pour générer au moins des avertissements sur le code bonkers, au lieu de générer silencieusement un code potentiellement peu fiable à l'extrême.
Mais à la place, les gars ont décidé, par exemple, de définir un ordre d'évaluation fixe en C ++ 17. Désormais, chaque imbécile logiciel est activement incité à mettre délibérément des effets secondaires dans son code, profitant de la certitude que les nouveaux compilateurs géreront avec empressement l'obscurcissement de manière déterministe.
K&R était l'une des véritables merveilles du monde informatique. Pour vingt dollars, vous avez obtenu une spécification complète de la langue (j'ai vu des personnes seules écrire des compilateurs complets en utilisant simplement ce livre), un excellent manuel de référence (la table des matières vous pointe généralement dans quelques pages de la réponse à votre question), et un manuel qui vous apprendrait à utiliser la langue de manière sensée. Complétez avec des justifications, des exemples et des mots judicieux d'avertissement sur les nombreuses façons dont vous pourriez abuser du langage pour faire des choses très, très stupides.
Détruire cet héritage pour si peu de gains me semble une perte cruelle. Mais encore une fois, je pourrais très bien ne pas voir le point complètement. Peut-être qu'une âme aimable pourrait me diriger vers un exemple de nouveau code C qui tire un avantage significatif de ces effets secondaires?
la source
0,expr,0
.