Performances relatives des verrous swap vs compare-and-swap sur x86

Deux idiomes de verrouillage communs sont:

if (!atomic_swap(lockaddr, 1)) /* got the lock */

Et:

if (!atomic_compare_and_swap(lockaddr, 0, val)) /* got the lock */

val pourrait simplement être une constante ou un identificateur pour le nouveau propriétaire potentiel de la serrure.

Ce que j'aimerais savoir, c'est s'il y a une différence de performance significative entre les deux sur les machines x86 (et x86_64). Je sais que c'est une question assez large puisque la réponse peut varier beaucoup entre les modèles de cpu individuels, mais cela fait partie de la raison pour laquelle je le demande plutôt que de simplement le faire benchmarks sur quelques processeurs auxquels j'ai accès.

22
demandé sur R.. 2011-03-17 16:37:05

5 réponses

Je suppose qu'atomic_swap(lockaddr, 1) est traduit en une instruction xchg reg,mem et atomic_compare_and_swap (lockaddr, 0, val) est traduit en cmpxchg[8b|16b].

Certains développeurs du noyau linux pensent que cmpxchg est plus rapide, car le préfixe de verrouillage n'est pas implicite comme avec xchg. Donc, si vous êtes sur un monoprocesseur, multithread ou autrement, assurez-vous que le verrou n'est pas nécessaire, vous êtes probablement mieux avec cmpxchg.

Mais il y a de fortes chances que votre compilateur le traduise en un "verrouiller cmpxchg" et dans ce cas elle n'a pas vraiment d'importance. Notez également que si les latences pour ces instructions sont faibles (1 cycle sans verrou et environ 20 avec verrou), si vous utilisez une variable de synchronisation commune entre deux threads,ce qui est assez habituel, des cycles de bus supplémentaires seront appliqués, qui durent éternellement par rapport aux latences d'instruction. Ceux - ci seront probablement complètement cachés par un cache de 200 ou 500 cycles cpu long Snoop/sync/mem access/bus lock/whatever.

13
répondu Gunther Piez 2011-03-17 15:21:10

J'ai trouvé ce document Intel, indiquant qu'il n'y a pas de différence dans la pratique:

Http://software.intel.com/en-us/articles/implementing-scalable-atomic-locks-for-multi-core-intel-em64t-and-ia32-architectures/

Un mythe commun est que le verrou utilisant une instruction cmpxchg est moins cher qu'un verrou utilisant une instruction xchg. Ceci est utilisé car cmpxchg n'essaiera pas d'obtenir le verrou en mode exclusif puisque le cmp passera en premier. La Figure 9 montre que le cmpxchg est tout aussi cher que l'instruction xchg.

13
répondu Bo Persson 2011-03-17 15:28:35

Sur x86, toute instruction avec un préfixe de verrouillage effectue toutes les opérations de mémoire en tant que cycles de lecture-modification-écriture. Cela signifie que XCHG (avec son verrou implicite) et LOCK CMPXCHG (dans tous les cas, même si la comparaison échoue) obtiennent toujours un verrou exclusif sur la ligne de cache. Le résultat est qu'il n'y a fondamentalement aucune différence de performance.

Notez que de nombreux processeurs tournant tous sur le même verrou peuvent causer beaucoup de surcharge de bus dans ce modèle. C'est l'une des raisons pour lesquelles les boucles spin-lock devraient contenir PAUSE instructions. Certaines autres architectures ont de meilleures opérations pour cela.

3
répondu Mycroft 2012-08-17 07:13:08

