RFO可以算是Intel CPU上的原子添加操作和高速缓存行锁定吗?

时间:2018-10-07 22:51:50

标签: x86 shared-memory cpu-architecture perf atomicity

我试图了解原子加法运算的本质。因此,我正在Broadwell机器上运行以下代码。

int main(int argc, char ** argv){
    int nThreads = -1;
    float shareFrac = -1;
    uint64_t nIter = -1;

    ParseArg(argc, argv, nThreads, shareFrac, nIter);

    atomic<uint64_t> justToAvoidCompilerOptimization;

    #pragma omp parallel num_threads(nThreads)
    {
        int me = omp_get_thread_num();
        atomic<uint64_t> *tsData = &trueSharingData.data[0];
        atomic<uint64_t> *privateData = &(new SharedData_t())->data[0];
        for(uint64_t i = 0 ; i < nIter; i++) {
            // Use RDTSC as a proxy random number generator
            unsigned long lo, hi;
                asm volatile( "rdtsc" : "=a" (lo), "=d" (hi) ); 
                int rNum  = (lo % 54121) % 100; // mod by a prime.
            // if the random number is < shareFrac, perform a shared memory operation
            if (rNum < shareFrac) {
                *tsData += rNum2;
            } else {
                *privateData += rNum;
            }
        }       
        justToAvoidCompilerOptimization += *tsData;     
        justToAvoidCompilerOptimization += *privateData;        
    }


    return justToAvoidCompilerOptimization.load() ^ justToAvoidCompilerOptimization.load();
}

在此代码中,基本上每个线程执行原子加法操作nIter的次数为nIter作为循环行程计数。在每次循环迭代中,原子加法运算都可以在共享内存位置或线程局部变量上执行。

在共享存储器位置上执行原子加法操作所花费的循环行程计数的分数由参数shareFrac确定。例如,如果shareFrac为0.3,而nIter为1000,则可以预期在共享内存位置执行原子加法大约300次。


因此,我进行了一个小实验,其中随着shareFrac值的增加,我多次运行了此简单代码。对于每次运行,我都使用perf计数了L2_RQSTS.RFO_MISS事件的发生。我还将比较perf给出的计数与预期计数。预期的计数只是nthreads * nIter * shareFrac

结果如下。

nThreads = 2,nIter = 1亿
nThreads = 2, nIter = 100 millions

nThreads = 8,nIter = 1亿
nThreads = 8, nIter = 100 millions

从图中可以看出,在大多数运行中,RFO未命中计数超过了预期计数。这怎么可能??一个可能的解释是,原子添加使RFO希望读取并更新后再行。 但是,在读取和写入之间可能会窃取线路,在这种情况下,必须将线路调回。但是,据我所知,对于x86上的原子操作,缓存行已被锁定,因此,一旦获得了独占许可,就不得窃取该缓存行。还是我的理解不正确?

为了消除由于预取而导致高速缓存行传输的可能性,在获得这些结果之前,我还取消了机器所有内核上的硬件预取器。

1 个答案:

答案 0 :(得分:3)

我认为当前Intel始终无条件锁定原子操作的缓存行的假设可能是不准确的,因此根据访问次数可以准确预测L2丢失的次数。

例如,this Intel patent的背景描述了锁定指令的“常规”机制,该机制用于直接背对背执行指令的锁定/加载和解锁/存储部分这样,关联的线路就可以很容易地始终保持锁定状态。我认为这大致上与您描述它的工作方式相符,并且如果仅以这种方式工作,您可能会期望L2 RFO丢失会遵循预期的路线。

然而,专利本身描述了一种用于放松锁定要求的机构。特别是,尽早执行操作的加载/锁定部分(基本上作为普通加载),并推测在加载执行和存储提交之间的时间里关联的缓存不会“被盗”。如果确实发生了这种被盗的高速缓存行,则需要重播该操作。用专利中的英特尔话来说:

  

但是,如果预测是特定的锁定指令   实际上不会被竞争,那么有可能继续进行   投机发行的正常负载微操作并监控   与监视逻辑116有关的存储器位置以确定   是否有任何竞争迹象。因此,我们可能实际上并没有   在执行读取-修改-写入部分时锁定内存位置   强制执行原子性的指令,而是执行这些部分   分别观察可能表明   另一个处理器或线程可能破坏了对   原子性。这样的竞争指示可以包括对缓存的监听   包含加载指令目标地址的行,   中断,或者后续的store_unlock微操作未命中   缓存。

     

在一些实施例中,监控器逻辑116可以监控多个   处理器内存在的现有逻辑信号。如果没有竞争   指示出现在一段时间内,相当于   锁定状态,然后是推测性发出的正常负载   微操作可能会正常退休。这可能会导致乱序   执行锁定指令并增强处理器性能。   但是,如果确实出现了竞争迹象,则管道可能必须   被刷新并重新执行锁定指令。

那只是一个小片段,但包含了一个相关的想法:尝试以一种与乱序执行更兼容的方式执行锁,如果失败,请尝试采用一种更为保守的方法。该专利继续解释了预测变量如何工作,并与分支预测进行了类比。基本方法是简单地基于每个IP跟踪竞争行为。

这可以解释为什么额外的RFO事件在shareFrac接近100%时变为零:此时,这些行的竞争非常激烈,以至于尝试触发更具攻击性的锁定实现的启发式/预测器不会触发,所以它总是走保守路线。

您也许可以通过测试来验证这一理论,该测试检测到乱序执行的缺乏或存在,并表明当RFO请求数量增加时,也会发生一些OoO执行。