x86 LOCK 指令前缀

Intel® 64 and IA-32 Architectures Software Developer's Manual中的章节LOCK-Assert LOCK$ Signal Prefix中给出LOCK指令的详细解释

LOCK是一个指令前缀,也就是说LOCK会使紧跟在其后面的指令变成原子指令(atomic instruction)。

LOCK指令前缀只能加在以下这些指令前面
ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,CMPXCH8B,CMPXCHG16B,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,XCHG

总线锁

在多处理器环境中,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线LOCK,如果汇编语言的程序中在一条指令前面加上前缀“LOCK”,经过汇编以后的机器代码就是CPU在执行这条指令的时候把引线LOCK的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。

总线锁这种做法锁定的范围太大了,导致CPU利用率急剧下降,因为使用LOCK#是把CPU和内存之间的通信锁住了,这使得锁定期间其他处理器不能操作其内存地址的数据,所以总线锁的开销比较大

缓存锁

从P6系列处理器开始,如果访问的内存区域已经缓存在处理器的缓存行中,LOCK#信号不会被发送。它会对CPU缓存中的缓存行进行锁定,在锁定期间,其他CPU不能同时缓存此数据。在修改之后通过缓存一致性协议来保证修改的原子性。这个操作被称为“缓存锁”

什么情况下使用总线锁

当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,也会使用总线锁
因为从P6系列处理器开始才有缓存锁,所以对于早些处理器是不支持缓存锁定的,也会使用总线锁。

x86中,有些指令是自带 lock 语义的,比如 XCHG;另外一些指令可以手动加上lock前缀来实现lock语义,比如BTS, BTR,CMPXCHG指令。在这些指令中,最核心的是 CAS(Compare And Swap) 指令,它是实现各种锁语义的核心指令。不同于自带原子语义的XCHG,CAS操作要通过"lock CMPXCHG"这样的形式来实现。一般而言,原子操作的数据长度不会超过8个字节。简单来说,自旋锁和读写锁的核心都是利用原子指令来CAS操纵一个32位/64位的值

1,x86上 atomic_add

 /**
  * atomic_add - add integer to atomic variable
  * @i: integer value to add
  * @v: pointer of type atomic_t
  *
  * Atomically adds @i to @v.
  */
 static inline void atomic_add(int i, atomic_t *v)
 {
     asm volatile(LOCK_PREFIX "addl %1,%0"
              : "+m" (v->counter)
              : "ir" (i));
 }

2,__raw_cmpxchg

/*
 * Atomic compare and exchange.  Compare OLD with MEM, if identical,
 * store NEW in MEM.  Return the initial value in MEM.  Success is
 * indicated by comparing RETURN with OLD.
 */
