x86_64架构下如何将内存解引用延迟控制在3.6周期的10倍以内?
问题背景与需求
我正在优化并发链表访问,针对x86_64架构(AMD Ryzen处理器)、Ubuntu系统,对内存解引用的平均耗时做了基准测试,结果如下:
- 连续邻近内存访问平均耗时3.6处理器周期;
- 非邻近内存访问的平均耗时最高攀升至390周期,是前者的110倍。
需明确技术方案:在不更换处理器架构(AMD/Intel x86_64)和操作系统(Ubuntu)的前提下,如何让内存解引用的耗时最多仅为3.6周期的10倍(即≤36周期),而非当前的110倍?
基准测试代码
#include <iostream> #include <iomanip> #include <cstdint> #define repeat_0x10(x) x x x x x x x x x x x x x x x x #define repeat_0x100(x) repeat_0x10(repeat_0x10(x)) #define repeat_0x1000(x) repeat_0x100(repeat_0x10(x)) const int loopn=0x4000; #define asmTimes 0x100 #define tt(x) repeat_0x100(x) #include <x86intrin.h> #include <unistd.h> using namespace std; int64_t aaa=0x0; typedef void fT(); // 传入一个函数,bench0执行它n次,返回执行所需的处理器时钟周期数 // 执行前会将变量aaa的地址加载到R15寄存器 auto bench0(fT f,size_t n=loopn){ auto s1=__rdtsc(); asm ("lea %0,%%r15" : : "m" (aaa)); for(int i=n;i--!=0;) f(); auto s2=__rdtsc(); return s2-s1; } // 传入一个由rep次重复代码组成的函数,循环执行n次,返回单次重复代码所需的时钟周期数(浮点型) auto bench(fT f,size_t n=loopn,size_t rep=asmTimes){ return (float)bench0(f,n)/n/rep; } #define countof(A) (sizeof(A)/sizeof(*A)) void* P[0x1000000]; // 构建链表,链表中连续节点的间隔为"step" void initP(size_t step){ void* p=nullptr; for(size_t i=0;i!=step;i++) for(size_t j=0;j!=countof(P);j+=step){*(P+j+i)=p;p=P+j+i;} p=P+countof(P)-1; //for(size_t i=countof(P);i--!=0;) p=*(void**)p; std::cout<<" p="<<p<<endl; } void walkP(){repeat_0x100(asm("mov (%r15),%r15");)} auto cpuFrequency(){ const int sleepN=500000; auto t1=__rdtsc(); usleep(sleepN); auto t2=__rdtsc(); return double(t2-t1)/(sleepN*1e-6); } int main(int argc, char **argv) { cout<<" cpuFrequency()="<<cpuFrequency()<<" sizeof(void*)="<<sizeof(void*)<<endl; for(size_t i=0;i<0x10;i++){ aaa=(uintptr_t)(P+countof(P)-1); size_t step=1<<i; initP(step);cout<<"bench(walkP("<<step<<"))="<<bench(walkP,0x10000,0x100)<<endl; } return 0; }
测试结果
cpuFrequency()=3.69355e+09 sizeof(void*)=8 p=0 bench(walkP(1))=3.6146 p=0 bench(walkP(2))=4.48525 p=0 bench(walkP(4))=8.33419 p=0 bench(walkP(8))=16.4063 p=0 bench(walkP(16))=126.335 p=0 bench(walkP(32))=177.424 p=0 bench(walkP(64))=336.674 p=0 bench(walkP(128))=340.422 p=0 bench(walkP(256))=350.896 p=0 bench(walkP(512))=360.694 p=0 bench(walkP(1024))=362.763 p=0 bench(walkP(2048))=367.925 p=0 bench(walkP(4096))=391.085 p=0 bench(walkP(8192))=386.941 p=0 bench(walkP(16384))=398.989 p=0 bench(walkP(32768))=389.542
技术优化方案
以下是针对x86_64(AMD Ryzen)+ Ubuntu环境的优化手段,可将非邻近内存解引用耗时控制在连续访问的10倍以内:
1. 硬件预取机制利用
- 显式预取指令:在访问链表节点前,使用
__builtin_prefetch()或x86汇编指令prefetcht0/prefetcht1提前加载下一个节点到缓存。例如遍历链表时,每次解引用当前节点后立即预取下一个节点地址:
AMD Ryzen的预取器对显式预取响应良好,可将高延迟内存访问转化为缓存命中。void walkP() { repeat_0x100( asm("mov (%r15),%r15"); asm("prefetcht0 (%r15)"); ); } - 调整预取距离:根据链表节点间隔和CPU缓存行大小(通常64字节),设置合适的预提前瞻步数,避免预取失效或缓存污染。
2. 内存布局优化
- 节点紧凑化:将链表节点的核心数据(含指针)压缩到同一缓存行内,避免跨缓存行访问。必要时采用分离式存储,将高频访问的指针、数据与低频数据拆分。
- 强制内存对齐:用
alignas(64)关键字确保链表节点和指针地址按64字节(缓存行大小)对齐,避免缓存行拆分访问:struct Node { alignas(64) Node* next; // 其他数据字段 }; - 内存池分配:使用自定义内存池(如基于
mmap或posix_memalign的固定块分配器)分配链表节点,确保节点在物理内存中连续或邻近分布,减少页表跳转和TLB miss。
3. 并发访问优化
- 链表分片:将大链表拆分为多个小分片,每个分片由单独线程负责访问,减少线程间缓存争用(伪共享)。每个分片的节点集中存储,提升局部性。
- 隔离共享变量:将线程间共享的控制变量(锁、原子计数器)用
alignas(64)单独放在缓存行中,防止多线程频繁修改同一缓存行导致的缓存失效风暴。 - 批量操作:在并发场景下,批量读取多个链表节点到本地缓存后再处理,利用CPU乱序执行能力隐藏内存延迟。
4. 软件局部性优化
- 链表转数组遍历:若访问模式允许(如读多写少、短暂数据不一致可接受),定期将链表节点复制到连续数组中,利用数组的连续内存访问特性获得缓存命中优势,完成后同步回链表。
- 循环展开与指令调度:手动展开遍历循环,增加指令级并行度,让CPU乱序执行单元提前调度内存访问指令,隐藏延迟。
5. 系统层面优化
- 启用透明大页:在Ubuntu上执行
echo always > /sys/kernel/mm/transparent_hugepage/enabled强制开启透明大页,减少TLB miss次数,降低地址转换延迟。 - 线程核心绑定:用
sched_setaffinity将线程绑定到同一CCX(AMD Ryzen核心复合体)内的CPU核心,避免线程迁移导致的缓存失效,利用CCX共享的L3缓存提升命中率。
内容的提问来源于stack exchange,提问作者George Kourtis
相关产品推荐
相关产品推荐

