ARM64/AArch64 系统编程深度实战:从内存模型到性能优化的工程实践

为什么要关注 ARM64?AWS Graviton4、Azure AmpereOne、阿里云倚天 710、Apple M4 Ultra——ARM 架构正在从移动端向数据中心和桌面全面渗透。但与 x86_64 的强内存模型(TSO)不同,ARM64 采用弱内存模型(Weakly-Ordered Memory Model),这意味着相同的并发代码在 ARM64 上可能表现出完全不同的行为。本文将从 ARM64 的硬件特性出发,深入剖析内存屏障、缓存架构、SIMD 编程和性能优化的工程实践。


一、ARM64 弱内存模型与 x86_64 TSO 的本质差异

1.1 什么是弱内存模型?

x86_64 遵循 Total Store Order(TSO)模型:所有 CPU 看到的 Store 操作顺序一致,且每个 CPU 自己的 Store 操作对其它 CPU 可见的顺序与程序执行顺序一致。换句话说,x86_64 只可能存在 Store-Load 重排。

ARM64 遵循 Weakly-Ordered 模型:除了 Store-Load 外,Store-Store、Load-Load、Load-Store 都可能被重排。编译器重排、CPU 乱序执行、多级缓存一致性协议的异步特性都可能导致内存操作的观察顺序与程序顺序不一致。

1.2 为什么 ARM64 要这么做?

弱内存模型不是"缺陷",而是有意为之的性能优化。通过允许更灵活的重排,CPU 可以:

  • 更自由地填充流水线气泡
  • 减少因为等待内存访问而导致的流水线停顿
  • 降低缓存一致性协议的带宽消耗
  • 支持更大规模的多核扩展

在 ARM64 的 big.LITTLE / DynamIQ 架构中,不同性能核和能效核可能运行在不同的电源域中,弱内存模型能够更好地支持异构计算场景。

1.3 生动示例:Dekker 算法在 ARM64 上失败

经典的 Dekker 互斥算法在 x86_64 上能正常工作(但在 C 语言层面也有问题),但在 ARM64 上必然失败:


// 线程 1
flag1 = __ATOMIC_RELEASE_STORE(&flag1, 1);
if (flag2 == 0) {  // 普通读,可能被重排到 flag1=1 之前
    // 进入临界区
}

// 线程 2
flag2 = __ATOMIC_RELEASE_STORE(&flag2, 1);
if (flag1 == 0) {
    // 进入临界区
}

在 ARM64 上,Store 之后的 Load 可以被 CPU 重排到 Store 之前执行,导致两个线程同时进入临界区。正确的写法必须使用 acquire/release 语义或更强的顺序一致性:


#include <stdatomic.h>

atomic_int flag1 = 0, flag2 = 0;

// 线程 1
atomic_store_explicit(&flag1, 1, memory_order_seq_cst);
if (atomic_load_explicit(&flag2, memory_order_seq_cst) == 0) {
    // 进入临界区
}

二、ARM64 内存屏障指令详解与正确使用

2.1 ARM64 的三类屏障指令

指令 全称 作用
DMB Data Memory Barrier 保证屏障两侧的内存访问顺序
DSB Data Synchronization Barrier 等待所有内存访问完成 + 刷新流水线
ISB Instruction Synchronization Barrier 刷新流水线,后续指令重新取指

DMB 的选项:


DMB SY   ; 全系统最强的屏障(最常用)
DMB ST   ; 仅 Store-Store 有序
DMB LD   ; 仅 Load-Load 和 Load-Store 有序
DMB OSH  ; 仅 Outer Shareable 域
DMB ISH  ; Inner Shareable 域(同一 CPU 簇内)
DMB NSH  ; Non-shareable 域(仅当前核)

2.2 在 C11/C++11 原子操作中的映射

C11 原子操作到 ARM64 指令的精确映射:


#include <stdatomic.h>

atomic_int x;
atomic_int y;

