Comment écrire des valeurs multi-octets dans la mémoire partagée en C ++ 14?
Supposons que j'ai deux processus qui partagent tous deux un bloc de mémoire en utilisant shm_open
et mmap
et qu'il existe une primitive de synchronisation partagée - disons un sémaphore - qui assure un accès exclusif à la mémoire. Ie pas de conditions de course.
Je crois comprendre que le pointeur renvoyé par mmap
doit toujours être marqué comme volatile pour éviter les lectures mises en cache.
Maintenant, comment écrire par exemple a std::uint64_t
dans une position alignée dans la mémoire?
Naturellement, j'utiliserais simplement std::memcpy
mais cela ne fonctionne pas avec des pointeurs vers la mémoire volatile.
Premier essai
// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;
// Value to store, initialize "randomly" to prevent compiler
// optimization, for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(nullptr);
// Store byte-by-byte
unsigned char* src = reinterpret_cast<unsigned char*>(&value);
for(std::size_t i=0;i<sizeof(value);++i)
ptr[i]=src[i];
Godbolt .
Je crois fermement que cette solution est correcte mais même avec -O3
, il y a 8 transferts de 1 octet. Ce n'est vraiment pas optimal.
Deuxième essai
Puisque je sais que personne ne changera la mémoire pendant que je la verrouille, peut-être que le volatile n'est pas nécessaire après tout?
// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;
// Value to store, initialize "randomly" to prevent compiler
// optimization for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(0xAA);
unsigned char* src = reinterpret_cast<unsigned char*>(&value);
//Obscure enough?
auto* real_ptr = reinterpret_cast<unsigned char*>(reinterpret_cast<std::uintptr_t>(ptr));
std::memcpy(real_ptr,src,sizeof(value));
Godbolt .
Mais cela ne semble pas fonctionner, le compilateur voit à travers le casting et ne fait rien. Clang génère des ud2
instructions, je ne sais pas pourquoi, y a-t-il UB dans mon code? En dehors de l' value
initialisation.
Troisième tentative
Celui-ci vient de cette réponse . Mais je pense que cela enfreint la règle stricte d'aliasing, n'est-ce pas?
// Pointer to the shared memory, assume it is aligned correctly.
volatile unsigned char* ptr;
// Value to store, initialize "randomly" to prevent compiler
// optimization for testing purposes.
std::uint64_t value = *reinterpret_cast<volatile std::uint64_t*>(0xAA);
unsigned char* src = reinterpret_cast<unsigned char*>(&value);
volatile std::uint64_t* dest = reinterpret_cast<volatile std::uint64_t*>(ptr);
*dest=value;
Godbolt .
Gcc fait réellement ce que je veux - une simple instruction pour copier la valeur 64 bits. Mais c'est inutile si c'est UB.
Une façon de résoudre ce problème est de vraiment créer un std::uint64_t
objet à cet endroit. Mais, apparemment, le placement nouveau ne fonctionne pas non plus avec les volatile
pointeurs.
Des questions
- Alors, y a-t-il un meilleur moyen (sûr) que la copie octet par octet?
- Je voudrais également copier des blocs encore plus grands d'octets bruts. Cela peut-il être fait mieux que par octets individuels?
- Y a-t-il une possibilité de forcer à
memcpy
faire ce qu'il faut? - Dois-je m'inquiéter inutilement des performances et devrais-je simplement suivre la boucle?
- Des exemples (principalement C) ne sont pas du tout utilisés
volatile
, dois-je le faire aussi? Est -cemmap
pointeur ed traité différemment déjà? Comment?
Merci pour vos suggestions.
ÉDITER:
Les deux processus s'exécutent sur le même système. Supposons également que les valeurs puissent être copiées octet par octet, sans parler de classes virtuelles complexes stockant des pointeurs vers quelque part. Tous les nombres entiers et pas de flotteurs seraient très bien.
Réponses
Je crois comprendre que le pointeur renvoyé par mmap doit toujours être marqué comme volatile pour empêcher les lectures mises en cache.
Votre compréhension est fausse. Ne l'utilisez pas volatile
pour contrôler la visibilité de la mémoire - ce n'est pas à cela qu'il sert. Ce sera soit inutilement coûteux, soit insuffisamment strict, soit les deux.
Prenons l'exemple de la documentation GCC sur les volatiles , qui dit:
Les accès aux objets non volatils ne sont pas ordonnés par rapport aux accès volatils. Vous ne pouvez pas utiliser un objet volatil comme barrière de mémoire pour ordonner une séquence d'écritures dans la mémoire non volatile
Si vous voulez simplement éviter le déchirement, la mise en cache et la réorganisation, utilisez à la uint64_t
(et qu'il est correctement aligné), accédez-y simplement via un fichier std::atomic_ref
Si vous avez besoin d'une synchronisation normale, votre sémaphore existant fera l'affaire. Comme ci-dessous, il fournit déjà toutes les clôtures nécessaires et empêche la réorganisation des appels d'attente / post. Cela n'empêche pas la réorganisation ou d'autres optimisations entre eux, mais c'est généralement bien.
Pour ce qui est de
Tous les exemples (principalement C) n'utilisent pas du tout volatile, dois-je le faire aussi? Le pointeur mmaped est-il déjà traité différemment? Comment?
la réponse est que quelle que soit la synchronisation utilisée, il faut également appliquer les clôtures appropriées.
POSIX répertorie ces fonctions en tant que «mémoire de synchronisation», ce qui signifie qu'elles doivent à la fois émettre toutes les barrières de mémoire requises et empêcher une réorganisation inappropriée du compilateur. Ainsi, par exemple, votre implémentation doit éviter de déplacer les accès à la mémoire entre les appels pthread_mutex_*lock()
ou sem_wait()
/ sem_post()
afin d'être conforme à POSIX, même là où il s'agirait autrement de C ou C ++ légal.
Lorsque vous utilisez le thread intégré ou la prise en charge atomique de C ++, la sémantique correcte fait partie du standard du langage au lieu d'une extension de plate-forme (mais la mémoire partagée ne l'est pas).
Supposons que j'ai deux processus qui partagent tous deux un bloc de mémoire en utilisant shm_open et mmap et qu'il existe une primitive de synchronisation partagée - disons un sémaphore - qui assure un accès exclusif à la mémoire. Ie pas de conditions de course.
Vous avez besoin de plus qu'un accès exclusif à la mémoire. Vous devez synchroniser la mémoire. Chaque sémaphore que j'ai vu le fait déjà. Si ce n'est pas le cas, c'est la mauvaise primitive de synchronisation. Passez à un autre.
Je crois comprendre que le pointeur renvoyé par mmap doit toujours être marqué comme volatile pour empêcher les lectures mises en cache.
Eh bien, cela volatile
n'empêche pas les lectures en cache, mais presque tous les sémaphores, mutex et autres primitives de synchronisation agissent comme s'ils empêchaient les lectures et les écritures en cache. Sinon, ils seraient presque impossibles à utiliser.
Quel sémaphore utilisez-vous? S'il ne synchronise pas la mémoire, c'est le mauvais outil pour le travail.