7
votes

La meilleure façon de mettre à jour atomiblement deux membres d'une structure?

J'ai une struct qui contient deux membres de données: xxx

et un vecteur contenant bon nombre de ces objets de structure: xxx

à Runtime Les membres de données AB et AC à partir d'objets de structure sont définis sur zéro / une valeur x . Toutefois, B et C doit être modifié en même temps - on ne peut pas être mis à jour séparément sans l'autre. Je prévoyais d'utiliser un compare_exchange_weak () pour effectuer la mise à jour.

Je ne suis pas sûr que je devrais représenter chaque structure sous forme de std :: atomic dans le vecteur, ou si je devrais placer une union à l'intérieur de la structure, combinez les deux shorts dans un seul uint32_t , puis modifiez ceci: xxx

Quelle serait la meilleure solution? Devrais-je stocker un vecteur de: xxx

et lors de l'accès à chaque élément, réinterpret_cast à A, pour obtenir d

Je voudrais que le verrouillage soit le moins intrusif possible.

La portabilité n'est pas requise, ce sera sur Linux, 64 bits, X86, GCC 4.8 + compilateur


9 commentaires

Je déclare B et C comme privé et écrire une seule fonction qui modifie les deux.


Vous ne pouvez pas vraiment utiliser std :: atomic <> intérieur des conteneurs (atomiques ne sont pas copieux ou mobiles). Si le nombre d'éléments du vecteur est corrigé et connu à l'avance, vous pouvez construire le vecteur avec le bon nombre d'éléments, tant que vous n'essayez jamais d'insérer / effacer des éléments


Pourquoi un réinterpret_cast à partir de uint32_t (qui serait un comportement indéfini) au lieu de stocker le type de syndicat dans le vecteur et de lire / écrire directement les membres directement? Qui repose sur le type punning, mais c'est soutenu par de nombreux compilateurs. Cela ne ferait pas les mises à jour atomiques, vous devez utiliser des opérations atomiques pour cela.


Sont struct s définis sans rembourrage s'ils font partie d'un Union ? Sinon, votre proposition semble assez risquée ...


Personnellement, je fusionnerais B et C dans un seul type intégré atomique de la taille correcte. Utiliser des opérations bitwises pour définir diverses parties de ce type. Votre syndicat ne sera pas portable en raison de l'emballage de structure.


@Bathsheba Puis-je aligner les deux membres de 16 bits à des frontières de 2 octets?


Pas de manière portable, non. Si vous sacrifiez la portabilité, vous pouvez aussi bien recourir à l'assemblage et utiliser une serrure de bus.


@Filmor, il n'est pas autorisé à effectuer un remplissage au début d'une structure de mise en page standard, et il est peu probable que le rembourrage entre deux shorts sur une plate-forme commune. A static_assert pourrait être utilisé pour vérifier cela.


Que se passe-t-il lorsque le vecteur redimensionne? Cela fera une copie et aucune atomicité n'est garantie. Il est presque certain que vous n'ayez pas Toujours Besoin de la garantie que vous demandez: Quand avez-vous besoin de cette garantie? Qui d'autre accède aux données et comment?


4 Réponses :


2
votes

Je recommande d'envelopper les variables dans une classe avec un getter et un setter gardé par un mutex et faites les variables privées.

L'utilisation d'un syndicat pourrait provoquer une fonctionnalité imprévue basée sur l'architecture de la machine et les drapeaux de compilateur.

Modifier Résultats d'exécution d'un programme simple qui stocke des valeurs du type de structure donné (Linux 32bit, X86):

  • Simple Store (pas de protection du tout) -> ~ 4000 US
  • MOUTEX GARTED MAGASIN -> ~ 12000 US
  • Utilisation d'un syndicat avec un champ d'agrégation atomique -> ~ 21000 US

2 commentaires

MuTex est trop lourd. Cela doit être très rapide :)


L'avez-vous testé dans votre configuration de l'impact? Un test simple (1000.000 magasins sur une variable du type donné) J'ai reçu: -Le magasin Simple (AKA. Pas de protection) -> ~ 4000 US -MUTED Garded Store -> ~ 12000 US-Automic Store utilisant un syndicat -> ~ 21000 US Testés sur un Linux 32 bits, X86



0
votes

Étant donné que la portabilité n'est pas un problème, le verrouillage n'est que comme intrusif que vous voudriez que cela soit (prenant "intrusif" pour signifier la boucle CAS ou le verrouillage excessif).

Dans votre cas, vous pouvez utiliser le Atomic Bountrins directement.

afaik, vous devriez être capable de mélanger des tailles de mots pour fonctionner directement sur B et C , ainsi que A . Je n'ai jamais fait ça, donc je ne peux pas dire si cela fonctionnerait de manière fiable sur toutes choses x86. Comme toujours: prototype et test, test, test!



1
votes

Faire simplement un Union code> d'un type atomique assez grand. C'est ce que j'utilise (l'extrait de code n'est pas parfaitement em> portable, à l'aide de (code> (code> au lieu de court code> et int code> serait sûrement préférable - mais c'est assez bon pour moi tel qu'il est), et cela fonctionne parfaitement bien et de manière fiable depuis ... Pratiquement pour toujours:

union A {
    struct {
         short b;
         short c;
    };

    std::atomic<int> d;
};


8 commentaires

J'aurais dû dire que la portabilité n'est pas requise.


@user: Si vous savez avec certitude quelle taille int et court sont sur votre plate-forme et que vous ne prévoyez pas de porter le code à un autre, alors c'est parfaitement parfaitement bien (C'est pourquoi j'utilise int , pas int32_t aussi). Mais même si c'est une contrepartie ou une exigence, les modifications sont minuscules :-)