Êtes-vous sûr que vous ne vouliez pas dire

 if (!atomic_load(lockaddr)) {
       if (!atomic_swap(lockaddr, val)) /* got the lock */

Pour le second?

Test et test et définir des verrous (voir Wikipedia https://en.wikipedia.org/wiki/Test_and_test-and-set ) sont une optimisation assez courante pour de nombreuses plates-formes.

Selon la façon dont compare et exchange est implémenté, il peut être plus rapide ou plus lent qu'un test et un test et un set.

Comme x86 est une plate-forme ordonnée relativement plus forte les optimisations HW qui peuvent rendre le test et le test et définir des verrous plus rapides peuvent être de moins en moins possible.

Figure 8 du document trouvé par Bo Persson http://software.intel.com/en-us/articles/implementing-scalable-atomic-locks-for-multi-core-intel-em64t-and-ia32-architectures/ montre que les verrous Test et Test et Set sont supérieurs en performance.

2
répondu Steven Stewart-Gallus 2017-04-24 21:51:56

En termes de performance sur les processeurs Intel, c'est la même chose, mais par souci de simplicité, pour avoir des choses plus faciles à comprendre, je préfère la première façon à partir des exemples que vous avez donnés. Il n'y a aucune raison d'utiliser cmpxchg pour acquérir un verrou si vous pouvez le faire avec xchg.

Selon le principe du rasoir D'Occam, les choses simples sont meilleures.

En outre, le verrouillage avec xchg est plus puissant - vous pouvez également vérifier l'exactitude de la logique de votre logiciel, c'est-à-dire que vous n'ACCÉDEZ PAS à l'octet mémoire qui n'a pas été explicitement alloué pour le verrouillage, ou que vous ne déverrouillez pas deux fois.

Il n'y a pas de consensus sur la question de savoir si la libération d'un verrou devrait être juste un magasin normal ou un magasin lock-ed. Par exemple, LeaveCriticalSection sous Windows 10 utilise lock - ed store pour libérer le verrou même sur un processeur à une seule prise; tandis que sur plusieurs processeurs physiques avec un accès mémoire Non uniforme (NUMA) , la question de savoir comment libérer le verrou: un magasin normal vs un lock-magasin ed peut être encore plus important.

Voir cet exemple de fonctions de verrouillage plus sûres qui vérifient la validité des données et interceptent les tentatives de libérer des verrous qui n'ont pas été acquis:

const
  cLockAvailable = 107; // arbitrary constant, use any unique values that you like, I've chosen prime numbers
  cLockLocked    = 109;
  cLockFinished  = 113;

function AcquireLock(var Target: LONG): Boolean; 
var
  R: LONG;
begin
  R := InterlockedExchange(Target, cLockByteLocked);
  case R of
    cLockAvailable: Result := True; // we've got a value that indicates that the lock was available, so return True to the caller indicating that we have acquired the lock
    cLockByteLocked: Result := False; // we've got a value that indicates that the lock was already acquire by someone else, so return False to the caller indicating that we have failed to acquire the lock this time
      else
        begin
          raise Exception.Create('Serious application error - tried to acquire lock using a variable that has not been properly initialized');
        end;
    end;
end;

procedure ReleaseLock(var Target: LONG);
var
  R: LONG;
begin
  // As Peter Cordes pointed out (see comments below), releasing the lock doesn't have to be interlocked, just a normal store. Even for debugging we use normal load. However, Windows 10 uses locked release on LeaveCriticalSection.
  R := Target;
  Target := cLockAvailable;
  if R <> cLockByteLocked  then
  begin
    raise Exception.Create('Serious application error - tried to release a  lock that has not been actually locked');
  end;
end;

Votre application principale va ici:

var
  AreaLocked: LONG;
begin
  AreaLocked := cLockAvailable; // on program initialization, fill the default value

  .... 

 if AcquireLock(AreaLocked) then
 try
   // do something critical with the locked area
   ... 

 finally
   ReleaseLock(AreaLocked); 
 end;

....

  AreaLocked := cLockFinished; // on program termination, set the special value to catch probable cases when somebody will try to acquire the lock

end.

Vous pouvez également utiliser le code suivant comme une boucle de rotation, il utilise une charge normale pendant la rotation pour économiser des ressources, comme suggéré par Peter Cordes. Après 5000 cycles, il appelle la fonction API Windows SwitchToThread (). Cette valeur de 5000 cycles est mon empirique. Les valeurs de 500 à 50000 semblent également être correctes, dans certains scénarios, les valeurs inférieures sont meilleures tandis que dans d'autres, les valeurs supérieures sont meilleures. Veuillez noter que vous ne pouvez utiliser ce code que sur les processeurs prenant en charge SSE2 - vous devez vérifier le bit CPUID correspondant avant d'appeler l'instruction pause - sinon il y aura juste un gaspillage d'énergie. Sur les processeurs sans pause, utilisez simplement d'autres moyens, comme EnterCriticalSection/LeaveCriticalSection ou Sleep(0) puis Sleep(1) dans une boucle. Certaines personnes disent que sur les processeurs 64 bits, vous ne pouvez pas vérifier SSE2 pour vous assurer que l'instruction pause est implémentée, car l'architecture amd64 originale a adopté les instructions SSE et SSE2 D'Intel comme instructions de base,et, pratiquement, si vous exécutez du code 64 bits, vous avez déjà SSE2 à coup sûr et donc l'instruction pause. Cependant, Intel décourage une pratique de s'appuyer sur une fonctionnalité spécifique à la présence et indique explicitement que certaines fonctionnalités peuvent disparaître dans les futurs processeurs et les applications doivent toujours vérifier caractéristiques via CPUID. Cependant, les instructions SSE sont devenues omniprésentes et de nombreux compilateurs 64 bits les utilisent sans vérification (par exemple Delphi pour Win64), donc les chances que dans certains futurs processeurs il n'y ait pas SSE2, et encore moins pause, sont très minces.

// on entry rcx = address of the byte-lock
// on exit: al (eax) = old value of the byte at [rcx]
@Init:
   mov  edx, cLockByteLocked
   mov  r9d, 5000
   mov  eax, edx
   jmp  @FirstCompare
@DidntLock:
@NormalLoadLoop:
   dec  r9
   jz   @SwitchToThread // for static branch prediction, jump forward means "unlikely"
   pause
@FirstCompare:
   cmp  [rcx], al       // we are using faster, normal load to not consume the resources and only after it is ready, do once again interlocked exchange
   je   @NormalLoadLoop // for static branch prediction, jump backwards means "likely"
   lock xchg [rcx], al
   cmp  eax, edx        // 32-bit comparison is faster on newer processors like Xeon Phi or Cannonlake.
   je   @DidntLock
   jmp  @Finish
@SwitchToThread:
   push  rcx
   call  SwitchToThreadIfSupported
   pop   rcx
   jmp  @Init
@Finish:
1
répondu Maxim Masiutin 2017-07-10 21:32:57