CPU写内存时,如果每个写都直接发到内存总线,效率会很低。写缓冲区合并(Write Combining/Merging)技术把多个小写入攒起来,打包成一个大块一起发,既省带宽又降延迟。


1. 为什么要合并写入

现代CPU和内存的速度差距越来越大。一个3GHz的CPU,一个时钟周期是0.33ns,而DDR4内存的访问延迟要100ns左右。如果每次写都等内存确认,CPU99%的时间都在干等。

写缓冲区(Write Buffer)是解决这个问题的经典方法。CPU把写入先存到缓冲区,继续执行,由缓冲区异步刷到内存。但如果写入很零碎(比如连续几个8字节写),缓冲区很快会被填满,CPU还是得等。

写合并就是在这个基础上做的优化:如果新写入的地址和缓冲区里已有的写入连续,就把它们合并成一个大块

1.1 合并的效果

假设写缓冲区有4个条目,每个条目能存4个64位字(32字节)。

不合并的情况

  • 连续4个store,地址0x100、0x108、0x110、0x118
  • 每个store占一个条目
  • 4个条目用完,第5个store必须等前面刷完

合并的情况

  • 4个store地址连续,合并到一个条目
  • 只占1/4的缓冲区空间
  • 后面还能继续接受写入

合并后的32字节可以用一个burst transaction发到内存,比4次单独的8字节传输快得多。


2. 硬件实现原理

2.1 写缓冲区的结构

写缓冲区通常由多个条目组成,每个条目包含:

  • 地址标签:记录这段数据的起始地址
  • 数据块:存储实际数据(通常32-64字节)
  • 有效位图:记录哪些字节是有效的
  • 状态标志:是否在等待写入、是否可合并等

Intel Core i7的写缓冲区[61]:

  • 4个条目
  • 每个条目64字节
  • 支持合并(Write Merging)

AMD Zen4的写缓冲区更大,支持更多条目和跨缓存行合并。

2.2 合并的条件

两个写入能合并,必须满足:

  1. 地址连续:新写入的地址紧接在已有写入后面(或前面)
  2. 同一方向:都是store,不能和load混
  3. 内存类型允许:WB(Write-Back)或WT(Write-Through)类型可以合并,UC(Uncacheable)和WC(Write-Combining)有特殊情况

地址对齐的影响

如果写入地址按缓存行对齐(64字节边界),合并最容易。不对齐的写入可能需要拆分到两个条目。

// 对齐写入,容易合并
*((uint64_t*)0x1000) = 1;  // 0x1000是64字节对齐
*((uint64_t*)0x1008) = 2;  // 紧接后面,合并

// 不对齐写入,可能拆分
*((uint64_t*)0x1004) = 1;  // 跨两个缓存行

2.3 合并的硬件逻辑

简化版的合并判断逻辑:

// 写缓冲区合并判断
module write_buffer_merge (
    input [63:0] new_addr,
    input [63:0] new_data,
    input [7:0]  new_be,        // 字节使能
    input [3:0]  entry_valid,   // 各条目有效
    input [63:0] entry_addr [0:3],
    input [511:0] entry_data [0:3],  // 64字节数据
    input [63:0] entry_be [0:3],     // 64位字节使能
    output can_merge,
    output [1:0] merge_entry
);

// 检查每个条目是否能合并
genvar i;
generate
    for (i = 0; i < 4; i = i + 1) begin
        // 地址连续检查:新地址在条目范围内或紧邻
        wire addr_adjacent = (new_addr >= entry_addr[i] && 
                              new_addr < entry_addr[i] + 64) ||
                             (new_addr + 8 == entry_addr[i]);
        wire can_merge_i = entry_valid[i] && addr_adjacent;
        // ...
    end
endgenerate

endmodule

3. 性能影响:真实数据

3.1 Skadron & Clark的研究

1997年,Skadron和Clark研究了写缓冲区合并的效果[61][64]:

发现:即使是一个4条目、支持合并的写缓冲区,仍然会因为缓冲区满导致5%-10%的性能损失

这说明:

  1. 合并能缓解问题,但不能完全解决
  2. 缓冲区大小是关键,4条目在高强度写入下还是不够
  3. 现代处理器增加到8-12条目是有原因的

3.2 现代处理器的改进

处理器写缓冲区条目每条目大小合并能力
Intel Core i7464字节基本合并
Intel Skylake+8-1264字节增强合并
AMD Zen48+64字节跨缓存行合并
ARM Cortex-X6-864字节有限合并

3.3 实际性能测试

一个memcpy的性能对比(32MB数据,从CPU内存到GPU显存)1

模式耗时传输效率
普通写入~90ms低,多次小传输
Write Combining~6.8ms高13.3倍

注意:这个13.3倍的提升不只是因为合并减少了transaction数量(实际只减少了一半)。更重要的是,WC模式允许处理器:

  1. 先把数据预取到WC缓冲区
  2. 然后用burst传输方式推到总线
  3. 从WC缓冲区到总线的延迟(10ns)比从主存(100ns)快10倍

4. Write Combining内存类型

x86处理器通过MTRR(Memory Type Range Registers)或PAT(Page Attribute Table)可以设置内存类型23。其中**WC(Write Combining)**是一种特殊的内存类型:

