Les variables globales signifient-elles un code plus rapide?

J’ai lu récemment, dans un article sur la programmation de jeux écrit en 1996, que l’utilisation de variables globales est plus rapide que la transmission de parameters.

Était-ce toujours vrai, et si oui, est-ce toujours le cas aujourd’hui?

Réponse courte – Non, les bons programmeurs accélèrent l’exécution du code en connaissant et en utilisant les outils appropriés pour le travail, puis en optimisant méthodiquement leur code lorsque celui-ci ne répond pas à leurs besoins.

Réponse plus longue – Cet article, qui à mon avis n’est pas particulièrement bien écrit, n’est en aucun cas un conseil général sur l’accélération du programme mais sur «15 façons de faire plus rapidement des blits». Extrapoler cela au cas général manque le sharepoint vue de l’auteur, peu importe ce que vous pensez des mérites de l’article.

Si je cherchais des conseils de performance, je mettrais zéro crédibilité dans un article qui n’identifie pas ou ne montre pas un seul changement de code concret pour prendre en charge les assertions du code, sans suggérer que mesurer le code pourrait être une bonne idée. Si vous n’allez pas montrer comment améliorer le code, pourquoi l’inclure?

Certains conseils sont des années dépassées – les pointeurs FAR ont cessé d’être un problème sur le PC il y a longtemps.

Un développeur de jeux sérieux (ou tout autre programmeur professionnel) aurait bien rigolé à propos de ces conseils:

Vous pouvez soit retirer complètement l’ #define NDEBUG , soit append un #define NDEBUG lorsque vous comstackz la version finale.

Mon conseil pour vous, si vous voulez vraiment évaluer le mérite de l’un de ces 15 conseils, et puisque l’article a 14 ans, serait de comstackr le code dans un compilateur moderne (Visual C ++ 10) et d’essayer d’identifier zone où l’utilisation d’une variable globale (ou de l’un des autres conseils) le rendrait plus rapide.

[Je plaisante – mon vrai conseil serait d’ignorer complètement cet article et de poser des questions de performances spécifiques sur Stack Overflow lorsque vous rencontrez des problèmes dans votre travail que vous ne pouvez pas résoudre. De cette façon, les réponses que vous recevrez seront examinées par des pairs, sockets en charge par un exemple de code ou de bonnes preuves externes et actuelles.]

Lorsque vous passez des parameters aux variables globales, l’une des trois choses suivantes peut se produire:

  • il court plus vite
  • il court le même
  • il tourne plus lentement

Vous devrez mesurer les performances pour voir ce qui est plus rapide dans un cas concret non sortingvial. C’était vrai en 1996, c’est vrai aujourd’hui et c’est vrai demain.

Laissant de côté la performance pour un moment, les variables globales d’un grand projet introduisent des dépendances qui rendent presque toujours la maintenance et les tests beaucoup plus difficiles.

En essayant de trouver des utilisations légitimes des variables globales pour des raisons de performances aujourd’hui, je suis tout à fait d’accord avec les exemples de la réponse de Preet : des variables très souvent nécessaires dans les programmes de microcontrôleurs ou les pilotes de périphériques. Le cas extrême est un registre de processeur exclusivement dédié à la variable globale.

Lors du raisonnement sur les performances des variables globales par rapport au passage de parameters, la manière dont le compilateur les implémente est pertinente. Les variables globales sont généralement stockées à des emplacements fixes. Parfois, le compilateur génère un adressage direct pour accéder aux globals. Parfois, le compilateur utilise une indirection supplémentaire et utilise une sorte de table de symboles pour les globales. IIRC gcc pour AIX l’a fait il y a 15 ans. Dans cet environnement, les globales de petits types étaient toujours plus lents que les locals et les parameters passaient.

En revanche, un compilateur peut transmettre des parameters en les poussant sur la stack, en les faisant passer dans des registres ou dans un mélange des deux.

Tout le monde a déjà donné les réponses appropriées à ce sujet, qu’il s’agisse de plates-formes et de programmes spécifiques, nécessitant de mesurer les timings, etc. Ainsi, déjà dit, permettez-moi de répondre directement à la question du cas particulier de la programmation sur x86 et PowerPC.

En 1996, il y avait certains cas où le fait de pousser des parameters sur la stack prenait des instructions supplémentaires et pouvait provoquer un bref arrêt dans le pipeline du processeur Intel. Dans ces cas, il pourrait y avoir une très petite accélération de la suppression totale des parameters et de la lecture des données à partir des adresses littérales.

