Verrouille la manipulation de la mémoire via un assemblage en ligne

Je suis nouveau dans le domaine des bas niveaux, je ne suis donc absolument pas au courant du type de problèmes auquel vous pourriez être confronté et je ne suis même pas sûr de bien comprendre le terme «atomique». En ce moment, j’essaie de créer de simples verrous atomiques autour de la manipulation de la mémoire via un assemblage étendu. Pourquoi? Par curiosité. Je sais que je réinvente la roue ici et que je simplifie peut-être excessivement tout le processus.

La question? Le code que je présente ici a-t-il pour objective de rendre la manipulation de la mémoire à la fois threadsafe et réentrante?

  • Si ça marche, pourquoi?
  • Si ça ne marche pas, pourquoi?
  • Pas assez bon? Devrais-je par exemple utiliser le mot-clé register en C?

Ce que je veux simplement faire …

  • Avant la manipulation de la mémoire, verrouillez.
  • Après manipulation de la mémoire, déverrouillez.

Le code:

volatile int atomic_gate_memory = 0; static inline void atomic_open(volatile int *gate) { asm volatile ( "wait:\n" "cmp %[lock], %[gate]\n" "je wait\n" "mov %[lock], %[gate]\n" : [gate] "=m" (*gate) : [lock] "r" (1) ); } static inline void atomic_close(volatile int *gate) { asm volatile ( "mov %[lock], %[gate]\n" : [gate] "=m" (*gate) : [lock] "r" (0) ); } 

Puis quelque chose comme:

 void *_malloc(size_t size) { atomic_open(&atomic_gate_memory); void *mem = malloc(size); atomic_close(&atomic_gate_memory); return mem; } #define malloc(size) _malloc(size) 

.. idem pour calloc, realloc, free et fork (pour linux).

 #ifdef _UNISTD_H int _fork() { pid_t pid; atomic_open(&atomic_gate_memory); pid = fork(); atomic_close(&atomic_gate_memory); return pid; } #define fork() _fork() #endif 

Après avoir chargé la stack pour atomic_open, objdump génère:

 00000000004009a7 : 4009a7: 39 10 cmp %edx,(%rax) 4009a9: 74 fc je 4009a7  4009ab: 89 10 mov %edx,(%rax) 

Aussi, étant donné le déassembly ci-dessus; puis-je supposer que je suis en train de faire une opération atomique parce que ce n’est qu’une instruction?

Pas assez bon? Devrais-je par exemple utiliser le mot-clé register en C?

register est un indice dénué de sens dans les compilateurs d’optimisation modernes.


Je pense qu’un simple spinlock qui ne présente aucun des problèmes de performances vraiment majeurs / évidents sur x86 est quelque chose comme ça. Bien sûr, une implémentation réelle utiliserait un appel système (comme le futex Linux) après un certain temps d’ futex , et le délocking devrait vérifier s’il faut informer les serveurs lors d’un autre appel système. C’est important; vous ne voulez pas perdre votre temps à perdre du temps processeur (et énergie / chaleur) à ne rien faire. Mais conceptuellement, il s’agit de la rotation d’un spinlock avant que vous ne preniez le chemin du repli. C’est un élément important de la mise en œuvre du locking léger . (Tenter de verrouiller le système une seule fois avant d’appeler le kernel serait un choix valide, au lieu de tourner du tout.)

Implémentez autant de fonctionnalités que vous le souhaitez dans inline asm, ou stdatomic utiliser C11 stdatomic , comme cette implémentation de sémaphore .

 ;;; UNTESTED ;;;;;;;; ;;; TODO: **IMPORTANT** fall back to OS-supported sleep/wakeup after spinning some ; first arg in rdi, in the AMD64 SysV ABI ;;;;;void spin_lock (volatile char *lock) global spin_unlock spin_unlock: ;; debug: check that the old value was non-zero. double-unlocking is a nasty bug mov byte [rdi], 0 ret ;; The store has release semantics, but not sequential-consistency (which you'd get from an xchg or something), ;; because acquire/release is enough to protect a critical section (hence the name) ;;;;;void spin_unlock(volatile char *lock) global spin_lock spin_lock: cmp byte [rdi], 0 ; avoid writing to the cache line if we don't own the lock: should speed up the other thread unlocking jnz .spinloop mov al, 1 ; only need to do this the first time, otherwise we know al is non-zero .retry: xchg al, [rdi] test al,al ; check if we actually got the lock jnz .spinloop ret ; no taken twigs on the fast-path .spinloop: pause ; very old CPUs decode it as REP NOP, which is fine cmp byte [rdi], 0 ; To get a comstackr to do this in C++11, use a memory_order_acquire load jnz .spinloop jmp .retry 

Si vous utilisiez un champ de bits d’indicateurs atomiques, vous pourriez utiliser lock bts (test et set) pour l’équivalent de xchg-with-1. Vous pouvez faire tourner ou test . Pour déverrouiller, vous aurez besoin de lock btr , pas seulement btr , car ce serait un octet lecture-modification-écriture non atomique de l’octet, voire même les 32 bits qui le contiennent.

Avec un verrou de la taille d’un mot ou d’un octet, vous n’avez même pas besoin d’une opération lock pour le déverrouiller; la sémantique de publication suffit . Le pthread_spin_unlock de glibc fait la même chose que ma fonction de délocking: un simple magasin.


Cela évite d’écrire dans la serrure si elle est déjà verrouillée. Cela évite d’invalider la ligne de cache dans L1 du kernel exécutant le thread qui le possède, de sorte qu’elle puisse revenir à “Modifié” ( MESIF ou MOESI ) avec un délai de cohérence du cache inférieur lors du délocking.

Nous n’inondons pas non plus la CPU avec des opérations lock dans une boucle. Je ne suis pas sûr de combien cela ralentit les choses en général, mais 10 threads qui attendent tous le même verrou tournant maintiendront le matériel d’arbitrage de la mémoire très occupé. Cela pourrait ralentir le thread qui détient le verrou ou d’autres threads indépendants du système, alors qu’ils utilisent d’autres verrous, ou de la mémoire en général.

PAUSE est également essentielle pour éviter toute spéculation erronée sur l’ordre de la mémoire par la CPU. Vous quittez la boucle uniquement lorsque la mémoire que vous lisez a été modifiée par un autre kernel. Cependant, nous ne voulons pas faire de pause dans le cas non contesté. Sur Skylake, PAUSE attend beaucoup plus longtemps, comme ~ 100cycles IIRC, vous devez donc absolument séparer le spinloop de la vérification initiale du déverrouillé.

Je suis sûr que les manuels d’optimisation d’Intel et d’AMD en parlent, consultez le wiki des balises x86 pour cela et des tonnes d’autres liens.