// memory_order_relaxed → 无屏障
atomic_store_explicit(&x, 1, memory_order_relaxed);
// ARM64: STR W0, [X1]          ;; 普通存储,无任何屏障

// memory_order_release → STLR (Store-Release)
atomic_store_explicit(&x, 1, memory_order_release);
// ARM64: STLR W0, [X1]         ;; 所有之前的内存操作必须完成后才执行 Store

// memory_order_acquire → LDAR (Load-Acquire)
int val = atomic_load_explicit(&x, memory_order_acquire);
// ARM64: LDAR W0, [X1]         ;; 之后的所有内存操作必须在本 Load 之后

// memory_order_seq_cst → 全屏障
atomic_store_explicit(&x, 1, memory_order_seq_cst);
// ARM64: STLR W0, [X1]         ;; seq_cst Store
//         DMB ISH              ;; 额外 DMB 防止 Store-Load 重排

2.3 实战案例:Lock-Free 单生产者单消费者环形缓冲区

这是一个在 ARM64 上极易出错的场景:


#include <stdatomic.h>
#include <stdint.h>
#include <stddef.h>

#define RING_SIZE 1024

struct ring_buffer {
    atomic_size_t head;     // 仅由生产者写入
    atomic_size_t tail;     // 仅由消费者写入
    char buffer[RING_SIZE];
};

// 生产者:SPSC 入队
int ring_enqueue(struct ring_buffer *rb, const char *data, size_t len) {
    size_t head = atomic_load_explicit(&rb->head, memory_order_relaxed);
    size_t tail = atomic_load_explicit(&rb->tail, memory_order_acquire); // 看到消费者最新的 tail
    
    if ((head - tail) + len > RING_SIZE)
        return -1; // 缓冲区满
    
    // 在 head 位置写入数据
    for (size_t i = 0; i < len; i++) {
        rb->buffer[(head + i) % RING_SIZE] = data[i];
    }
    
    // 关键:使用 release 语义更新 head,确保数据写入完成后头指针才可见
    atomic_thread_fence(memory_order_release); // DMB ISHST
    atomic_store_explicit(&rb->head, head + len, memory_order_release);
    
    return 0;
}

// 消费者:SPSC 出队
int ring_dequeue(struct ring_buffer *rb, char *out, size_t max_len) {
    size_t tail = atomic_load_explicit(&rb->tail, memory_order_relaxed);
    size_t head = atomic_load_explicit(&rb->head, memory_order_acquire); // 看到生产者最新的 head
    
    size_t available = head - tail;
    if (available == 0) return 0; // 无数据
    
    size_t to_read = available < max_len ? available : max_len;
    
    for (size_t i = 0; i < to_read; i++) {
        out[i] = rb->buffer[(tail + i) % RING_SIZE];
    }
    
    // 使用 release 更新 tail
    atomic_store_explicit(&rb->tail, tail + to_read, memory_order_release);
    
    return to_read;
}

关键点分析:

  • 生产者用 acquire 读 tail → 确保读到消费者最新释放的数据
  • 生产者用 release 更新 head → 确保 buffer 写入完成后 head 才变化
  • 消费者用 acquire 读 head → 确保读到生产者最新写入的数据
  • 消费者用 release 更新 tail → 确保数据读取完成后 tail 才变化

三、ARM64 缓存架构与 NUMA 感知编程

3.1 ARM64 多级缓存的典型配置

现代 ARM64 处理器的缓存层次(以 Neoverse N2 为例):


Core: 
  L1 I-Cache: 64KB, 4-way
  L1 D-Cache: 64KB, 4-way
  
Cluster (共享 L2):
  L2 Cache: 512KB-1MB per core, 8-way
  
Socket (共享 L3):
  L3 Cache: 32-64MB, 16-way
  
NUMA:
  跨 Socket 通过 CCIX/CXL 或 mesh 互联

3.2 缓存行与伪共享

