缓存命中和缓存未命中测量ARMV8

问题描述

我一直在尝试测量缓存命中和缓存未命中。
我一直在研究1.5GHz的四核Cortex-A72(ARM v8)64位SoC。
我用来衡量缓存命中率的C代码是:

#define _GNU_SOURCE
#include <assert.h>
#include <sched.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/random.h>
#include <stdbool.h>
#include <fcntl.h>
#include <fcntl.h>
#include <sys/time.h>
    
char *chunk;
const size_t chunk_size = 1<<30; 
    
/* FUNCTIONS */

struct timeval start_time,start_time1;  
double get_diff(){
struct timeval end_time;
            int rc = gettimeofday(&end_time,NULL);
            assert(rc == 0);
            return (end_time.tv_sec - start_time.tv_sec + (double) (end_time.tv_usec - start_time.tv_usec) / 1e6);
}
    
void print_affinity(){
        cpu_set_t mask;
        long nproc,i;
        if (sched_getaffinity(0,sizeof(cpu_set_t),&mask) == -1){
            perror("sched_getaffinity");
            assert(false);
        }
        nproc = sysconf(_SC_NPROCESSORS_ONLN);
        printf("sched_getaffinity = ");
        for (i = 0; i < nproc; i++)
            printf("%d ",CPU_ISSET(i,&mask));
}
    
void bind_to_cpu (){
        cpu_set_t mask;
    
        print_affinity();
        printf("\n");
        printf("sched_getcpu = %d\n",sched_getcpu());
        CPU_ZERO(&mask);
        CPU_SET(0,&mask);
        if (sched_setaffinity(0,&mask) == -1) {
            perror("sched_setaffinity");
            assert(false);
        }
        print_affinity();
        printf("\nsched_getcpu = %d\n",sched_getcpu());
}

void reset_mem(){
    memset(chunk,-1,chunk_size);
}

void initialize(size_t chunk_size){

    chunk = (char *) mmap(NULL,chunk_size,PROT_READ |  MAP_POPULATE  |PROT_WRITE,MAP_ANONYMOUS | MAP_PRIVATE,0);
    assert(chunk!=MAP_FAILED);
    //initialize all bits to INIT_BIT value
    printf("Initializing memory...\n\n");
    reset_mem();

}


int main(int argc,char** argv){

    bind_to_cpu(); // pinning/binding cpu
    initialize(chunk_size);
    uint64_t temp=0 ;
    size_t offset1 = (rand() << 12) % chunk_size;
    size_t offset2 = (rand() << 12) % chunk_size;
    uint64_t *addr1 = (uint64_t*) (chunk+offset1);
    uint64_t *addr2 = (uint64_t*) (chunk+offset2);
    double time_result;
    sched_yield();
    __asm__ __volatile__("dc civac,%0\n\t" : : "r" (addr1) :"memory");
    __asm__ __volatile__("dc civac,%0\n\t" : : "r" (addr2) :"memory");
        
    for(int i =0; i<5000;i++){
        gettimeofday(&start_time,NULL);
        volatile uint64_t value;
        asm volatile ("LDR %0,[%1]\n\t"
        : "=r" (value)
        : "r" (addr1)
        );
        asm volatile ("LDR %0,[%1]\n\t"
        : "=r" (value)
        : "r" (addr2)
        );
        time_result += get_diff();
        //__asm__ __volatile__("dc civac,%0\n\t" : : "r" (addr1) :"memory");
        //__asm__ __volatile__("dc civac,%0\n\t" : : "r" (addr2) :"memory");   
    }
    sched_yield();
    printf("Total Time: %f\n\n",time_result);

    return 0;
}

用于测量缓存未命中的代码是相同的,但是使用两个带有注释的刷新指令:

__asm__ __volatile__("dc civac,%0\n\t" : : "r" (addr1) :"memory");  
__asm__ __volatile__("dc civac,%0\n\t" : : "r" (addr2) :"memory");  

因此,当我使用LDR指令时,一切似乎都正常,并且得到了以下输出:

Cache hit:  Cache miss:

0.000522    0.001503
0.000558    0.001696
0.000584    0.001977
0.000712    0.002032
0.000683    0.001137

当我使用STR指令时:

for(int i =0; i<5000;i++){
    gettimeofday(&start_time,NULL);
    asm volatile("str %x1,%x0" : "=m"(*addr1) : "r"(temp));
    asm volatile("str %x1,%x0" : "=m"(*addr2) : "r"(temp));
    time_result += get_diff();
    __asm__ __volatile__("dc civac,%0\n\t" : : "r" (addr2) :"memory"); 
}

我得到了这些输出:

Cache hit:  Cache miss:

0.000603    0.000299
0.000287    0.000311
0.000376    0.000290
0.000311    0.000305
0.000518    0.000297

缓存命中和缓存未命中之间的区别非常细微。
为什么呢我不是以正确的方式刷新缓存吗?

解决方法

高性能CPU的存储缓冲区使存储指令的执行从提交到高速缓存脱钩,从而使实际存储指令本身能够快速(推测地)执行,而与高速缓存的命中或未命中无关。 (请参见thisthisthis,也Does processor stall during cache coherence operation

在第二个版本中,您只是在定时 个存储,而不是dc civac instructions,这些存储必须做确保将脏数据写回到RAM的实际工作str指令本身仅需要将存储数据和地址写入存储缓冲区,而不必等待其提交到L1d缓存。

但是在您的LDR版本中,您正在计时实际的缓存丢失加载,直到它们实际从缓存中读取数据后,该加载才能完成。


如果您计时整个循环(包括dc civac指令),您可能会看到一些有意义的东西。与其注释掉dc civac指令,不如给它们地址不同的缓存行,而不是您要加载或存储到的缓存行。

在允许乱序执行的内核上尤其如此。当几个指令可以用读取时钟的指令重新排序时,以及当正常情况是许多指令处于“运行中”状态时,定时几个指令的意义并不大。排空障碍的执行障碍,以便您可以计时某些东西,这会创造出相当人为的条件,而计时开销意味着您永远无法以这种方式计时某物的实际成本。


gettimeofday与高速缓存未命中相比总开销很高;最好构建一个可以运行多个周期且时间不限的测试。

也不要使用全局变量;没有理由不只是将arg传递给get_diff

此外,您的负载测试出于明显的原因使用了volatile uint64_t value;作为asm输出,从而迫使编译器针对每次负载向堆栈发出2个存储。 asm volatile语句确保装入。除非CPU积极优化以放弃未使用的加载结果(如果以后的指令覆盖寄存器,则不要等待它们),您就可以让这些加载不使用。

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...