通过内联汇编locking内存操作
我是低层次的新手,所以我完全没有意识到你会遇到什么样的问题,我甚至不知道我是否理解了“primefaces”这个词。 现在我正试图通过扩展程序集来对内存操作进行简单的primefaceslocking。 为什么? 为了好奇。 我知道我在这里重新发明轮子,可能会把整个过程简单化。
这个问题? 我在这里提出的代码是否实现了使内存操作既是线程安全又是可重入的目标?
- 如果有效,为什么?
- 如果不行,为什么?
- 还不够好? 我应该例如使用C中的register关键字吗?
我只是想做…
- 在内存操作之前,locking。
- 内存操作后,解锁。
代码:
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) ); }
然后是这样的:
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)
对于calloc,realloc,free和fork也是一样的(对于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
加载atomic_open的堆栈后,objdump生成:
00000000004009a7 <wait>: 4009a7: 39 10 cmp %edx,(%rax) 4009a9: 74 fc je 4009a7 <wait> 4009ab: 89 10 mov %edx,(%rax)
另外,鉴于上面的反汇编; 我可以假设我正在做一个primefaces操作,因为它只是一个指令?
还不够好? 我应该例如使用C中的register关键字吗?
register
是现代优化编译器中毫无意义的提示。
我认为一个简单的自旋锁在x86上没有任何主要的/明显的性能问题就是这样的。 当然,一个真正的实现会在旋转一段时间之后使用一个系统调用(如Linux futex
),解锁将不得不检查是否需要通过另一个系统调用来通知任何服务员。 这个很重要; 你不想永远浪费CPU时间(和能量/热量)无所事事。 但从概念上讲,这是自旋锁的一部分,然后再采取后备path。 这是如何实现轻量级locking的重要部分。 (只有在调用内核之前尝试locking一次才是有效的select,而不是根本就不旋转。)
stdatomic
联asm中尽可能多地使用它,或者最好使用C11 stdatomic
,就像这个信号量实现一样 。
;;; 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
如果你使用的是primefaces标志的位域,你可以使用lock bts
testing(testing和设置)等价于xchg-with-1。 你可以旋转bt
或test
。 要解锁,你需要lock btr
,而不仅仅是btr
,因为这将是一个非primefaces读 – 修改 – 写的字节,甚至包含32位。
使用字节或字大小的锁,您甚至不需要lock
操作来解锁; 释放语义就足够了 。 glibc的pthread_spin_unlock
和我的解锁函数一样:一个简单的商店。
如果我们看到它已经被locking,这将避免写入锁。 这样可以避免使运行拥有它的线程的核心L1中的caching线无效,因此可以在解锁期间返回到“修改”( MESIF或MOESI ),具有较less的caching一致性延迟。
我们也不会在循环中lock
CPU操作。 我不确定这样做总的来说会减慢多less,但是等待同一个螺旋锁的10个线程会使内存仲裁硬件非常繁忙。 这可能会减慢持有该锁的线程或系统上其他不相关的线程,而它们通常使用其他锁或内存。
PAUSE
也是必不可less的,以避免错误猜测CPU的内存sorting。 只有当您正在阅读的内存被另一个内核修改时,才退出循环。 但是,我们不想在不争论的情况下pause
来。 在Skylake上, PAUSE
会等待更长的时间,比如〜100cycles IIRC,所以你应该保持spinloop与初始检查分离。
我相信英特尔和AMD的优化手册谈论这一点,请参阅x86标签wiki以及其他许多链接。