ARM64 处理器的缓存行通常是 64 字节(Apple M4 等大核为 128 字节)。伪共享是性能杀手:


#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>

// ❌ 错误:伪共享
struct bad_counter {
    atomic_int counter; // 多个核的 counter 可能在同一缓存行
};

// ✅ 正确:缓存行对齐
struct alignas(64) good_counter {
    atomic_int counter;
    char padding[60]; // 填充到缓存行大小
};

#define NUM_THREADS 4
#define ITERATIONS 10000000

struct good_counter counters[NUM_THREADS];

void *thread_func(void *arg) {
    int id = *(int*)arg;
    for (int i = 0; i < ITERATIONS; i++) {
        atomic_fetch_add_explicit(&counters[id].counter, 1, memory_order_relaxed);
    }
    return NULL;
}

// 编译:gcc -O2 -pthread -march=armv8.2-a test.c -o test
// 运行:taskset -c 0-3 ./test

/*
 * 性能对比(Apple M2 Pro, 4 性能核):
 * 伪共享版本: ~1200ms,cache-misses 数千万次
 * 填充对齐版本: ~280ms,cache-misses 个位数
 * 
 * 使用 perf stat -e cache-misses,cache-references ./test 测量
 */

3.3 ARM64 特有缓存优化指令

ARM64 提供了一系列缓存预取和内存提示指令:


#include <arm_neon.h>
#include <stdint.h>

void process_data(int32_t *data, size_t len) {
    for (size_t i = 0; i < len; i += 4) {
        // PLD/PLI 指令:预取到 L1/L2/L3
        // Prfum Keep: 预取并保留在缓存中
        // Prfum Strm: 流式预取(数据只访问一次,避免污染缓存)
        
        // 使用内建函数触发预取
        __builtin_prefetch(&data[i + 16], 0, 3);  // 读,高时间局部性
        __builtin_prefetch(&data[i + 64], 1, 0);  // 写,低时间局部性(流式)
        
        // 处理当前数据
        int32x4_t vec = vld1q_s32(&data[i]);
        vec = vaddq_s32(vec, vdupq_n_s32(1));
        vst1q_s32(&data[i], vec);
    }
}

3.4 NUMA 感知内存分配

在数据中心 ARM64 服务器(如 128 核 AmpereOne)上,NUMA 拓扑的优化至关重要:


#include <numa.h>
#include <numaif.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 检查 NUMA 拓扑
void check_numa_topology(void) {
    if (numa_available() < 0) {
        printf("NUMA not available\n");
        return;
    }
    
    int max_node = numa_max_node();
    printf("NUMA nodes: %d\n", max_node + 1);
    
    for (int i = 0; i <= max_node; i++) {
        long long free_mem = 0;
        long long total_mem = numa_node_size64(i, &free_mem);
        printf("Node %d: total=%lldMB free=%lldMB cpus=", i, 
               total_mem / (1024*1024), free_mem / (1024*1024));
        
        struct bitmask *cpus = numa_node_to_cpus(i);
        for (int j = 0; j < numa_num_configured_cpus(); j++) {
            if (numa_bitmask_isbitset(cpus, j)) {
                printf("%d ", j);
            }
        }
        printf("\n");
        numa_free_cpumask(cpus);
    }
}

// NUMA 本地分配
void *numa_alloc_local(size_t size, int node) {
    void *ptr = numa_alloc_onnode(size, node);
    if (!ptr) {
        perror("numa_alloc_onnode");
        return NULL;
    }
    return ptr;
}

// 将线程绑定到特定 NUMA 节点执行
int bind_thread_to_node(int node) {
    struct bitmask *cpus = numa_node_to_cpus(node);
    if (!cpus) return -1;
    
    int ret = numa_run_on_node_mask(cpus);
    numa_free_cpumask(cpus);
    return ret;
}

四、ARM64 SIMD/NEON 编程实战

4.1 NEON 寄存器架构

