Java 8 Instructions non sécuritaires: xxxFence ()
en Java 8, Trois instructions de barrière de mémoire ont été ajoutées à Unsafe
classe ( source):
/**
* Ensures lack of reordering of loads before the fence
* with loads or stores after the fence.
*/
void loadFence();
/**
* Ensures lack of reordering of stores before the fence
* with loads or stores after the fence.
*/
void storeFence();
/**
* Ensures lack of reordering of loads or stores before the fence
* with loads or stores after the fence.
*/
void fullFence();
si nous définissons la barrière de mémoire de la manière suivante (que je considère plus ou moins facile à comprendre):
Considérons X et Y les types d'opération/de classes qui font l'objet de la réorganisation,
X_YFence()
est une instruction de barrière de mémoire qui assure que toutes les opérations de type X avant la barrière terminé avant toute opération de type Y après le début de la barrière.
nous pouvons maintenant "cartographier" les noms des barrières de Unsafe
cette terminologie:
loadFence()
devientload_loadstoreFence()
;storeFence()
devientstore_loadStoreFence()
;fullFence()
devientloadstore_loadstoreFence()
;
Enfin, ma question est: - pourquoi n'avons-nous pas load_storeFence()
,store_loadFence()
,store_storeFence()
et load_loadFence()
?
j'imagine - ils ne sont pas vraiment nécessaire, mais je ne comprends pas pourquoi à l'heure actuelle. Donc, j'aimerais savoir les raisons pour ne pas les ajouter. Les conjectures à ce sujet sont les bienvenues aussi (espérons que cela ne cause pas cette question d'être offtopique en tant que basé sur l'opinion, cependant).
Merci d'avance.
3 réponses
résumé
les cœurs CPU ont des tampons spéciaux de commande de mémoire pour les aider à exécuter les ordres. Ceux-ci peuvent être (et sont généralement) séparés pour le chargement et le stockage: les LOBs pour les tampons d'ordre de charge et les SOBs pour les tampons d'ordre de stockage.
les opérations de clôture choisies pour L'API Non Sécuritaire ont été sélectionnées en fonction des hypothèse: les processeurs sous-jacents auront des tampons d'ordre de charge séparés( pour réordonner les charges), tampons (pour réordonner les magasins).
par conséquent, basé sur cette hypothèse, du point de vue du logiciel, vous pouvez demander une des trois choses au CPU:
- vider les LOBs (loadFence): signifie qu'aucune autre instruction ne commencera à s'exécuter sur ce noyau, jusqu'à ce que toutes les entrées des LOBs aient été traitées. EN x86, c'est un LFENCE.
- vider les SOBs( storeFence): signifie qu'aucune autre instruction ne commencera à s'exécuter sur ce noyau, jusqu'à ce que tous les entrées dans les sob ont été traitées. EN x86, C'est une SFENCE.
- vider les deux LOBs et les sanglots (fullFence): signifie les deux de ce qui précède. En x86 c'est un MFENCE.
en réalité, chaque architecture de processeur spécifique fournit différentes garanties de commande de mémoire, qui peuvent être plus strictes, ou plus flexibles que les précédentes. Par exemple, L'architecture SPARC peut réorganiser les séquences charge-mémoire et charge-mémoire, alors que x86 ne le fera pas. En outre, les architectures il y a des zones où il n'est pas possible de contrôler individuellement les lieux de sépulture et les lieux de sépulture (c'est-à-dire que seule une clôture complète est possible). Dans les deux cas, cependant:
lorsque l'architecture est plus flexible, L'API ne fournit tout simplement pas l'accès aux combinaisons de séquençage "laxer" comme une question de choix
lorsque l'architecture est plus stricte, L'API implémente simplement la garantie de séquençage plus stricte dans tous les cas (par exemple, les 3 appels en fait et plus étant mise en œuvre en tant que clôture complète)
la raison des choix particuliers de L'API est expliquée dans le PEC selon la réponse d'assylias qui est 100% sur place. Si vous connaissez l'ordre de mémoire et la cohérence de cache, la réponse d'assylias devrait suffire. Je pense que le fait qu'ils correspondent à l'instruction standardisée de L'API C++ a été un facteur majeur (simplifie beaucoup la mise en œuvre JVM):http://en.cppreference.com/w/cpp/atomic/memory_order au total selon toute vraisemblance, la mise en œuvre réelle fera appel à l'API C++ respective au lieu d'utiliser des instructions spéciales.
ci-Dessous, j'ai une explication détaillée avec x86 exemples, qui offrent tout le contexte nécessaire pour comprendre ces choses. En fait, la section délimitée ci-dessous répond à une autre question: "Pouvez-vous fournir des exemples de base du fonctionnement des clôtures mémoire pour contrôler la cohérence du cache dans l'architecture x86?"
la raison en est que je moi-même (venant d'un développeur de logiciels et non d'un concepteur de matériel) avait du mal à comprendre ce qu'est la réorganisation de la mémoire, jusqu'à ce que j'apprenne des exemples spécifiques de la façon dont la cohérence du cache fonctionne réellement dans x86. Cela fournit un contexte précieux pour discuter des clôtures mémoire en général (pour d'autres architectures également). A la fin je discute un peu de SPARC en utilisant les connaissances acquises à partir des exemples x86
la référence [1] est une explication encore plus détaillée et comporte une section séparée pour discutant chacun de: x86, SPARC, ARM et PowerPC, il est donc une excellente lecture si vous êtes intéressé par Plus de détails.
x86 exemple d'architecture
x86 fournit 3 types d'instructions de clôture: LFENCE (load fence), SFENCE (store fence) et MFENCE (load-store fence), de sorte qu'il correspond à 100% à L'API Java.
c'est parce que x86 a des tampons d'ordre de charge (LOBs) et des tampons d'ordre de conservation (SOBs) séparés, donc en fait les instructions de LFENCE/SFENCE appliquer au tampon respectif, tandis que MFENCE s'applique aux deux.
les SOBs sont utilisés pour stocker une valeur sortante (du processeur au système de cache) pendant que le protocole de cohérence de cache fonctionne pour acquérir la permission d'écrire sur la ligne de cache. Les LOBs sont utilisés pour stocker les requêtes d'invalidation afin que l'invalidation puisse être exécutée de manière asynchrone (réduit le décrochage du côté récepteur dans l'espoir que le code qui y est exécuté n'aura pas besoin de cette valeur).
magasins hors-service and SFENCE
supposons que vous ayez un système de double processeur avec ses deux CPU, 0 et 1, exécutant les routines ci-dessous. Considérez le cas où la ligne de cache contient failure
est initialement détenu par CPU 1, alors que la ligne de cache contient shutdown
est initialement détenu par CPU 0.
// CPU 0:
void shutDownWithFailure(void)
{
failure = 1; // must use SOB as this is owned by CPU 1
shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
while (shutdown == 0) { ... }
if (failure) { ...}
}
en l'absence d'une clôture de stockage, le CPU 0 peut signaler un arrêt dû à une défaillance, mais le CPU 1 sort de la boucle et ne rentre pas dans le bloc de gestion de la défaillance.
C'est parce que CPU0 écrira la valeur 1 pour failure
à un tampon de commande de stockage, en envoyant également un message de cohérence de cache pour acquérir un accès exclusif à la ligne de cache. Il passera ensuite à l'instruction suivante (en attendant l'accès exclusif) et mettra à jour le shutdown
drapeau immédiatement (cette ligne de cache est détenue exclusivement par CPU0 déjà, donc pas besoin de négocier avec d'autres noyaux). Enfin, lorsqu'il reçoit par la suite un message de confirmation D'invalidation de la part de CPU1 (concernant failure
), il sera procédez au traitement du SOB pour failure
et écrire la valeur dans le cache (mais l'ordre est inversé).
Insertion d'une storeFence() va arranger les choses:
// CPU 0:
void shutDownWithFailure(void)
{
failure = 1; // must use SOB as this is owned by CPU 1
SFENCE // next instruction will execute after all SOBs are processed
shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
while (shutdown == 0) { ... }
if (failure) { ...}
}
un dernier aspect qui mérite d'être mentionné est que x86 a un transfert de stock: lorsqu'un CPU écrit une valeur qui est bloquée dans un SOB (en raison de la cohérence du cache), il peut ensuite tenter d'exécuter une instruction de chargement pour la même adresse avant que le SOB ne soit traité et livré au cache. Cpu seront par conséquent, consultez les sob avant d'accéder au cache, de sorte que la valeur récupérée dans ce cas est la dernière valeur écrite de la sob. cela signifie que les stocks de ce noyau ne peuvent jamais être réordonnés avec les charges subséquentes de ce noyau peu importe ce que.
charges hors service et pertes
maintenant, supposons que vous avez la barrière du magasin en place et que vous êtes heureux que shutdown
ne peut pas dépasser failure
en route vers CPU 1, et se concentrer de l'autre côté. Même dans le présence de la clôture du magasin, Il ya des scénarios où la mauvaise chose se produit. Considérons le cas où failure
dans les deux caches (partagé) alors que shutdown
n'est présent que dans le cache de CPU0 et appartient exclusivement à celui-ci. Les mauvaises choses peuvent se produire comme suit:
- CPU0 écrit 1 à
failure
; il envoie également un message à CPU1 pour invalider sa copie de la ligne de cache partagée dans le cadre du protocole de cohérence du cache. - CPU0 exécute la SFENCE et stands, en attendant le SOB utilisé pour
failure
à s'engager. - contrôles CPU1
shutdown
en raison de la boucle while et (réalisant qu'il manque la valeur) envoie un message de cohérence de cache pour lire la valeur. - CPU1 reçoit le message de CPU0 à l'étape 1 pour invalider
failure
, envoi d'un accusé de réception immédiat. NOTE: Ceci est implémenté en utilisant la file d'attente d'invalidation, donc en fait il entre simplement une note (attribue une entrée dans sa LOB) pour faire plus tard la l'Annulation, mais ne l'exécute pas avant l'envoi de l'accusé de réception. - CPU0 reçoit l'accusé de réception pour
failure
et produit passé la SFENCE à la prochaine instruction - CPU0 écrit 1 à shutdown sans utiliser un SOB, car il possède déjà la ligne de cache exclusivement. aucun message supplémentaire pour invalidation n'est envoyé car la ligne de cache est exclusive à CPU0
- CPU1 reçoit le
shutdown
valeur et l'engage à son cache local, allant à la ligne suivante. - CPU1 vérifie le
failure
valeur pour la déclaration if, mais puisque la queue invalidate (note LOB) n'est pas encore traitée, elle utilise la valeur 0 de son cache local (n'entre pas si le bloc). - processes CPU1 the invalidate queue and update
failure
à 1, mais il est déjà trop tard...
ce que nous appelons les tampons d'ordre de charge, est actaully la mise en file d'attente des demandes d'invalidation, et ce qui précède peut être fixe avec:
// CPU 0:
void shutDownWithFailure(void)
{
failure = 1; // must use SOB as this is owned by CPU 1
SFENCE // next instruction will execute after all SOBs are processed
shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
while (shutdown == 0) { ... }
LFENCE // next instruction will execute after all LOBs are processed
if (failure) { ...}
}
votre question sur x86
maintenant que vous savez ce que font les sanglots/LOBs, pensez aux combinaisons que vous avez mentionnées:
loadFence() becomes load_loadstoreFence();
non, une barrière de charge attend pour les LOBs à traiter, vidant essentiellement la file d'attente d'invalidation. Cela signifie que tous les chargements suivants verront des données à jour (pas de nouvelle commande), car elles seront récupérées à partir du sous-système de cache (qui est cohérent). Les stocks ne peuvent pas être réordonnés avec des chargements ultérieurs, car ils ne passent pas par le lobe. (et en outre, le transfert de stockage s'occupe des lignes de cachce modifiées localement) du point de vue de ce noyau particulier (celui qui exécute la barrière de charge), un stockage qui suit la barrière de charge s'exécutera après que tous les registres ont les données chargées. Il n'y a pas moyen de contourner cela.
load_storeFence() becomes ???
il n'y a pas besoin de load_storeFence car cela n'a pas de sens. Pour stocker quelque chose vous devez le calculer en utilisant l'entrée. Pour récupérer les entrées, vous devez exécuter des charges. Les sauvegardes se feront en utilisant les données récupérées à partir des chargements. Si vous voulez vous assurer de voir les valeurs à jour de tous les autres processeurs lors du chargement, utilisez loadFence. Pour les charges après la clôture, le transfert de stock s'occupe de la cohérence de la commande.
Tous les autres cas sont similaires.
SPARC
SPARC est encore plus flexible et peut réorganiser les stocks avec les charges subséquentes (et les charges avec les stocks subséquents). Je n'étais pas aussi familier avec SPARC, donc mon GUESS était qu'il n'y avait pas de transfert de stock (les sob ne sont pas consultés lors du rechargement d'une adresse) donc "des lectures sales" sont possibles. En fait, j'avais tort: J'ai trouvé L'architecture SPARC dans [3] et la réalité est que le transfert de magasin est fileté. De la section 5.3.4:
tous les chargements vérifiez le tampon de stockage (même thread seulement) pour les risques de lecture après écriture (RAW). Une RAW complète se produit lorsque l'adresse dword de la charge correspond à celle d'un magasin dans le STB et tous les octets du les chargements sont valables dans le tampon de stockage. Une RAW partielle se produit lorsque les adresses dword concordent, mais tous les octets ne sont pas valides dans la mémoire tampon. (Ex., un ST (word store) suivi D'un LDX (dword load) à la même adresse donne une RAW partielle, parce que le dword complet n'est pas dans l'entrée de mémoire tampon.)
ainsi, différents threads consultent des tampons de commande de magasin différents d'où la possibilité pour des lectures sales après commerces.
Références
[1] les Barrières de la Mémoire: un Affichage Matériel pour les Hackers de Logiciels, Linux, Centre de la Technologie, IBM Beaverton http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf
[2] Intel ® 64 et IA-32 Architectoftware Developer's Manual, Volume 3A http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.pdf
[3] Spécification De Microarchitecture De Base T2 D'OpenSPARC http://www.oracle.com/technetwork/systems/opensparc/t2-06-opensparct2-core-microarch-1537749.html
Une bonne source d'information est le JEP 171 lui-même.
justification:
les trois méthodes fournissent les trois types différents de clôtures de mémoire dont certains compilateurs et processeurs ont besoin pour s'assurer que les accès particuliers (charges et magasins) ne sont pas réordonnés.
mise en Œuvre (extrait):
pour les versions d'exécution C++ (dans prims / unsafe.cpp), mise en œuvre par le biais de L'accès aux ordres existants méthodes:
loadFence: { OrderAccess::acquire(); }
storeFence: { OrderAccess::release(); }
fullFence: { OrderAccess::fence(); }
en d'autres termes, les nouvelles méthodes sont étroitement liées à la façon dont les barrières de mémoire sont mises en œuvre aux niveaux JVM et CPU. Ils correspondent aussi à l' barrière de mémoire les instructions disponibles en C++, la langue dans laquelle le hotspot est implémenté.
par exemple si vous regardez la table des instructions cpu dans the JSR 133 Cookbook, vous verrez que LoadStore et LoadLoad sont reliés aux mêmes instructions sur la plupart des architectures, c'est-à-dire que les deux sont effectivement des instructions Load_LoadStore. Donc avoir un seul Load_LoadStore (loadFence
) l'instruction au niveau JVM semble être une décision de conception raisonnable.
La doc pour storeFence() est faux. Voir https://bugs.openjdk.java.net/browse/JDK-8038978
loadFence() est LoadLoad plus LoadStore, si utile, souvent appelé acquérir enceinte.
storeFence() est StoreStore plus LoadStore, si utile, souvent appelé la libération de la clôture.
LoadLoad LoadStore stores sont des clôtures bon marché (nop sur x86 ou Sparc, bon marché sur L'énergie, peut-être cher sur le bras).
IA64 a des instructions différentes pour acquérir et relâchez la sémantique.
fullFence () is LoadLoad LoadStore Storeplore plus Storeplace.
StordLoad clôture est cher (sur presque tous les CPU), presque aussi cher que le plein de la clôture.
cela justifie la conception de L'API.