2016-05-15 7 views
2

Ich bin neu in den Low-Level-Zeug, so dass ich völlig vergessen bin, welche Art von Problemen Sie dort unten konfrontiert werden und ich bin nicht einmal sicher, ob ich den Begriff "Atom" richtig verstehe. Im Moment versuche ich, einfache atomare Sperren um Speichermanipulation über erweiterte Assembly zu machen. Warum? Aus Neugierde. Ich weiß, dass ich das Rad hier neu erfinde und möglicherweise den ganzen Prozess zu sehr vereinfache.Sperrt Speicher-Manipulation über Inline-Assembly

Die Frage? Hat der Code, den ich hier vorstelle, das Ziel, Speichermanipulation sowohl threadsafe als auch reentrant zu machen?

  • Wenn es funktioniert, warum?
  • Wenn es nicht funktioniert, warum?
  • Nicht gut genug? Soll ich zum Beispiel das Register Stichwort in C verwenden?

Was ich einfach tun wollen ...

  • Bevor Speichermanipulation, sperren.
  • Nach der Speicherbearbeitung entsperren.

Der 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) 
    ); 
} 

Dann so etwas wie:

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) 

.. gleiche gilt für calloc, realloc, frei und Gabel (für 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 

Nach dem Stapelrahmen für atomic_open Laden objdump erzeugt:

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

Auch angesichts der oben Demontage; kann ich annehmen, dass ich eine atomare Operation mache, weil es nur eine Anweisung ist?

+3

Nein, es ist nicht threadsicher, da zwei Threads simultan die 'cmp' ausführen könnten und annehmen, dass sie die Sperre übernehmen können. – Jester

+0

@Jester Oh, snap ... Ich hatte irgendwie angenommen, dass die CPU nur einen Satz Befehle gleichzeitig ausführt, wobei sie mit verschiedenen Befehlssätzen verschachtelt wird, wenn sie in mehreren Zeilen enthalten sind ... Das kompliziert die Dinge wirklich ... – user1235831

+2

Interleaving (Multitasking) verursacht auch das gleiche Problem. Nachdem ein Thread den 'cmp' erstellt hat, könnte der nächste Thread die CPU bekommen und auch seinen' cmp'. – Jester

Antwort

5

Nicht gut genug? Soll ich zum Beispiel das Schlüsselwort register in C benutzen?

register ist ein bedeutungsloser Hinweis in modernen optimierenden Compilern.


Ich denke, eine einfache spinlock, die nicht eine der wirklich großen/offensichtliche Performance-Probleme auf x86 so etwas wie dieses hat ist. Natürlich würde eine echte Implementierung einen Systemaufruf verwenden (wie Linux futex), nachdem sie sich eine Weile gedreht hat, und das Entsperren müsste überprüfen, ob es irgendwelche Kellner mit einem anderen Systemaufruf benachrichtigen muss. Das ist wichtig; Sie wollen nicht für immer CPU Zeit verschwenden (und Energie/Hitze) nichts tun. Aber konzeptionell ist dies der Spin-Teil eines Spinlocks, bevor Sie den Fallback-Pfad nehmen. Es ist ein wichtiger Teil davon, wie light-weight locking implementiert ist. (Es ist eine gültige Wahl, nur einmal die Sperre zu übernehmen, bevor der Kernel aufgerufen wird, statt sich überhaupt zu drehen.)

Implementieren Sie so viel wie Sie mögen in Inline-Asm, oder vorzugsweise mit C11 stdatomic, wie diese semaphore implementation.

;;; 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 branches on the fast-path 

.spinloop: 
    pause      ; very old CPUs decode it as REP NOP, which is fine 
    cmp byte [rdi], 0  ; To get a compiler to do this in C++11, use a memory_order_acquire load 
    jnz .spinloop 
    jmp .retry 

Wenn Sie ein Bitfeld von Atom Flaggen verwendet haben, könnten Sie lock bts (Test and Set) für das Äquivalent von xchg-mit-1 verwenden. Sie können unter bt oder test drehen. Zum Entsperren benötigen Sie lock btr, nicht nur btr, denn es wäre ein nicht-atomarer Read-Modify-Write des Bytes oder sogar der enthaltenen 32Bits.

Mit einer Byte- oder Wort-sortierten Sperre benötigen Sie nicht einmal eine lock Ed-Operation zum Entsperren; release semantics are enough. Glibcs ​​pthread_spin_unlock macht das gleiche wie meine Unlock-Funktion: ein einfacher Laden.


Dies vermeidet Schreiben in das Schloss, wenn wir sehen, dass es bereits gesperrt ist. Dies verhindert, dass die Cache-Zeile in L1 des Kerns ungültig wird, auf dem der Thread ausgeführt wird, der sie besitzt, so dass sie zu "Modified" (MESIF oder MOESI) mit weniger Cache-Kohärenz-Verzögerung während des Entsperrens zurückkehren kann.

Wir überschwemmen auch nicht die CPU mit lock Ed Operationen in einer Schleife. Ich bin mir nicht sicher, wie sehr dies die Dinge im Allgemeinen verlangsamt, aber 10 Threads, die alle auf den gleichen Spinlock warten, werden die Speicherarbitrierungs-Hardware ziemlich beschäftigt halten. Dies kann den Thread verlangsamen, der die Sperre oder andere nicht verwandte Threads auf dem System enthält, während sie andere Sperren oder Speicher im Allgemeinen verwenden.

PAUSE ist auch wichtig, um Fehlspekulationen über die Speicherordnung durch die CPU zu vermeiden. Sie verlassen die Schleife nur dann, wenn der von Ihnen gelesene Speicher von einem anderen Kern geändert wurde. Wir wollen jedoch nicht im unbeherrschten Fall pause. Auf Skylake, PAUSE wartet viel länger, wie ~ 100cycles IIRC, so sollten Sie auf jeden Fall die Spinloop von der ersten Prüfung für entsperrt getrennt halten.

Ich bin sicher Intel Optimierungshandbücher von Intel und AMD sprechen darüber, siehe das Tag Wiki für das und Tonnen anderer Links.

+0

Ich darf das jetzt nicht sagen aber Danke für die Einsicht! Eine Frage zur Rechtfertigung dieses Kommentars: Würde das Schlüsselwort volatile den Compiler zwingen, das Schlüsselwort register zu verwenden? Selbst wenn das der Fall ist, wäre es ein Vorteil/Punkt, das Registrierungsschlüsselwort für Sperren zu verwenden? – user1235831

+1

'volatile' ist das Gegenteil von' register'. Dies bedeutet, dass der Wert jedes Mal, wenn er referenziert wird, erneut aus dem Speicher gelesen werden muss, und jeder Speicher muss separat und in Programmreihenfolge ausgeführt werden. Was hoffst du, dass "register" in der generierten asm überhaupt funktioniert? Es macht keinen Sinn für eine Sperrimplementierung, selbst wenn es etwas getan hat. –

+0

Es ist nur ein Mangel an Verständnis meinerseits. Ich war mir nicht sicher, wie Schlösser gemacht werden. Aus einer Amateurperspektive scheint es, dass es einen Sinn ergeben könnte, da Schlösser in Anbetracht ihrer Natur etwas Besonderes sind. Sie müssen schnell sein und atomare Operationen nutzen, damit wir Threadsafety und Retentabilität haben können. Für eine Amatur wie mich schien die Reservierung eines Registers für Schlösser Sinn zu ergeben; aber ich war mir auch nicht ganz sicher. Ich dachte an etwas in Zeilen von "int register res asm (" r0 ") = 0;" – user1235831