4.1 WC vs WB vs UC

类型缓存写入行为适用场景
WB (Write-Back)写缓存,延迟写回普通内存
WT (Write-Through)写缓存+同时写内存需要一致性的场景
UC (Uncacheable)直接写内存,不合并设备寄存器
WC (Write-Combining)写合并缓冲区,批量写显存、帧缓冲区

4.2 WC的工作原理

WC内存的写入流程:

  1. CPU执行store到WC内存地址
  2. 数据进入WC缓冲区(不是L1/L2/L3缓存)
  3. 缓冲区积累数据,直到:
    • 缓冲区满(64字节)
    • 遇到序列化指令(SFENCE/MFENCE)
    • 读取WC内存地址(触发flush)
    • 中断或LOCK指令
  4. 一次性burst写入内存

关键特性

  • 弱序(Weakly Ordered):写入顺序不保证,可能被重排
  • 不可缓存:WC缓冲区不是缓存,不参与缓存一致性协议
  • 无snoop:其他CPU看不到WC缓冲区的数据,直到flush

4.3 编程中使用WC

Linux可以通过mmap设置WC内存:

#include <sys/mman.h>

// 分配WC内存
void* wc_mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

// 设置MTRR为WC类型(需要root或特殊权限)
// 或者使用现有的WC内存区域(如显存映射)

x86指令集提供非临时存储指令(Non-Temporal Stores):

// 使用MOVNTDQA/MOVNTDQ绕过缓存,直接写入WC缓冲区
#include <emmintrin.h>

void stream_write(__m128i* dst, __m128i* src, int n) {
    for (int i = 0; i < n; i++) {
        _mm_stream_si128(&dst[i], src[i]);  // MOVNTDQ
    }
    _mm_sfence();  // 确保所有WC写入完成
}

这些指令告诉CPU:这个数据是"流式"的,不会很快重用,不用进缓存,直接走WC缓冲区。


5. 实际应用场景

5.1 显卡帧缓冲区

GPU的显存映射到CPU地址空间时,通常设置为WC类型。因为:

  1. CPU写显存是单向的(CPU→GPU),不需要缓存一致性
  2. 写入通常是连续的(填充像素数据)
  3. 顺序不重要,重要的是吞吐量

如果误把显存设为WB类型,会导致:

  • 缓存污染(显存数据不会重用,却占用了缓存)
  • 额外的写回开销(缓存行替换时要写回显存)
  • 性能下降10倍以上

5.2 网络设备缓冲区

网卡的发送缓冲区通常也是WC类型。TCP/IP协议栈构造包时,直接写入WC内存,网卡DMA从WC内存读取。

5.3 必须禁用合并的场景

内存映射I/O(MMIO)

// 设备寄存器映射
volatile uint32_t* gpu_reg = ioremap(GPU_REG_ADDR, 4096);

// 错误:合并可能导致命令顺序错乱
gpu_reg[0] = CMD_START;  // 可能被合并到后面
gpu_reg[1] = PARAM;      // 实际先执行

设备寄存器通常要求:

  • 每次写入立即生效
  • 严格顺序
  • 不能合并

所以MMIO区域要标记为UC(Uncacheable),而不是WC。

原子操作

LOCK前缀的指令(如LOCK XCHG、LOCK CMPXCHG)必须绕过写缓冲区,确保全局可见性。这些操作不能合并。


6. 写缓冲区满的处理

当写缓冲区满且无法合并时,CPU必须等待。这称为写停顿(Write Stall)

6.1 停顿的原因

  1. 缓冲区满:4个条目都占用了,新store必须等
  2. 地址冲突:新store地址和缓冲区里某条目重叠,但数据还没刷完
  3. 内存类型限制:UC内存的写不能缓冲,必须等总线空闲

6.2 减少停顿的方法

硬件层面

  • 增加缓冲区条目(从4到8到12)
  • 提高合并率(更智能的地址匹配)
  • 预取和预写(提前准备总线)

软件层面

  • 批量写入:用memcpy代替逐个store
  • 对齐访问:确保地址按64字节对齐
  • 减少UC写入:设备寄存器访问批量处理

7. 总结

写缓冲区合并是缓解CPU-内存速度差距的有效手段:

优化效果代价
写缓冲隐藏写入延迟需要缓冲区硬件
写合并减少总线事务,提高带宽增加合并逻辑复杂度
WC内存类型最大化合并效果弱序,需要显式fence

关键要点

  • 合并需要地址连续,对齐访问很重要
  • 4条目缓冲区在现代CPU下仍可能满,导致5-10%性能损失
  • WC内存类型适合流式写入,但不适合需要顺序保证的场景
  • MMIO和设备寄存器必须禁用合并

理解写合并,写高性能代码时就能知道什么时候用memcpy,什么时候用movnt,什么时候必须sfence


参考


  1. CSDN博客. write combine机制介绍. 实测memcpy性能提升13.3倍… ↩︎

  2. LinkedIn. Understanding X86 CPU Cache, MTRR, and MSR in Cache-as-RAM Implementation. ↩︎

  3. OSDev Wiki. MTRR (Memory Type Range Registers). ↩︎

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