ARM64 NEON 提供 32 个 128-bit 寄存器(V0-V31),支持多种数据类型:


V0-V31: 128-bit 向量寄存器
  - 16 × 8-bit  (int8x16_t)
  - 8 × 16-bit  (int16x8_t)
  - 4 × 32-bit  (int32x4_t)
  - 2 × 64-bit  (int64x2_t)
  - 4 × 32-bit float (float32x4_t)

4.2 高性能字符串长度计算

手写 NEON 实现的 strlen 可以比 glibc 默认实现快 3-5 倍:


#include <arm_neon.h>
#include <stdint.h>
#include <stddef.h>

// NEON 优化的 strlen
size_t neon_strlen(const char *s) {
    const char *p = s;
    
    // 1. 对齐到 16 字节边界
    while ((uintptr_t)p & 15) {
        if (*p == '\0') return p - s;
        p++;
    }
    
    const uint8x16_t zero = vdupq_n_u8(0);
    
    // 2. 每次处理 16 字节
    for (;;) {
        uint8x16_t data = vld1q_u8((const uint8_t *)p);
        
        // 比较是否等于零,结果中每个字节 0xFF == 零字节
        uint8x16_t cmp = vceqq_u8(data, zero);
        
        // vminv 在所有字节中取最小值(0 == 存在零字节,0xFF == 无零)
        // 如果有零字节,min 会是 0
        if (vminvq_u8(cmp) == 0) {
            // 找到零字节,精确定位
            uint8_t bytes[16];
            vst1q_u8(bytes, cmp);
            for (int i = 0; i < 16; i++) {
                if (bytes[i] == 0xFF) {
                    return (p - s) + i;
                }
            }
        }
        p += 16;
    }
}

// 验证正确性
#include <string.h>
#include <stdio.h>

int main(void) {
    const char *tests[] = {
        "",
        "a",
        "hello",
        "hello world, this is a longer string",
        "exactly16chars!!", // 正好 16 字符
        "more than 16 chars here!!!!!!",
        NULL
    };
    
    for (int i = 0; tests[i]; i++) {
        size_t std_len = strlen(tests[i]);
        size_t neon_len = neon_strlen(tests[i]);
        printf("'%s': std=%zu neon=%zu %s\n", tests[i], std_len, neon_len,
               std_len == neon_len ? "✓" : "✗ FAIL");
    }
    return 0;
}

4.3 SVE(可伸缩向量扩展)简介

ARMv9 引入的 SVE/SVE2 是 NEON 的革命性升级,核心特性是向量长度无关(Vector Length Agnostic)编程:


// SVE 代码编译后在不同 VL 的硬件上无需重新编译
// VL 可以是 128, 256, 512, 1024, 2048 bits

// 示例:SVE 向量加法(VL 无关)
// void sve_add(float *a, float *b, float *c, int n)
void sve_add(float *a, float *b, float *c, int n) {
    int i = 0;
    
    // 获取硬件向量长度(以 float 数量计)
    // svcntw() 返回当前硬件每向量可装载的 32-bit 元素数
    
    while (i < n) {
        // svwhilelt_b32: 生成谓码,标记哪些元素在有效范围内
        // 数据-dependent 长度处理,自动处理剩余元素
        svbool_t pg = svwhilelt_b32(i, n);
        
        // 加载向量
        svfloat32_t va = svld1_vnum_f32(pg, a, i);
        svfloat32_t vb = svld1_vnum_f32(pg, b, i);
        
        // 向量加法
        svfloat32_t vc = svadd_f32_z(pg, va, vb);
        
        // 存储结果
        svst1_vnum_f32(pg, c, i, vc);
        
        i += svcntw(); // 步进一个向量长度
    }
}

SVE2 vs NEON 的实用对比:

特性 NEON SVE2
向量宽度 固定 128-bit 128-2048 bit
剩余处理 需手动处理尾部 谓码自动处理
跨步访问 有限支持 原生支持(ld2/st2/ld3/st3)
谓码操作 无 完整谓码寄存器
适用场景 媒体处理 HPC、密码学、ML

