Dans les programmes simultanés, les programmeurs accorderont une attention particulière à la synchronisation des données entre différents processus ou threads. Surtout lorsque plusieurs threads modifient la même variable en même temps, une synchronisation fiable ou d'autres mesures doivent être prises pour garantir que les données sont modifiées correctement. Le point ici est Le principe est le suivant : ne supposez pas l'ordre dans lequel les instructions sont exécutées. Vous ne pouvez pas prédire l'ordre dans lequel les instructions entre les différents threads seront exécutées.
Mais dans un programme monothread, il nous est généralement facile de supposer que les instructions sont exécutées séquentiellement, sinon nous pouvons imaginer les terribles changements qui arriveront au programme. Le modèle idéal est le suivant : l'ordre dans lequel les différentes instructions sont exécutées est unique et ordonné. Cet ordre est l'ordre dans lequel elles sont écrites dans le code, quel que soit le processeur ou d'autres facteurs. Ce modèle est appelé modèle de cohérence séquentielle. c'est un modèle basé sur le système de von Neumann. Bien entendu, cette hypothèse est raisonnable en soi et se produit rarement anormalement dans la pratique, mais en fait, aucune architecture multiprocesseur moderne n’adopte ce modèle car il est tout simplement trop inefficace. Dans l'optimisation de la compilation et le pipeline CPU, presque tous impliquent une réorganisation des instructions.
réorganisation du temps de compilation
Une réorganisation typique au moment de la compilation consiste à ajuster l'ordre des instructions pour réduire autant que possible le nombre de lectures et de stockages de registres sans modifier la sémantique du programme, et à réutiliser entièrement les valeurs stockées des registres.
Supposons que la première instruction calcule une valeur et l'attribue à la variable A et la stocke dans un registre. La deuxième instruction n'a rien à voir avec A mais doit occuper un registre (en supposant qu'elle occupera le registre où se trouve la troisième). L'instruction utilise la valeur de A et n'a rien à voir avec la deuxième instruction. Alors si selon le modèle de cohérence séquentielle, A est mis dans le registre après l'exécution de la première instruction, A n'existe plus lorsque la deuxième instruction est exécutée, et A est de nouveau lu dans le registre lorsque la troisième instruction est exécutée, et pendant ce processus, la valeur de A n’a pas changé. Habituellement, le compilateur échange les positions des deuxième et troisième instructions, de sorte que A existe dans le registre à la fin de la première instruction, puis la valeur de A peut être lue directement à partir du registre, réduisant ainsi la surcharge de lecture répétée.
L’importance de la réorganisation du pipeline
Les processeurs modernes utilisent presque tous le mécanisme de pipeline pour accélérer le traitement des instructions. De manière générale, le traitement d'une instruction nécessite plusieurs cycles d'horloge du processeur, et grâce à l'exécution parallèle du pipeline, plusieurs instructions peuvent être exécutées dans le même cycle d'horloge. La méthode est simplement indiquée. Divisez simplement les instructions en différentes Le cycle d'exécution, tel que la lecture, l'adressage, l'analyse, l'exécution et d'autres étapes, est traité en différents composants. En même temps, dans l'unité d'exécution EU, l'unité fonctionnelle est divisée en différents composants, tels que des composants d'addition, des composants de multiplication. , et le chargement de composants, d'éléments de stockage, etc., peuvent en outre réaliser une exécution parallèle de différents calculs.
L'architecture du pipeline impose que les instructions soient exécutées en parallèle, et non comme le prévoit le modèle séquentiel. La réorganisation permet d'utiliser pleinement le pipeline, obtenant ainsi des effets superscalaires.
Assurer l'ordre
Bien que les instructions ne soient pas nécessairement exécutées dans l'ordre dans lequel nous les avons écrites, il ne fait aucun doute que dans un environnement monothread, l'effet final de l'exécution de l'instruction doit être cohérent avec son effet dans l'exécution séquentielle, sinon cette optimisation perdra de son importance.
Habituellement, les principes ci-dessus seront satisfaits, que la réorganisation des instructions soit effectuée au moment de la compilation ou au moment de l'exécution.
Réorganisation dans le modèle de stockage Java
Dans le modèle de mémoire Java (JMM), la réorganisation est une section très importante, notamment en programmation simultanée. JMM garantit la sémantique d'exécution séquentielle via la règle de l'opération avant. Si vous souhaitez que le thread effectuant l'opération B observe les résultats du thread effectuant l'opération A, alors A et B doivent satisfaire au principe de l'opération avant. Sinon, la JVM peut effectuer des opérations arbitraires. opérations sur eux. Tri pour améliorer les performances du programme.
Le mot-clé volatile peut garantir la visibilité des variables, car les opérations sur volatile sont toutes dans la mémoire principale et la mémoire principale est partagée par tous les threads. Le prix ici est que les performances sont sacrifiées et que les registres ou le cache ne peuvent pas être utilisés car ils ne sont ni globaux. , la visibilité ne peut pas être garantie et des lectures incorrectes peuvent se produire.
Une autre fonction de volatile est d'empêcher localement la réorganisation des instructions d'opération sur les variables volatiles, car si elles sont réorganisées, des problèmes de visibilité peuvent survenir.
En termes d'assurance de la visibilité, les verrous (y compris les verrous explicites, les verrous d'objet) ainsi que la lecture et l'écriture de variables atomiques peuvent garantir la visibilité des variables. Cependant, les méthodes de mise en œuvre sont légèrement différentes. Par exemple, le verrouillage de synchronisation garantit que les données sont relues de la mémoire pour actualiser le cache lorsque le verrou est obtenu. Lorsque le verrou est libéré, les données sont réécrites dans la mémoire pour garantir. que les données sont visibles, tandis que les variables volatiles lisent et écrivent simplement la mémoire.