Ce n’est plus le cas sur le x86 ou sur le PowerPC utilisé dans la plupart des consoles de jeux. L’utilisation de globals est généralement plus lente que le passage de parameters pour deux raisons:

  • Le passage des parameters est mieux implémenté maintenant. Les processeurs modernes transmettent leurs parameters dans les registres, donc la lecture d’une valeur à partir de la liste de parameters d’une fonction est plus rapide qu’une opération de chargement de mémoire. Le x86 utilise l’enregistrement de registre et le transfert de magasin, ce qui ressemble à un armsage de données sur la stack et le dos peut être un simple mouvement de registre.
  • La latence du cache de données dépasse largement la vitesse d’horloge du processeur dans la plupart des considérations de performances . La stack, étant très utilisée, est presque toujours en cache. Le chargement à partir d’une adresse globale arbitraire peut entraîner un échec de la mémoire cache, ce qui constitue une énorme pénalité, car le contrôleur de mémoire doit aller chercher les données à partir de la mémoire RAM principale. (“Énorme” est de 600 cycles ou plus.)

Que voulez-vous dire par “plus vite”?

Je sais pertinemment que comprendre un programme avec des variables globales me prend beaucoup plus de temps qu’un sans.

Si le temps supplémentaire nécessaire au (x) programmeur (s) est inférieur au temps gagné par les utilisateurs lorsqu’ils exécutent le programme avec des globals, alors je dirais que l’utilisation de global est plus rapide.

Mais considérez que le programme sera géré par 10 personnes une fois par jour pendant 2 ans. Et cela prend 2.84632 secondes sans globales et 2.84217 secondes avec des globales (une augmentation de 0.00415 sec). Cela représente 727 secondes de moins que le temps d’exécution TOTAL . Gagner 10 minutes de temps d’exécution ne vaut pas l’introduction d’un temps global en ce qui concerne le temps du programmeur.

Dans une certaine mesure, tout code qui évite les instructions du processeur (c’est-à-dire un code plus court) sera plus rapide . Cependant, combien plus rapide? Pas très! Notez également que les stratégies d’optimisation du compilateur peuvent aboutir au code le plus petit de toute façon.

Ces jours-ci, il ne s’agit là que d’une optimisation sur des applications très spécifiques, généralement dans les pilotes à temps extrêmement critique ou dans le code de micro-contrôle.

Mis à part les problèmes de maintenabilité et d’exactitude, il y a essentiellement deux facteurs qui régiront la performance en ce qui concerne les globaux et les parameters.

Lorsque vous faites un global, vous évitez une copie. C’est un peu plus rapide. Lorsque vous transmettez un paramètre par valeur, il doit être copié pour qu’une fonction puisse fonctionner sur une copie locale et ne pas endommager la copie des données de l’appelant. Au moins en théorie. Certains optimiseurs modernes font des choses très délicates s’ils identifient que votre code se comporte bien. Une fonction peut être automatiquement mise en ligne, et le compilateur peut remarquer que la fonction ne fait rien aux parameters, et optimise simplement toute copie.

Lorsque vous créez un global, vous vous trouvez dans le cache. Lorsque vous avez toutes vos variables soigneusement contenues dans votre fonction et quelques parameters, les données tendent à se trouver au même endroit. Certaines des variables seront dans des registres, et d’autres seront probablement en cache tout de suite car elles se trouvent juste à côté. L’utilisation de nombreuses variables globales est fondamentalement un comportement pathologique pour le cache. Il n’y a aucune garantie que divers globaux seront utilisés par les mêmes fonctions. L’emplacement n’a aucune corrélation évidente avec l’utilisation. Peut-être que vous avez un petit groupe de travail suffisamment petit pour que rien ne change, et que tout se retrouve dans le cache.

Tout cela ne fait qu’append au point soulevé par une affiche au-dessus de moi:

Lorsque vous passez des parameters aux variables globales, l’une des trois choses suivantes peut se produire:

 * it runs faster * it runs the same * it runs slower 

Vous devrez mesurer les performances pour voir ce qui est plus rapide dans un cas concret non sortingvial. C’était vrai en 1996, c’est vrai aujourd’hui et c’est vrai demain.

En fonction du comportement spécifique de votre compilateur exact et des détails précis sur le matériel que vous utilisez pour exécuter votre code, il est possible que les variables globales génèrent parfois de très faibles performances dans certains cas. Cette possibilité vaut peut-être la peine d’essayer un code trop lent en tant qu’expérience. Cela ne vaut probablement pas la peine de vous y consacrer, car la réponse de votre expérience pourrait changer demain. Donc, la bonne réponse consiste presque toujours à adopter des modèles de conception «corrects» et à éviter la conception la plus laide. Recherchez de meilleurs algorithmes, des structures de données plus efficaces, etc., avant d’essayer intentionnellement de spaghettiser votre projet. Beaucoup mieux à long terme.