五、ARM64 性能 Profiling 与调优

5.1 PMU(Performance Monitor Unit)实战

ARM64 的 PMU 提供了丰富的硬件性能计数器,远超 x86:


// 使用 perf_event_open 系统调用访问 PMU
// Linux 5.x+ 支持通过 perf_event_open 编程访问 PMU 计数器

#include <linux/perf_event.h>
#include <linux/hw_breakpoint.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>

// perf_event_open 包装
static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                            int cpu, int group_fd, unsigned long flags) {
    return syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags);
}

// ARM64 常用 PMU 事件编号(参考 ARM TRM)
#define ARM_PM_CYCLES         0x11  // 时钟周期
#define ARM_PM_INST_RETIRED   0x08  // 已退休指令数
#define ARM_PM_L1D_CACHE      0x04  // L1 D-Cache 访问
#define ARM_PM_L1D_CACHE_REFILL 0x03 // L1 D-Cache 填充
#define ARM_PM_BR_MIS_PRED    0x10  // 分支预测失败
#define ARM_PM_STALL_FRONTEND 0x23  // 前端停顿
#define ARM_PM_STALL_BACKEND  0x24  // 后端停顿

// 测量代码段的 IPC(每周期指令数)
void measure_ipc(void (*func)(void), const char *desc) {
    struct perf_event_attr pe;
    long long count1, count2;
    int fd1, fd2;
    
    // 配置:CPU 周期计数
    memset(&pe, 0, sizeof(pe));
    pe.type = PERF_TYPE_HARDWARE;
    pe.size = sizeof(pe);
    pe.config = PERF_COUNT_HW_CPU_CYCLES;
    pe.disabled = 1;
    pe.exclude_kernel = 1;
    pe.exclude_hv = 1;
    fd1 = perf_event_open(&pe, 0, -1, -1, 0);
    
    // 配置:指令计数
    pe.config = PERF_COUNT_HW_INSTRUCTIONS;
    fd2 = perf_event_open(&pe, 0, -1, -1, 0);
    
    // 启用计数器
    ioctl(fd1, PERF_EVENT_IOC_RESET, 0);
    ioctl(fd1, PERF_EVENT_IOC_ENABLE, 0);
    ioctl(fd2, PERF_EVENT_IOC_RESET, 0);
    ioctl(fd2, PERF_EVENT_IOC_ENABLE, 0);
    
    func(); // 执行被测代码
    
    // 禁用计数器
    ioctl(fd1, PERF_EVENT_IOC_DISABLE, 0);
    ioctl(fd2, PERF_EVENT_IOC_DISABLE, 0);
    read(fd1, &count1, sizeof(count1));
    read(fd2, &count2, sizeof(count2));
    
    printf("%s: cycles=%lld instructions=%lld IPC=%.2f\n",
           desc, count1, count2, (double)count2 / count1);
    
    close(fd1);
    close(fd2);
}

5.2 编译器向量化提示

在 ARM64 上,__builtin_expect 和 __restrict 对性能的影响非常大:


#include <arm_neon.h>
#include <stdint.h>

// 使用 restrict 帮助编译器做向量化
void vector_add(int32_t * __restrict__ a,
                const int32_t * __restrict__ b,
                const int32_t * __restrict__ c,
                int n) {
    // 对齐提示
    a = __builtin_assume_aligned(a, 16);
    b = __builtin_assume_aligned(b, 16);
    c = __builtin_assume_aligned(c, 16);
    
    // 告诉编译器 n 是 4 的倍数,帮助移除 peel loop
    n = n & ~3;
    
    #pragma GCC ivdep  // 告诉编译器无循环依赖
    for (int i = 0; i < n; i++) {
        a[i] = b[i] + c[i];
    }
    // GCC -O2 -march=armv8.2-a+simd 可产出 NEON 代码
}

