如何在不读取数据到缓存的情况下实现4K及以上内存页清零
回答
你记的特性是x86-64下的**非临时写入(Non-Temporal Write, NT Write)**机制,刚好匹配你的需求:普通内存写入在目标地址不在Cache时会触发写分配,先把旧的整Cache行数据读入Cache再修改,这就是你说的无关数据占用Cache的问题;而非临时写入会直接绕过写分配逻辑,凑齐整Cache行数据后直接写入内存,完全不需要加载旧数据到各级Cache,对4K及以上连续内存的清零、拷贝场景效率提升非常明显。
别和操作系统层的零页机制搞混:比如mmap的MAP_ANONYMOUS、madvise(MADV_DONTNEED)返回的零页虽然也不需要你手动写零,但会触发缺页异常,开销远高于用户态指令,还会把内存控制权交回内核,完全不适合自定义内存池的场景。
GCC/Clang 可直接使用的intrinsic
所有你需要的intrinsic在GCC 4.9+、Clang 3.5+版本都已经支持,覆盖你说的Haswell及更新架构:
- 全零寄存器生成:
_mm256_zero_si256()(AVX2,256位全零,Haswell原生支持)、_mm512_zero_si512()(AVX-512,512位全零,Skylake-X及以上支持) - 非临时存储:
_mm256_stream_si256()(256位NT写)、_mm512_stream_si512()(512位NT写) - 存储围栏:
_mm_sfence(),用来保证非临时写入的结果对后续访问全局可见,写完必须调用 - 非临时读(可选,适合源数据后续不会再访问的拷贝场景):
_mm256_stream_load_si256()
如果不想手动拼SIMD逻辑,GCC 11+、Clang 12+还提供了__builtin_memset_non_temporal、__builtin_memcpy_non_temporal两个内置函数,传入目标地址、值/源地址、长度,编译器会自动根据当前编译的目标架构生成最优的NT指令序列,不过可控性不如手动写SIMD逻辑。
需要参考的原生汇编指令
如果要手写汇编或者核对编译器生成的代码,核心就这几条指令:
vmovntdq:对应256/512位的非临时整数存储,是实现NT写零、NT拷贝的核心指令sfence:存储围栏,对应上面的_mm_sfence()- 可选新指令:
movdir64b,Ice Lake、Zen3及以上架构支持,单次直接写入64字节数据完全不经过Cache,适合小批量非临时写入场景,老架构不支持,必须用CPUID检测特性位后才能用。
三类使用场景的落地参考
三个场景都可以基于上述指令实现,按需平衡Cache利用率和写入效率:
- 直接清零4K及以上内存
先将一个YMM/ZMM寄存器置为全零,从目标内存首地址开始,按寄存器宽度(32字节/64字节)对齐,循环调用非临时存储指令写入全零值,覆盖完整块后调用一次_mm_sfence()即可。自定义分配器一般都会做32/64字节地址对齐,刚好满足指令的对齐要求。 - 整块内存执行memcpy拷贝
不要用普通memcpy:普通memcpy会触发写分配把目标区域旧数据读入Cache,如果拷贝完的内存不会立刻访问,会造成无意义的Cache污染。实现时从源地址正常读取数据(如果源数据后续也不会访问,可以换成非临时读指令),再用非临时存储指令写入目标地址,写完执行_mm_sfence()即可,全程不会加载目标区域的旧数据进Cache。 - 混合操作(前64/512字节拷贝数据,剩余区域清零)
按访问热度分两段处理即可:开头的64/512字节是后续会立刻访问的热点数据,直接用普通SIMD load/store写入,让这部分留在Cache里降低后续访问延迟;后面剩余的大块冷区域,直接用上面的非临时写零逻辑处理,最后统一调用一次_mm_sfence()保证所有写入可见。
落地注意事项
- 非临时存储只适合4K及以上的连续大块内存,小于64字节的零散小块内存不要用NT指令,绕过Cache反而会提升访问延迟
- 最低支持Haswell的话,256位AVX2的NT指令是全兼容的,不需要额外做特性检测;如果要用到512位AVX-512指令或者
movdir64b,必须在初始化阶段用CPUID检测对应特性位,不要直接在不支持的CPU上执行 - 非临时写入的内存顺序弱于普通写入,绝对不能省略
_mm_sfence(),否则会出现后续访问读到旧数据的诡异问题
内容的提问来源于stack exchange,提问作者Cal
相关产品推荐
相关产品推荐