Et, mis à part l’argument temps de développement vs temps utilisateur, j’appendai le temps de développement par rapport à l’argument de temps de Moore. Si vous supposez que la loi de Moore rendra les ordinateurs à peu près la moitié plus rapide chaque année, alors, pour un simple chiffre rond, nous pouvons supposer que le progrès se produit avec un progrès constant de 1% par semaine. Si vous envisagez une micro-optimisation qui pourrait améliorer des choses comme 1%, et que cela compliquera les choses pendant une semaine, le simple fait de prendre une semaine de congé aura le même effet sur les temps d’exécution moyens pour vos utilisateurs.

Peut-être une optimisation micro, et serait probablement effacée par des optimisations que votre compilateur pourrait générer sans recourir à de telles pratiques. En fait, l’utilisation des globales peut même empêcher certaines optimisations du compilateur. Un code fiable et maintenable serait généralement de plus grande valeur, et les globals ne sont pas propices à cela.

L’utilisation de globales pour remplacer les parameters de fonction rend toutes ces fonctions non réentrantes, ce qui peut poser problème si le multi-threading est utilisé – ce qui n’est pas une pratique courante dans le développement de jeux en 1996, mais plus fréquent avec les processeurs multicœurs. Cela exclut également la récursivité, bien que cela soit probablement moins grave puisque la récursivité a ses propres problèmes.

Dans tout corpus de code significatif, l’optimisation des algorithmes et des structures de données à un niveau supérieur risque d’être plus importante. De plus, il existe des options autres que les variables globales qui évitent le passage de parameters, en particulier les variables de classe C ++.

Si l’utilisation habituelle de variables globales dans votre code fait une différence mesurable ou utile à ses performances, je remettrais en question la conception en premier.

Pour une discussion sur les problèmes inhérents aux variables globales et sur les moyens de les éviter, consultez A Pox on Globals de Jack Gannsle. L’article concerne le développement de systèmes intégrés, mais est généralement applicable; c’est juste que certains développeurs de systèmes embarqués pensent qu’ils ont de bonnes raisons d’utiliser les globals, probablement pour les mêmes raisons erronées utilisées pour le justifier dans le développement de jeux.

Eh bien, si vous envisagez d’utiliser des parameters globaux au lieu de passer des parameters, cela peut signifier que vous avez une longue chaîne de méthodes / fonctions pour passer ce paramètre. Si tel est le cas, vous économiserez vraiment les cycles du processeur en passant de la variable paramètre à la variable globale.

Donc, les gars qui disent que ça dépend, je suppose qu’ils ont tout à fait tort. Même avec le passage du paramètre REGISTER, il y aura toujours PLUS de cycles cpu et PLUS de temps de charge pour envoyer les parameters à l’appelé.

Cependant, je ne fais jamais ça. Les processeurs sont supérieurs maintenant, et parfois il y avait 12Mhz 8086 qui pourraient être le problème. De nos jours, si vous n’écrivez pas de code de performance embarqué ou super turbo, restz fidèle à ce qui semble bon en code, qui ne casse pas la logique du code et qui se veut modulaire.

Enfin, laissez le compilateur générer du code de langage machine. Les personnes qui l’ont conçu sont les mieux placées pour savoir comment leur bébé se comporte et pour que votre code fonctionne de manière optimale.

En général (mais cela peut dépendre beaucoup de l’implémentation du compilateur et de la plate-forme), passer des parameters signifie les écrire sur la stack dont vous n’auriez pas besoin avec la variable globale.

Cela dit, la variable globale peut signifier inclure le rafraîchissement de la page dans la MMU ou le contrôleur de mémoire alors que la stack peut se trouver dans une mémoire beaucoup plus rapide disponible pour le processeur …

Désolé, pas de bonne réponse à une question générale comme celle-ci, il suffit de la mesurer (et d’essayer différents scénarios)

C’était plus rapide avec des processeurs <100 MHz. Maintenant que les processeurs sont 100 fois plus rapides, ce problème est 100 fois moins important. Ce n'était pas un gros problème à l'époque, c'était une grosse affaire quand vous l'avez fait en assemblage et que vous n'aviez aucun (bon) optimiseur.

Dit le gars qui a programmé sur un processeur 3mhz. Oui, vous avez bien lu et 64k ne suffisait pas.

Je vois beaucoup de réponses théoriques, mais pas de conseils pratiques pour votre scénario. Ce que je suppose, c’est que vous avez un grand nombre de parameters à transmettre à travers un certain nombre d’appels de fonctions, et vous vous inquiétez de la surcharge accumulée à partir de nombreux niveaux de trames d’appel et de nombreux parameters à chaque niveau. Sinon, votre préoccupation est totalement infondée.

