写缓冲区合并:把零碎写入打包成批量传输
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 合并的条件
两个写入能合并,必须满足:
- 地址连续:新写入的地址紧接在已有写入后面(或前面)
- 同一方向:都是store,不能和load混
- 内存类型允许: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%的性能损失。
这说明:
- 合并能缓解问题,但不能完全解决
- 缓冲区大小是关键,4条目在高强度写入下还是不够
- 现代处理器增加到8-12条目是有原因的
3.2 现代处理器的改进
| 处理器 | 写缓冲区条目 | 每条目大小 | 合并能力 |
|---|---|---|---|
| Intel Core i7 | 4 | 64字节 | 基本合并 |
| Intel Skylake+ | 8-12 | 64字节 | 增强合并 |
| AMD Zen4 | 8+ | 64字节 | 跨缓存行合并 |
| ARM Cortex-X | 6-8 | 64字节 | 有限合并 |
3.3 实际性能测试
一个memcpy的性能对比(32MB数据,从CPU内存到GPU显存)1:
| 模式 | 耗时 | 传输效率 |
|---|---|---|
| 普通写入 | ~90ms | 低,多次小传输 |
| Write Combining | ~6.8ms | 高13.3倍 |
注意:这个13.3倍的提升不只是因为合并减少了transaction数量(实际只减少了一半)。更重要的是,WC模式允许处理器:
- 先把数据预取到WC缓冲区
- 然后用burst传输方式推到总线
- 从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内存的写入流程:
- CPU执行store到WC内存地址
- 数据进入WC缓冲区(不是L1/L2/L3缓存)
- 缓冲区积累数据,直到:
- 缓冲区满(64字节)
- 遇到序列化指令(SFENCE/MFENCE)
- 读取WC内存地址(触发flush)
- 中断或LOCK指令
- 一次性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类型。因为:
- CPU写显存是单向的(CPU→GPU),不需要缓存一致性
- 写入通常是连续的(填充像素数据)
- 顺序不重要,重要的是吞吐量
如果误把显存设为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 停顿的原因
- 缓冲区满:4个条目都占用了,新store必须等
- 地址冲突:新store地址和缓冲区里某条目重叠,但数据还没刷完
- 内存类型限制:UC内存的写不能缓冲,必须等总线空闲
6.2 减少停顿的方法
硬件层面:
- 增加缓冲区条目(从4到8到12)
- 提高合并率(更智能的地址匹配)
- 预取和预写(提前准备总线)
软件层面:
- 批量写入:用memcpy代替逐个store
- 对齐访问:确保地址按64字节对齐
- 减少UC写入:设备寄存器访问批量处理
7. 总结
写缓冲区合并是缓解CPU-内存速度差距的有效手段:
| 优化 | 效果 | 代价 |
|---|---|---|
| 写缓冲 | 隐藏写入延迟 | 需要缓冲区硬件 |
| 写合并 | 减少总线事务,提高带宽 | 增加合并逻辑复杂度 |
| WC内存类型 | 最大化合并效果 | 弱序,需要显式fence |
关键要点:
- 合并需要地址连续,对齐访问很重要
- 4条目缓冲区在现代CPU下仍可能满,导致5-10%性能损失
- WC内存类型适合流式写入,但不适合需要顺序保证的场景
- MMIO和设备寄存器必须禁用合并
理解写合并,写高性能代码时就能知道什么时候用memcpy,什么时候用movnt,什么时候必须sfence。
参考
更多推荐

所有评论(0)