gpt4 book ai didi

c - 通过内联汇编锁定内存操作

转载 作者:太空狗 更新时间:2023-10-29 16:01:05 24 4
gpt4 key购买 nike

我是新来的低层次的东西,所以我完全不知道什么样的问题,你可能会面临那里,我甚至不知道我是否理解“原子”一词的权利。现在,我正试图通过扩展程序集围绕内存操作创建简单的原子锁。为什么?为了好奇。我知道我在重新发明轮子,可能把整个过程过于简单化了。
这个问题?
我在这里展示的代码是否实现了使内存操作既安全又可重入的目标?
如果成功了,为什么?
如果不起作用,为什么?
不够好?例如,我应该在c中使用register关键字吗?
我只是想做的…
在操作内存之前,锁定。
内存操作后,解锁。
代码:

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

加载stackframe以打开atomic_后,objdump生成:
00000000004009a7 <wait>:
4009a7: 39 10 cmp %edx,(%rax)
4009a9: 74 fc je 4009a7 <wait>
4009ab: 89 10 mov %edx,(%rax)

另外,考虑到上面的反汇编,我是否可以假设我正在进行原子操作,因为它只是一条指令?

最佳答案

不够好?例如,我应该在c中使用register关键字吗?
register在现代优化编译器中是一个毫无意义的提示。
我认为一个简单的spinlock在x86上不存在任何主要/明显的性能问题就是这样的。当然,真正的实现在旋转一段时间后会使用一个系统调用(比如linuxfutex),而解锁则需要检查是否需要用另一个系统调用通知任何服务生。这一点很重要;你不想永远无所事事地浪费cpu时间(和能量/热量)。但从概念上讲,这是自旋锁在采用回退路径之前的自旋部分。这是如何实现light-weight locking的一个重要部分。(在调用内核之前只尝试获取一次锁是一个有效的选择,而不是完全旋转。)
在内联asm中尽可能多地实现这一点,或者最好使用c11stdatomic,像这样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

如果您使用的是原子标志的位字段,那么可以使用 lock bts(test and set)作为xchg-with-1的等效值。您可以旋转 bttest。要解锁,您需要 lock btr,而不仅仅是 btr,因为它是字节的非原子读-修改-写,甚至是包含32位的。
使用字节或字大小的锁,您甚至不需要 locked操作就可以解锁; release semantics are enough。glibc的 pthread_spin_unlock功能与我的解锁功能相同:一个简单的存储。
如果我们看到锁已经锁上了,这就避免了写入锁。这避免了使运行拥有它的线程的内核的l1中的缓存线失效,因此它可以在解锁期间以较少的缓存一致性延迟返回到“已修改”( MESIFMOESI)。
我们也不会在一个循环中用 locked操作充斥cpu。我不确定这会在多大程度上降低速度,但是10个线程都在等待同一个spinlock,这会使内存仲裁硬件非常繁忙。这可能会减慢持有锁的线程或系统上其他无关线程的速度,而它们通常使用其他锁或内存。
PAUSE也是必要的,以避免对cpu的内存顺序的错误猜测。只有当你正在读取的内存被另一个内核修改时,才会退出循环。然而,我们不想在未被起诉的案件中 pause。在skylake上, PAUSE等待的时间要长得多,比如~100个循环iirc,所以您一定要将spinloop与初始的unlocked检查分开。
我敢肯定英特尔和AMD的优化手册都谈到了这一点,请参阅 标签wiki上的链接和其他链接。

关于c - 通过内联汇编锁定内存操作,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37241553/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com