#define __raw_cmpxchg(ptr, old, new, size, lock)                        \
({                                                                      \
        __typeof__(*(ptr)) __ret;                                       \
        __typeof__(*(ptr)) __old = (old);                               \
        __typeof__(*(ptr)) __new = (new);                               \
        switch (size) {                                                 \
        case __X86_CASE_B:                                              \
        {                                                               \
                volatile u8 *__ptr = (volatile u8 *)(ptr);              \
                asm volatile(lock "cmpxchgb %2,%1"                      \
                             : "=a" (__ret), "+m" (*__ptr)              \
                             : "q" (__new), "0" (__old)                 \
                             : "memory");                               \
                break;                                                  \
        } 

3,__down_read

/*
 * lock for reading
 */
static inline void __down_read(struct rw_semaphore *sem)
{
        asm volatile("# beginning down_read\n\t"
                     LOCK_PREFIX _ASM_INC "(%1)\n\t"
                     /* adds 0x00000001 */
                     "  jns        1f\n"
                     "  call call_rwsem_down_read_failed\n"
                     "1:\n\t"
                     "# ending down_read\n\t"
                     : "+m" (sem->count)
                     : "a" (sem) 
                     : "memory", "cc");
}

4,volatile如何保证可见性

加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,它有三个功能:

  • 确保指令重排序时不会把其后面的指令重排到内存屏障之前的位置,也不会把前面的指令排到内存屏障后面,即在执行到内存屏障这句指令时,前面的操作已经全部完成;

  • 将当前处理器缓存行的数据立即写回系统内存(由volatile先行发生原则保证);

  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。写回操作时要经过总线传播数据,而每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器要对这个值进行修改的时候,会强制重新从系统内存里把数据读到处理器缓存(也是由volatile先行发生原则保证);

Intel 为了优化总线锁导致的性能问题,在 P6 后的处理器上,引入了缓存锁(cache locking)机制:通过缓存一致性协议保证多个 CPU 核访问跨 cache line 的内存地址的多次访问的原子性与一致性,而不需要锁内存总线。(缓存锁是依赖缓存一致性协议来保证内存访问的原子性,因为缓存一致性协议会阻止被多个 CPU 缓存的内存地址被多个 CPU 同时修改;如果要访问的内存区域已经在当前cpu的cache中了,就会利用cache一致性协议来实现原子操作,否则会锁总线;缓存锁是如何基于 MESI 协议实现内存读写的原子性:深入剖析 split locks,i++ 可能导致的灾难 - 知乎

缓存一致性协议有多种,但是日常处理的大多数计算机设备都属于”嗅探(snooping)”机制,它的基本思想是:
所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线:缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个CPU缓存可以读写内存)。
CPU缓存不仅仅在做内存传输的时候才与总线打交道,而是不停在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。所以当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。只要某个处理器一写内存,其它处理器马上知道这块内存在它们的缓存段中已失效。

可以得出lock指令的几个作用:
1、锁总线,其它CPU对内存的读写请求都会被阻塞,直到锁释放,不过实际后来的处理器都采用锁缓存替代锁总线,因为锁总线的开销比较大,锁总线期间其他CPU没法访问内存
2、lock后的写操作会回写已修改的数据,同时让其它CPU相关缓存行失效,从而重新从主存中加载最新的数据(发出的 LOCK# 指令锁总线或锁缓存行,同时让其他高速缓存中的缓存行内容失效,这种场景下多缓存的数据一致是通过缓存一致性协议来保证的);
3、不是内存屏障却能完成类似内存屏障的功能,阻止屏障两遍的指令重排序

由于效率问题,实际后来的处理器都采用锁缓存来替代锁总线,这种场景下多缓存的数据一致是通过缓存一致性协议来保证的 。

split lock

由于缓存一致性协议的粒度是一个 cache line,当原子操作的数据跨 cache line 时(x86 架构存在,arm 与 riscv 架构不允许),依赖缓存锁机制无法保证数据一致性,会退化为总线锁来保证一致性,这种情况就是 split lock,split 也可以理解为访存的 cache 被 split 为两个 line。

split lock 两个特征:1,原子操作,即汇编代码指令包含 Lock 前缀;2,操作的数据地址不对齐,跨越两个 cache line;

比如有如下数据结构:

struct Data {
   char padding[62]; // 62字节
    int32_t value;  // 4字节
} __attribute__((packed)) // 按实际字节对齐

被缓存到 cache line 大小为 64 字节的 cache 中时,value 成员会跨 cache line。

此时如果想要通过LOCK ADD 指令操作 Data 结构中的 value 成员,就无法通过缓存锁解决,只能走老路,锁总线来保证数据一致性。而锁总线会引起严重的性能下降,访存延迟增加百倍左右,如果是内存密集型业务,性能会下降 2 个数量级。所以在现代 X86 处理器中,要避免写出会产生 split lock 的代码,并有能力检测出 Split lock 的产生。

x86指令集中lock前缀的意义-阿里云开发者社区

原来add指令可以直接对内存进行加法操作-阿里云开发者社区

https://www.cnblogs.com/badboys/p/12695183.html

https://www.cnblogs.com/Courage129/p/14401680.html

深入剖析 split locks,i++ 可能导致的灾难 - 知乎

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
x86和ARM指令集是两种常见的计算机体系结构的指令集架构。它们都用于指导计算机硬件执行特定的任务和操作。以下是关于x86和ARM指令集的一些简要信息: x86指令集:x86是一种基于CISC(复杂指令集计算)体系结构的指令集。该指令集最初由英特尔开发,并成为主要的个人计算机和服务器体系结构。x86指令集包含丰富的指令,可执行多种操作,包括算术运算、逻辑运算、数据传输和控制流等。它是一种复杂的指令集,并且具有较高的执行能力和灵活性。凭借广泛的软件支持和计算能力,x86成为桌面和服务器领域最流行的体系结构之一。 ARM指令集:ARM是一种基于RISC(精简指令集计算)体系结构的指令集。它最初由英国公司ARM Holdings开发,并成为移动设备(如智能手机和平板电脑)和嵌入式系统的主要体系结构。ARM指令集设计简洁,指令数量较少,每条指令一般只执行一个简单的操作。与x86相比,ARM指令集执行效率更高,并具有更低的功耗和较小的芯片面积。因此,它成为便携式设备和嵌入式系统中的首选体系结构。 x86和ARM的比较:x86指令集适用于大型多任务处理、复杂的图形操作和计算密集型任务。它具有广泛的软件生态系统和令人称赞的性能。然而,由于复杂指令集的特性,x86架构的芯片更复杂、功耗较高,并且相对较贵。 相比之下,ARM指令集在低功耗设备方面表现出色。ARM芯片的功耗较低,性能较高,并且在移动设备和嵌入式系统中能够提供高度集成的解决方案。此外,由于ARM芯片广泛采用,其生态系统和软件支持也非常庞大。 总结而言,x86和ARM指令集都有各自的优势和适用场景。x86适用于高性能和复杂任务的计算机系统,而ARM则适用于低功耗和便携设备的应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值