// Apple M 系列额外 ARMv8.5-A 特性
void optimized_memcpy(void *dst, const void *src, size_t n) {
    // Apple M 系列对 DC ZVA 指令支持极好,快速清零内存
    if (src == NULL) {
        uint8_t *d = dst;
        // __builtin_memset 会被编译为 DC ZVA(零缓存块写入)
        __builtin_memset(d, 0, n);
        return;
    }
    __builtin_memcpy(dst, src, n);
}

5.3 分支预测与条件选择

ARM64 的 CSEL(条件选择)指令比分支跳转在不可预测的分支上性能更好:


// 编译器在 -O2 时通常会自动优化为 CSEL
// 但对于复杂条件,手动内联汇编可获得最优代码

static inline int32_t arm64_abs(int32_t x) {
    int32_t result;
    // CSEL: 条件选择,无分支跳转
    // cmp x, #0; csel result, x, negate, ge
    __asm__ (
        "cmp %w[x], #0\n\t"
        "cneg %w[res], %w[x], lt\n\t"
        : [res] "=r" (result)
        : [x] "r" (x)
        : "cc"
    );
    return result;
}

// 等价的标准 C:
// int abs(int x) { return x >= 0 ? x : -x; }
// GCC -O2 会生成相同代码

// 对于更复杂的条件操作,C 语言可能无法生成 CSEL:
static inline int32_t conditional_op(int32_t a, int32_t b, int32_t cond) {
    int32_t result;
    __asm__ (
        "cmp %w[cond], #0\n\t"
        "csel %w[res], %w[a], %w[b], ne\n\t"
        : [res] "=r" (result)
        : [a] "r" (a), [b] "r" (b), [cond] "r" (cond)
        : "cc"
    );
    return result;
    // 对比 C 版本:cond ? a : b;
    // 当 cond 的数据分布随机时,CSEL 比跳转快 2-3 个周期/次
}

六、Apple Silicon vs 服务器 ARM64:工程差异

6.1 内存模型差异

特性 Apple M1/M2/M3/M4 AWS Graviton3/4 Ampere AmpereOne
缓存行 128 字节 64 字节 64 字节
弱内存模型实现 强于标准 ARMv8 标准弱模型 标准弱模型
内存序屏障开销 ~2-3 周期 ~15-20 周期 ~10-15 周期
L1 延迟 ~4 周期 ~4 周期 ~4 周期
典型核心配置 4P+4E 64-96 同构核 80-192 同构核

Apple Silicon 的实现实际上比标准 ARMv8 规范更强:大部分内存操作表现出接近 TSO 的顺序性(推测性推测推测——Apple 没公开具体细节,但实测弱序 bug 更难触发)。

6.2 缓存行大小的重要性

Apple Silicon 为 128 字节缓存行,而大多数 ARM64 服务器为 64 字节。直接移植的填充代码会导致错误的结果:


// ❌ 危险:硬编码 64 字节缓存行大小
struct bad_align {
    atomic_int counter;
    char padding[60]; // 在 Apple M 系列上错误!
};

// ✅ 正确:运行时检测
size_t get_cache_line_size(void) {
    long sysconf_val = sysconf(_SC_LEVEL1_DCACHE_LINESIZE);
    if (sysconf_val <= 0) return 64; // 默认回退
    return (size_t)sysconf_val;
}

// 或使用 C11 标准方法
#include <stdalign.h>
#define CACHE_LINE_ALIGN alignas(_Alignof(max_align_t))  // 通常为 16 字节

// 更健壮的方式:对齐到最大可能值(避免跨设备兼容问题)
struct robust_counter {
    atomic_int counter;
    char padding[120]; // 支持最大 128 字节缓存行
};

// 或使用 C11 alignas 直接指定
struct alignas(128) apple_safe_counter {
    atomic_int counter;
};