Damon, comment utiliseriez-vous cela avec comparer_and_exchange? Vous rangez Vector puis invoquerez votre comparez_and_exchange sur D? Vous auriez besoin de charger une version non atomique de D comme argument pour comparer_and_exchange?


Oui, vous lisiez la valeur d'E.G. VEC [i] .d (ou iter-> d ) dans un (" attendu "), puis faites quelque chose comme VEC [I] .D.Compare_exchange_weak (attendu, souhaité); . Vous pouvez également utiliser la version «forte», mais elle n'a vraiment pas beaucoup d'avantages. Comme il peut échouer dans les deux sens, vous devez vérifier le résultat et la boucle dans tous les cas. Bien que la version "faible" puisse défaillonner, il n'y a pas vraiment une grande différence (mais "mais" faible "peut être plus rapide). Notez également que vous DOIT RESTAURER attendu si le PO échoue (il sera écrasé!). Oublier de le faire est une source de douleur commune.


Je suis sûr que std :: atomic est sans verrouillage sur la base x86-64. Pensez-vous au fait que CMPXCHG16B n'est pas de base (car il manquait de K8)? Vous n'en avez pas besoin pour comparer_exchange_weak A Single Pointeur; 64 bits CMPXCHG fonctionne bien pour cela et les compilateurs émettent que même sans -mcx16 . (Voir Cette réponse pour une astuce d'union similaire pour séparément atomique Ou un accès atomique tout aussi entier à une paire de pointeurs (ou d'indicateur pointeur +). Par exemple, atomique b, c .


Notez également que Contrairement à C, en C ++, il n'est pas garanti d'être portable d'écrire un membre de l'Union, puis de lire une autre, il n'est donc que portable des compilateurs où cela va bien (par exemple toute saveur de GNU C et probablement beaucoup d'autres).


@Petercordes: disant "non garanti" est en effet très indulgent. Un programme qui fait appel à un comportement indéfini (avec l'exception de préfixe commune, qui ne s'applique toutefois pas ici). Néanmoins, il s'agit d'une solution pragmatique qui "fonctionne bien" sous les hypothèses évidentes qui sont faites ici, telles que deux courts S n'ont aucun rembourrage supplémentaire entre eux et deux court s sont la taille d'un int . Généralement, pour les unions de la seule pod ou des types "principalement de POD" tels qu'un entier atomique, ce type de UB "fonctionne simplement". Cela se bloquerait évidemment et brûlerait étant donné un type qui allouait des ressources.


@DAMON: Les problèmes potentiels dont je parlais sont le même genre que vous pouvez obtenir de violer un aliasing strict (par exemple en lisant le deuxième court avec (réinterpret_cast *> (& D ) +1) -> charge () . Le programme peut sembler violer la causalité et qu'un magasin vers l'un des courte S pourrait arriver éventuellement, mais ne pas être reflété dans une lecture de Le atomic juste après. Évidemment, les mises en page doivent se chevaucher la façon dont vous vous attendez, mais ce n'est pas tout ce dont vous avez besoin pour garantir que le type punning de l'Union fonctionne.



4
votes

Sauf si le matériel que vous ciblez les supports double comparaison-and-swap (ce qui n'est probablement pas le cas), je pense que vous n'avez que deux solutions portables:

  1. introduisez une serrure de niveau supérieur (mutex ou spinlock en fonction de vos préférences) et transportez toutes les opérations sur B et C dans la portée de la serrure acquise . Un mutex est lourd, mais std :: atomic_flag est sans verrou et très léger, même dans des situations de haut niveau.

  2. fusionne les deux membres en un seul std :: atomic et scinder que int dans court s via un bit masquing . Notez que cela nécessite Tailleof (int)> = 2 * Taille de taille (court) . Utilisez des types d'entiers de taille fixe si vous devez appliquer cela.

    Pour déterminer quelle solution est les points de repère les plus rapides, bien sûr.

    Si vous connaissez le nombre de struct A , vous aurez besoin au moment de la compilation, je vous suggérerais de les mettre dans un std :: Array . Si vous ne le faites pas, std :: vecteur est bien tant que ce numéro reste constant tout au long de la vie du vecteur. Sinon, étant donné que std :: atomic n'est ni copible ni mobile, vous va écrire votre propre copie / déplacement constructeur pour struct A .


4 commentaires

J'aurais dû dire que la portabilité n'est pas requise.


La référence du CPP a même un exemple de Spinlock implémenté à l'aide de atomic_flag < / code>.


@Park Que se passe-t-il si je mets structurer un tableau de style C brut?


Si vous connaissez le nombre d'éléments à la compilation, je suggère d'utiliser std :: graphique à la place. Si vous ne le faites pas, std :: vecteur est bien tant que ce numéro reste constant tout au long de la vie du vecteur. Sinon, vous devez écrire une copie ou déplacer constructeur.