web-dev-qa-db-fra.com

Que fait le compilateur C ++ pour s'assurer que des emplacements de mémoire différents mais adjacents peuvent être utilisés en toute sécurité sur différents threads?

Disons que j'ai un struct:

struct Foo {
  char a;  // read and written to by thread 1 only
  char b;  // read and written to by thread 2 only
};

D'après ce que j'ai compris, la norme C++ garantit la sécurité de ce qui précède lorsque deux threads opèrent sur deux emplacements de mémoire différents.

Je pense cependant que, puisque les caractères a et b sont situés dans la même ligne de cache, le compilateur doit effectuer une synchronisation supplémentaire.

Qu'est-ce qui se passe exactement ici?

43
Nathan Doromal

Ceci dépend du matériel. Sur le matériel que je connais bien, C++ n’a rien de spécial à faire, car du point de vue matériel, accéder à différents octets, même sur une ligne mise en cache, est géré de manière "transparente". Du point de vue matériel, cette situation n’est pas vraiment différente de

char a[2];
// or
char a, b;

Dans les cas ci-dessus, nous parlons de deux objets adjacents, dont l'accès est garanti.

Cependant, j'ai mis "de manière transparente" entre guillemets pour une raison. Lorsque vous avez réellement ce type de cas, vous risquez de souffrir (d'un point de vue des performances) d'un "faux partage", ce qui se produit lorsque deux (ou plusieurs) threads accèdent simultanément à la mémoire adjacente et que celle-ci finit par être mise en cache dans plusieurs caches du processeur. Cela entraîne une invalidation constante du cache. Dans la vie réelle, il faut veiller à ce que cela ne se produise pas lorsque cela est possible.

36
SergeyA

Comme d'autres l'ont expliqué, rien de particulier sur le matériel commun. Cependant, il existe un problème: le compilateur doit s'abstenir d'effectuer certaines optimisations, sauf s'il peut prouver que d'autres threads n'accèdent pas aux emplacements de mémoire en question, par exemple:

std::array<std::uint8_t, 8u> c;

void f()
{
    c[0] ^= 0xfa;
    c[3] ^= 0x10;
    c[6] ^= 0x8b;
    c[7] ^= 0x92;
}

Ici, dans un modèle de mémoire à un seul thread, le compilateur pourrait émettre le code suivant (pseudo-Assembly; suppose le matériel little-endian):

load r0, *(std::uint64_t *) &c[0]
xor r0, 0x928b0000100000fa
store r0, *(std::uint64_t *) &c[0]

Cela est probablement plus rapide sur le matériel courant que de xorer les octets individuels. Cependant, il lit et écrit les éléments non affectés (et non mentionnés) de c aux indices 1, 2, 4 et 5. Si d'autres threads écrivent simultanément sur ces emplacements de mémoire, ces modifications pourraient être écrasées.

Pour cette raison, les optimisations de ce type sont souvent inutilisables dans un modèle de mémoire multithread. Tant que le compilateur effectue uniquement des chargements et des magasins de longueur correspondante, ou ne fusionne les accès que s’il n’ya pas d’espace vide (par exemple, les accès à c[6] et c[7] peuvent encore être fusionnés), le matériel fournit déjà les garanties nécessaires à une exécution correcte.

(Cela dit, certaines architectures ont des garanties d'ordre de mémoire faibles et contre-intuitives, par exemple, DEC Alpha ne suit pas les pointeurs en tant que dépendance de données comme d'autres architectures, il est donc nécessaire d'introduire une barrière de mémoire explicite dans certaines cas, en code de bas niveau. Il y a un peu connu petite diabolique de Linus Torvalds sur cette question . Cependant, une implémentation conforme C++ est devrait vous protéger de tels problèmes.)

21
Arne Vogel