规避Split Lock性能争抢最佳实践

Intel CPU架构支持不对齐的内存访问。Intel将跨缓存行(cache line)的原子操作称为Split Lock。在并发或高性能计算场景中,频繁的Split Lock会影响系统性能,并可能导致系统卡顿或崩溃。

工作原理

当一个原子操作的操作数跨越两个cache line时,为确保这类操作的原子性,处理器会锁定总线,强制所有其他核心暂停内存访问,直到操作完成。

Split Lock的发生具备两个特征:

  • 原子操作:执行带LOCK前缀的汇编指令。

  • 跨缓存行访问:操作数地址未对齐,跨两个缓存行(Cache Line)。

Split Lock示例

struct counter结构体内的buf成员占据了62字节。由于内存对齐的规则,8 字节的成员c的起始地址很可能位于一个64字节缓存行的末尾(例如,从第62字节开始),导致其存储空间横跨两个缓存行。当__sync_fetch_and_addc执行原子加法时,便会触发Split Lock。

#include<stdio.h>
#include <sys/mman.h>
#pragma pack(push,2)
struct counter
{
    char buf[62];
    long long c;
};
#pragma pack(pop)
int main () {
    struct counter *p;
    int size = sizeof(struct counter);
    int prot = PROT_READ | PROT_WRITE;
    int flags = MAP_PRIVATE | MAP_ANONYMOUS;
    p = (struct counter *) mmap(0, size, prot, flags, -1, 0);
    while(1) {
        __sync_fetch_and_add(&p->c, 1);
    }
    return 0;
}

Split Lock的影响

  • 全局性能下降:Split Lock会阻塞内存访问,降低系统中所有进程的性能,而不仅限于触发它的进程。

  • 延迟增长:频繁的Split Lock会增加内存访问延迟,并引起系统性能抖动。

检测Split Lock

重要

g9ic9ir9ig8ic8ir8i支持检测。

ecs.g8i.xlarge为例,如果counts列的数值大于0,则表明系统中存在触发Split Lock的操作。

perf stat -e cpu/event=0x2c,umask=0x10/ -a -I 1000

image

若内存访问未跨越缓存行(cache line),则对应的计数值为0。image

规避Split Lock

  1. 确保原子变量对齐到自然边界。

    // 推荐:64 字节对齐,避免跨行和伪共享
    alignas(64) atomic<uint64_t> counter;
    
    // 针对 128 位原子类型,16 字节对齐
    alignas(16) atomic<__int128> big_counter;
  2. 避免将大原子变量放置在结构体中间。

    // 不推荐:原子变量可能因前面的成员而发生位移,导致跨行
    struct BadExample {
        char a;                    // 占用 1 字节
        atomic<__int128> val;     // 可能跨缓存行
    };
    
    // 推荐:将对齐要求最高的成员放在最前,并显式声明
    struct GoodExample {
        alignas(16) atomic<__int128> val;
        char a;
    };
  3. 使用更小的原子类型组合替代大原子。对于不需要真正128位原子性的场景,可拆分为两个64位原子操作。

    struct PaddedCounter {
        alignas(64) atomic<uint64_t> low;
        alignas(64) atomic<uint64_t> high;
    };
  4. 不要使用未对齐指针进行原子操作。

    //错误:malloc 不保证 16 字节对齐(尤其老 libc)
    void* ptr = malloc(sizeof(atomic<__int128>));
    atomic<__int128>* p = new(ptr) atomic<__int128>;
    
    //正确: 使用 aligned_alloc
    void* aligned_ptr = aligned_alloc(16, sizeof(atomic<__int128>));
    atomic<__int128>* p = new(aligned_ptr) atomic<__int128>;
  5. 使用static_assert检查对齐。

    static_assert(alignof(atomic<__int128>) >= 16, "128-bit atomic must be 16-byte aligned");
  6. 避免在packed结构体中使用原子类型。

    #pragma pack(push, 1)
    struct Packed {
        uint8_t flag;
        atomic<uint64_t> counter; // 错误:8字节也可能因紧凑布局跨行
    };
    #pragma pack(pop)