Si tel est votre cas, vous devriez probablement placer tous les parameters dans une structure “contextuelle” et passer un pointeur sur cette structure. Cela assurera la localisation des données et vous évite d’avoir à passer plus d’un argument (le pointeur) à chaque appel de fonction.

Les parameters auxquels on accède de cette façon sont légèrement plus chers d’access que les arguments de fonction réels (vous avez besoin d’un registre supplémentaire pour placer le pointeur à la base de la structure, par opposition au pointeur de cadre qui servirait à cette fin). mais probablement pas avec les effets de cache pris en compte) plus coûteux à accéder que les variables globales dans un code normal, non-PIC . Cependant, si votre code se trouve dans une bibliothèque / DLL partagée utilisant un code indépendant de la position , le coût d’access aux parameters transmis par le pointeur vers struct est moins élevé que l’access à une variable globale et à l’access aux variables statiques. Ceci est une autre raison de ne jamais utiliser les variables globales pour le passage des parameters: si vous pouvez éventuellement placer votre code dans une bibliothèque / DLL partagée, tous les avantages possibles en termes de performances se retourneront soudainement!

Comme tout le rest: oui et non. Il n’y a pas de réponse unique car cela dépend du contexte.

Contrepoint:

  • Imaginez une programmation sur Itanium où vous avez des centaines de registres. Vous pouvez mettre plusieurs globales dans ceux-ci, qui seront plus rapides que la façon dont les globals sont implémentés en C (certaines adresses statiques (bien qu’elles puissent simplement coder en dur les globales en instructions si elles ont la longueur d’un mot)). Même si les globals sont en cache tout le temps, les registres peuvent encore être plus rapides.

  • En Java, la surutilisation des globales (statics) peut diminuer les performances en raison des verrous d’initialisation à effectuer. Si 10 classes veulent accéder à une classe statique, elles doivent toutes attendre que cette classe ait fini d’initialiser ses champs statiques, ce qui peut prendre n’importe où, sans jamais perdre de temps.

En tout état de cause, l’état mondial est une mauvaise pratique, il soulève la complexité du code. Un code bien conçu est naturellement assez rapide 99,9% du temps. Il semble que les nouveaux langages éliminent l’état global tous ensemble. E supprime l’état global car il viole leur modèle de sécurité. Haskell supprime tous les états. Le fait que Haskell existe et possède des implémentations qui surpassent la plupart des autres langages est une preuve suffisante pour moi que je n’utiliserai plus jamais de globales.

En outre, dans un avenir proche, alors que nous avons tous des centaines de cœurs, l’état mondial ne va pas vraiment aider beaucoup.

Cela peut encore être vrai dans certaines circonstances. Une variable globale peut être aussi rapide qu’un pointeur sur une variable, où son pointeur est stocké dans / passé via des registres uniquement. Donc, c’est une question sur le nombre de registres que vous pouvez utiliser.

Pour accélérer l’optimisation d’un appel de fonction, vous pouvez faire plusieurs autres choses, qui peuvent mieux fonctionner avec les variables globales:

  • Réduisez le nombre de variables locales dans la fonction à quelques variables de registre (explicites).
  • Minimiser le nombre de parameters de la fonction, c’est-à-dire en utilisant des pointeurs vers des structures au lieu d’utiliser les mêmes constellations de parameters dans les fonctions qui s’appellent.
  • Rendre la fonction “nue”, cela signifie qu’elle n’utilise pas la stack du tout.
  • Utilisez les “appels corrects” (ne fonctionne ni avec java / -bytecode, ni avec java- / ecma-script)
  • S’il n’y a pas de meilleur moyen, piratez-vous sth comme TABLES_NEXT_TO_CODE, qui localise vos variables globales à côté du code de la fonction. Dans les langages fonctionnels, il s’agit d’une optimisation du backend qui utilise également le pointeur de fonction comme pointeur de données. mais tant que vous ne programmez pas dans un langage fonctionnel, il vous suffit de localiser ces variables à côté de celles utilisées par la fonction. Là encore, vous ne voulez que supprimer la gestion des stacks de votre fonction. Si votre compilateur génère du code assembleur qui gère la stack, il est inutile de le faire, vous pouvez utiliser des pointeurs à la place.

J’ai trouvé cet “aperçu des atsortingbuts gcc”: http://www.ohse.de/uwe/articles/gcc-atsortingbutes.html

et je peux vous donner ces balises pour googler: – Appeler Tail Call (il est principalement pertinent pour les backends impératifs des langages fonctionnels) – TABLES_NEXT_TO_CODE (il concerne principalement Haskell et LLVM)

Mais vous avez «code spagetti», lorsque vous utilisez souvent des variables globales.