```c++constsize_tshm_size=16*1024*1024;//16Mstaticcharshm[shm_size];std::atomicshm_offset{0};voidf(){for(;;){autooff=shm_offset.fetch_add(sizeof(long));如果(关闭>=shm_size)中断;*(长*)(shm+关闭)=关闭;}}```看上面的程序,shm是16M字节的内存,而我的测试机的L3Cache是??32M,所以我选择16M的值,保证shm数组可以存放在Cache中。在循环中,f()函数将shm当成一个long类型的数组,依次为每个元素赋值。shm_offset用于记录偏移位置,shm_offset.fetch_add(sizeof(long))自动增加shm_offset的值(因为x86_64系统上long的长度为8,所以shm_offset每次增加8字节),返回增加前的值。在shm上为长数组的每个元素赋值后,循环结束并从函数返回。因为shm_offset是一个原子类型的变量,多线程调用f()仍然可以正常工作。虽然多个线程会竞争shm_offset,但每个线程都会独占地为每个long元素赋值,多线程并行会加快对shm的赋值操作。我们添加多线程调用,代码如下:```c++std::atomicstep{0};constintTHREAD_NUM=2;voidwork_thread(){constintN=10;对于(intn=1;n<=N;++n){f();++步骤;while(step.load()=shm_size)返回;*(long*)(shm+off)=j;off+=sizeof(long);for循环中,每次shm_offset不再是8字节(sizeof(long)),而是8*16=128字节,然后在内循环中,赋值16个Long连续元素,并且然后下一个循环再次增加128字节,直到完成整个shm的赋值。编译后重新执行程序,结果显示耗时减少到0.06秒,相比之前的耗时3.4秒,f_fast的性能有了很大的提升。```time./a.outreal0m0.062suser0m0.110ssys0m0.012s```f和f_fast的行为区别shm数组总共有2M长的元素,因为16M/sizeof(long)=>2M,1.f()函数逻辑线程1和线程2的work_thread会交替给shm元素赋值,shm的2M长的元素会一一赋值给两个线程进行赋值。比如:可能元素1被线程1赋值,元素2被线程2赋值,然后元素3和元素4被线程1赋值,然后元素5被线程2赋值……每次调度一个元素,shm_offset会自动增加8字节,所以不会出现2个线程给1个元素赋值的情况2.f_fast()函数的行为逻辑每调度一个元素,shm_offset就自动增加128字节(16个元素).这16个字section整体分配给线程1或线程2;虽然线程1和线程2仍然会交错操作shm元素,但是以16个元素(128字节)为单位,这16个连续的元素不会被赋值,一次分配给不同线程的16个元素会在内循环。为什么在一个线程中执行时f_fast更快?乍一看,f_fast()中的shm_offset.fetch_add()调用频率降低为原来的1/16。我们有理由怀疑减少原子变量的竞争会导致更快的程序执行。为了验证,让我们将原子变量test的fetch_add添加到内部循环中。测试原子变量的竞争会像f()函数中的shm_offset.fetch_add()一样激烈竞争。修改后的f_fast代码变为:```c++voidf_fast(){for(;;){constlonginner_loop=16;自动关闭=shm_offset.fetch_add(sizeof(long)*inner_loop);for(longj=0;j=shm_size)返回;*(long*)(shm+off)=j;关闭+=sizeof(长);}}}```避免test编译器优化了对.fetch_add(1)的调用,我们在main函数的末尾打印出test的值。编译测试后,结果显示执行时间只是略微增加到`real0m0.326s`。所以,很明显,导致性能飙升的并不是原子调用频率的降低。我们重新审视一下f()循环中的逻辑:f()循环中的操作非常简单:原子自增、判断、赋值。会不会是赋值太慢了?我们把f()中的赋值注释掉,再次测试,发现它的速度有了很大的提升。看起来`*(long*)(shm+off)=off;`是一行执行缓慢的代码,但是这显然只是一行赋值。我们拆开看看,就是一条mov指令,源操作数是寄存器,目的操作数是内存地址,从一个寄存器复制数据到内存地址,而这个内存数据应该缓存,为什么会这样慢的?羊毛布?答案现已揭晓。f()性能背后的罪魁祸首是错误共享。那么什么是虚假分享呢?要想弄清楚这个问题,就不得不接触到CPU的架构,以及CPU是如何访问数据的。再回顾一下多核Cache结构:背景知识我们知道现代CPU可以有多个核心,每个核心都有自己的L1-L2缓存。L1还区分了数据缓存(L1-DCache)和指令缓存(L1-ICache)。L2不区分数据和指令缓存,而L3是跨内核共享的。L3通过内存总线连接到内存,内存由所有CPU和所有Core共享。CPU访问L1Cache的速度大约是访问内存的100倍。Cache作为CPU和内存之间的缓存,降低CPU对内存的访问频率。从内存加载数据到Cache时,长度单位是CacheLine,CacheLine的长度通常为64字节。因此,即使只读取了一个字节,包含该字节的整个CacheLine也会被加载到缓存中。同样,如果修改了一个字节,那么整个CacheLine最终都会被flush到内存中。如果一段内存数据被多个线程访问,假设多个线程在多个Core上并行执行,那么它会被加载到多个Core的LocalCache中;这些线程运行在哪个Core上,会被加载到哪个Core的LocalCache中,因此,内存中的一条数据会同时在不同Core的Cache中有多个副本。如果我们修改了Core1缓存中的某个数据,则需要将该数据所在的CacheLine的状态同步到其他Core的缓存中,Core可以通过内核间的消息来同步状态,比如发送Invalidate向其他核发送消息,接收到消息的核会使相应的CacheLine失效,然后从内存中重新加载最新的数据。加载到多个Core缓存中的同一个CacheLine会被标记为共享(Shared)状态。在共享状态下修改缓存行,需要先获得缓存行的修改权(独占)。MESI协议用于保证多核Cache的一致性,更多细节请参考MESI文档。实例分析现在让我们看看我们的程序。假设线程1运行在Core1上,线程2运行在Core2上。因为shm被线程1和线程2两个线程并发访问,所以shm的内存数据会以CacheLine粒度同时加载到两个Core的Cache中。因为是多核共享的,所以CacheLine被标记为Shared状态。假设线程1在offset64的位置写了一个8字节的数据(sizeof(long)),想要修改一个状态为Shared的CacheLine,Core1会向Core2发送一个核间通信消息来获取CacheLine的独占权,之后Core1可以修改LocalCache。线程1执行完`shm_offset.fetch_add(sizeof(long))`后,shm_offset会增加到72,此时运行在Core2上的线程2也会执行`shm_offset.fetch_add(sizeof(long))`,返回72,增加shm_offset为80,线程2接下来需要修改shm[72]的内存位置,因为shm[64]和shm[72]在一个CacheLine中,而这个CacheLine设置为Invalidate,所以需要重新加载这个来自内存CacheLine,而在此之前,Core1上的线程1需要将CacheLine刷新到内存中,这样线程2才能加载最新的数据。这种交替的执行方式相当于需要在Core1和Core2之间频繁发送内核间消息。接收到消息的Core对应的CacheLine失效,数据从内存中重新加载到Cache中。Cache中的数据需要刷新到内存中。这实际上相当于丢弃了Cache,因为每次读写都直接和内存打交道,Cache的作用不复存在,性能下降。对于多核多线程程序,由于并发读写同一个CacheLine中的数据(相邻位置的内存数据),CacheLine频繁失效,内存频繁Load/Store导致性能急剧下降,这被称为虚假共享。这是一个性能杀手。另一个伪共享的例子假设线程x和y分别修改了Data的a和b变量。如果频繁调用,根据前面的分析,也会出现性能不佳的情况。如何避免?```c++structData{inta;intb;};数据数据;//globalvoidthread1(){data.a=1;}voidthread2(){data.b=2;}```**空间换时间**避免缓存错误共享导致性能下降的想法是以空间换时间,通过在a和b成员之间添加padding,让a和b这两个变量分布到不同的CacheLine上,这样a和b对b的修改作用在不同的CacheLine上,可以避免问题缓存行故障。```c++structData{inta;内部填充[60];intb;};```Linux内核中存在__cacheline_aligned_in_smp宏定义,解决伪共享问题。```c#ifdefCONFIG_SMP#define__cacheline_aligned_in_smp__cacheline_aligned#else#define__cacheline_aligned_in_smp#endifstructData{inta;intb__cacheline_aligned_in_smp;};```从上面的宏定义可以看出:在多核(MP)系统中,宏定义是__cacheline_aligned,即CacheLine的大小。在单核系统中,宏定义为空。虚假分享的问题。由于多个CPU和多个核在一个CacheLine中同时读写内存数据,会出现虚假共享。那么,我们的`atomicshm_offset`的fetch_add()操作也满足这个条件,多线程同时读写同一个shm_offset变量,为什么性能还不错呢?我们反汇编发现`atomic.fetch_add`会被翻译成`lock;xadd%rax(%rdx)`,lock是一个指令前缀,与其他指令结合使用。总线锁的作用是锁定总线,然后执行后面的xadd。在此期间,没有其他线程可以访问任何内存数据。其实lockbus的操作比较重,相当于一个全局的内存总线锁。锁前缀后的指令操作直接作用于内存,绕过了缓存,锁也相当于一个内存屏障。但是看Intel的手册,发现在执行lock指令的时候,CPU会根据情况决定是锁定cache还是断言#LOCK信号(lockbus)。如果访问的内存区域已经缓存在处理器的缓存行中,Intel现代处理器不会断言#LOCK信号,它会将缓存行锁定在CPU的缓存中,在锁定期间,其他CPU不能同时缓存此后数据被修改,通过缓存一致性协议保证修改的原子性。此操作称为“缓存锁定”。Falsesharing对应的是多个线程同时读写一个CacheLine的多个数据。Core-A修改数据x后,CacheLine会被设置为Invalid。Core-B读取另一个cacheline的数据y,这就需要Core-A将CacheLine存储到内存中,然后Core-B从内存中加载对应的CacheLine,数据必须经过内存。而原子的,多个线程修改同一个变量。lock指令的前缀应该使用缓存锁(lockCacheLine)。CacheLine中atomic的最新值可以通过核间消息发送给其他核。不需要频繁的Store/Load,所以性能不会太差。.