C&C++   发布时间:2022-04-03  发布网站:大佬教程  code.js-code.com
大佬教程收集整理的这篇文章主要介绍了通过内联汇编锁定内存操作大佬教程大佬觉得挺不错的,现在分享给大家,也给大家做个参考。
我是低级别的新手,所以我完全忘记了你可能面临的问题,我甚至不确定我是否理解“原子”这个词.现在我试图通过扩展程序集围绕内存操作进行简单的原子锁.为什么?为了好奇.我知道我在这里重新发明轮子,可能会过度简化整个过程.

这个问题?
在这里提供的代码是否实现了使内存操作既线程安全又可重入的目标?

>如果有效,为什么?
>如果它不起作用,为什么?
>还不够好?我应该在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 *R_989_11845@em = 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)

另外,鉴于上面的拆卸;我可以假设我正在进行原子操作,因为它只是一条指令吗?

解决方法

在现代优化编译器中,寄存器是一个毫无意义的暗示.

我认为一个简单的自旋锁在x86上没有任何真正重大/明显的性能问题就是这样的.当然,真正的实现将在旋转一段时间之后使用系统调用(如Linux futex),并且解锁将必须检查是否需要通过另一个系统调用通知任何服务员.这个很重要;你不想永远浪费cpu时间(和能量/热量)无所事事.但从概念上讲,在你采用后备路径之前,这是自旋锁的旋转部分.这是light-weight locking实施的重要部分. (在调用内核之前只尝试锁定一次是一个有效的选择,而不是完全旋转.)

在内联asm中实现尽可能多的这个,或者最好使用C11 stdatomic,就像这个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

如果您使用原子标记的位域,则可以使用锁定bts(测试和设置)相当于xchg-with-1.你可以旋转bt或测试.要解锁,你需要锁btr,而不仅仅是btr,因为它将是字节的非原子读 – 修改 – 写,甚至包含32位.

使用字节或字大小的锁定,您甚至不需要锁定操作来解锁; release semantics are enough. glibc的pthread_spin_unlock与我的解锁功能相同:一个简单的商店.

如果我们看到它已被锁定,这可以避免写入锁定.这避免了在运行拥有它的线程的核心的L1中使高速缓存行无效,因此它可以在解锁期间返回到“已修改”(MESIFMOESI),具有较少的高速缓存一致性延迟.

我们也不会在循环中使用锁定操作来充斥cpu.我不确定这一般会减慢多少,但是所有等待相同螺旋锁的10个线程都会使内存仲裁硬件保持相当繁忙.这可能会减慢持有锁的线程或系统上其他无关线程的速度,同时它们会使用其他锁或内存.

PAUSE也是必不可少的,以避免cpu对内存排序的错误推测.只有当您正在读取的内存被另一个内核修改时才退出循环.但是,我们不想在无争议的情况下暂停.在Skylake上,PAUSE等待的时间要长得多,比如~100cycles IIRC,所以你绝对应该将spinloop与初始检查分开进行解锁.

我确信英特尔和​​AMD的优化手册都在讨论这个问题,请参阅x86标签维基以及大量其他链接.

大佬总结

以上是大佬教程为你收集整理的通过内联汇编锁定内存操作全部内容,希望文章能够帮你解决通过内联汇编锁定内存操作所遇到的程序开发问题。

如果觉得大佬教程网站内容还不错,欢迎将大佬教程推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。