// 实际内存影响分析:
// 4 核 × 128 字节 = 512 字节(填充后)
// 4 核 × 64 字节 = 256 字节(标准)
// 这在 L1 D-Cache 仅 64KB 的 ARM 核心上代价可忽略

6.3 编译器兼容性

不同编译器对 ARM64 特性的支持程度不同:


# 查看编译器支持的 ARMv8 扩展
gcc -march=help -E - < /dev/null 2>&1 | head -30

# 不同平台的最佳编译选项
# Apple Clang (M1-M4):
clang -O2 -mcpu=apple-m1

# AWS Graviton3 (ARMv8.4-A + SVE):
gcc -O2 -march=armv8.4-a+sve -mtune=neoverse-v1

# AWS Graviton4 (ARMv9.0-A + SVE2):
gcc -O2 -march=armv9-a+sve2+bti+pauth -mtune=neoverse-n2

# AmpereOne (ARMv8.6-A, 128+ 核):
gcc -O2 -march=armv8.6-a+fp16+sve -mtune=ampere1

# 通用跨平台(最低兼容 - ARMv8.0-A):
gcc -O2 -march=armv8-a+simd -mtune=generic

七、综合实战:ARM64 上的高性能内存池

将前面讨论的所有主题(内存屏障、缓存行、NUMA、编译器提示)综合到一个完整的高性能内存池实现:


#define _GNU_SOURCE
#include <stdatomic.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>

// 缓存行大小(运行时检测)
static const size_t CACHE_LINE = 64;

// 将 size 向上取整到 cache line 的倍数
static inline size_t cache_align(size_t size) {
    return (size + CACHE_LINE - 1) & ~(CACHE_LINE - 1);
}

// 内存块头:位于每个分配块之前
// 紧凑排列在同一缓存行中(8 字节对齐)
struct block_header {
    uint32_t size;      // 块大小(含 header)
    uint32_t flags;     // 标志位
};

// 分配器(缓存行对齐)
struct alignas(128) pool_allocator {
    atomic_uintptr_t free_list;  // LIFO 空闲链表头
    uintptr_t       base;        // 池基址
    size_t          pool_size;   // 池总大小
    atomic_size_t   allocated;   // 已分配字节数统计
};

#define FLAG_MAGIC 0xA1640000  // "ARM64" 标记
#define FLAG_FREE  0x0001
#define FLAG_USED  0x0002

// 初始化内存池
int pool_init(struct pool_allocator *pool, size_t size) {
    // 对齐到 cache line 的倍数
    size = cache_align(size);
    
    // 使用 mmap 分配页对齐内存
    void *mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
    if (mem == MAP_FAILED) return -1;
    
    pool->base = (uintptr_t)mem;
    pool->pool_size = size;
    atomic_store_explicit(&pool->allocated, 0, memory_order_relaxed);
    
    // 初始化 LIFO 空闲链表
    // 第一个块包含整个池空间
    struct block_header *first = (struct block_header *)mem;
    first->size = (uint32_t)size;
    first->flags = FLAG_MAGIC | FLAG_FREE;
    
    // 链表尾(NULL 指针)放在块的末尾
    atomic_store_explicit(&pool->free_list, pool->base, memory_order_release);
    
    // 存储链表 next 指针在外(避免覆盖 block_header 给用户)
    // 使用 DMB 确保初始化完成
    atomic_thread_fence(memory_order_seq_cst);
    
    return 0;
}

// 分配
void *pool_alloc(struct pool_allocator *pool, size_t size) {
    size = cache_align(size + sizeof(struct block_header));
    
    while (1) {
        uintptr_t old_head = atomic_load_explicit(&pool->free_list, memory_order_acquire);
        if (old_head == 0) return NULL;
        
        struct block_header *block = (struct block_header *)old_head;
        uintptr_t next = 0;
        if (block->size > size) {
            // 从头部切分
            uintptr_t new_block_addr = old_head + size;
            struct block_header *new_block = (struct block_header *)new_block_addr;
            new_block->size = block->size - size;
            new_block->flags = FLAG_MAGIC | FLAG_FREE;
            next = new_block_addr;
        }
        
        // CAS 原子操作:尝试将 free_head 从 old_head 改为 next
        if (atomic_compare_exchange_weak_explicit(
                &pool->free_list, &old_head, next,
                memory_order_acq_rel, memory_order_acquire)) {
            
            block->size = size;
            block->flags = FLAG_MAGIC | FLAG_USED;
            atomic_fetch_add_explicit(&pool->allocated, size, memory_order_relaxed);
            
            // 返回用户可见区域(跳过 header)
            return (void *)(old_head + sizeof(struct block_header));
        }
        // CAS 失败 → 重试
    }
}

// 释放
void pool_free(struct pool_allocator *pool, void *ptr) {
    if (!ptr) return;
    
    struct block_header *block = (struct block_header *)((uintptr_t)ptr - sizeof(struct block_header));
    
    // 验证 magic(调试模式下)
    // if ((block->flags & 0xFFFF0000) != FLAG_MAGIC) abort();
    
    block->flags = FLAG_MAGIC | FLAG_FREE;
    atomic_fetch_sub_explicit(&pool->allocated, block->size, memory_order_relaxed);
    
    // 将 block 插入 LIFO 链表头部
    uintptr_t block_addr = (uintptr_t)block;
    while (1) {
        uintptr_t old_head = atomic_load_explicit(&pool->free_list, memory_order_relaxed);
        // 注意:这里 block 的 header 后面本应存 next,
        // 但为了简化(不覆盖用户数据),我们只维护头指针链表
        // 实际生产中使用的侵入式链表可以在 header 前预留 next 指针
        
        if (atomic_compare_exchange_weak_explicit(
                &pool->free_list, &old_head, block_addr,
                memory_order_release, memory_order_relaxed)) {
            break;
        }
    }
}

// 销毁内存池
void pool_destroy(struct pool_allocator *pool) {
    munmap((void *)pool->base, pool->pool_size);
    memset(pool, 0, sizeof(*pool));
}

八、总结与展望

ARM64 系统编程的核心挑战不在于语法或 API,而在于对内存模型、缓存层次和硬件特性的深刻理解。关键要点:

  1. 弱内存模型是特性,不是 bug:正确使用 acquire/release 语义可以获得比 x86 TSO 更好的并发性能。
    1. 缓存行是第一生产力:ARM64 上缓存行大小可能不同(64 vs 128 字节),运行时检测和正确对齐是必备技能。
      1. NEON/SVE 是性能倍增器:手动 SIMD 优化的代码在 ARM64 上可能获得数量级的性能提升,远超编译器自动向量化的效果。
        1. 编译器是你的朋友,也是敌人:restrict、alignas、__builtin_assume_aligned 等提示是释放 ARM64 性能的关键。
          1. Apple Silicon 不是标准 ARM64:在 M1-M4 上验证通过的代码不一定在 Graviton 或 AmpereOne 上正确运行。
          2. 展望:ARMv9 的 SVE2、MTE(Memory Tagging Extension)、BTI(Branch Target Identification)和 PAuth(Pointer Authentication)正在重塑系统编程的安全边界。随着 RISC-V 和 ARM 架构的崛起,x86 独占数十年的系统编程范式正在被重新定义。尽早掌握这些新架构的特性,是每个系统程序员的必备功课。


            延伸阅读推荐:

            - ARM Architecture Reference Manual for A-profile architecture

            - ARM Cortex-A72 Software Optimization Guide

            - [Async: Miscompilation of sequential consistency atomics on arm64](https://kristerw.github.io/2022/06/17/arm64-atomics/)

            - [Preshing on Programming: Memory Reordering Caught in the Act](https://preshing.com/20120515/memory-reordering-caught-in-the-act/)

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部