【问题标题】:Cache size estimation on your system?您系统上的缓存大小估计?
【发布时间】:2014-02-13 11:43:17
【问题描述】:

我从这个链接获得了这个程序(https://gist.github.com/jiewmeng/3787223).I 一直在搜索网络,以更好地了解处理器缓存(L1 和 L2)。我希望能够编写一个程序,使我能够猜猜我的新笔记本电脑上 L1 和 L2 缓存的大小。(仅用于学习目的。我知道我可以查看规格。)

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define KB 1024
#define MB 1024 * 1024

int main() {
    unsigned int steps = 256 * 1024 * 1024;
    static int arr[4 * 1024 * 1024];
    int lengthMod;
    unsigned int i;
    double timeTaken;
    clock_t start;
    int sizes[] = {
        1 * KB, 4 * KB, 8 * KB, 16 * KB, 32 * KB, 64 * KB, 128 * KB, 256 * KB,
        512 * KB, 1 * MB, 1.5 * MB, 2 * MB, 2.5 * MB, 3 * MB, 3.5 * MB, 4 * MB
    };
    int results[sizeof(sizes)/sizeof(int)];
    int s;

    /*for each size to test for ... */
    for (s = 0; s < sizeof(sizes)/sizeof(int); s++)
    {
            lengthMod = sizes[s] - 1;
            start = clock();
            for (i = 0; i < steps; i++)
            {
                arr[(i * 16) & lengthMod] *= 10;
                arr[(i * 16) & lengthMod] /= 10;
            }

            timeTaken = (double)(clock() - start)/CLOCKS_PER_SEC;
            printf("%d, %.8f \n", sizes[s] / 1024, timeTaken);
    }

    return 0;
}

我的机器中程序的输出如下。如何解释数字?这个程序告诉我什么?

1, 1.07000000 
4, 1.04000000 
8, 1.06000000 
16, 1.13000000 
32, 1.14000000 
64, 1.17000000 
128, 1.20000000 
256, 1.21000000 
512, 1.19000000 
1024, 1.23000000 
1536, 1.23000000 
2048, 1.46000000 
2560, 1.21000000 
3072, 1.45000000 
3584, 1.47000000 
4096, 1.94000000 

【问题讨论】:

  • 不是缓存专家,但它似乎可以处理越来越大的数据块,同时保持时间。因此,您“应该”能够通过查看时间的波动来猜测您的缓存有多大。我建议你在 excel 中绘制它们,因为它会给你一个更好的画面。
  • 它告诉我发生了一些奇怪的事情。处理该循环的 1024 次迭代不应花费超过 1 秒的时间!
  • 您的代码中有几个错误,主要是因为您一直访问相同的地址而不是扫描您的数据集。请参阅下面的答案

标签: c performance caching cpu-cache


【解决方案1】:
  1. 您需要直接访问内存

    我的意思不是 DMA 传输。当然,CPU 必须访问内存(否则您不会测量 CACHE),但要尽可能直接地访问...所以测量可能不会很准确Windows/Linux 因为服务和其他进程可能会在运行时弄乱缓存。多次测量并取平均值以获得更好的结果(或使用最快的时间或一起过滤)。为了获得最佳准确性,请使用 DOSasm 例如

    rep + movsb,movsw,movsd 
    rep + stosb,stosw,stosd
    

    所以你测量的是内存传输,而不是你的代码中的其他东西!!!

  2. 测量原始传输时间并绘制图表

    • x 轴是传输块大小
    • y轴为传输速度

    传输速率相同的区域与相应的CACHE层一致

[Edit1] 找不到我的旧源代码,所以我现在在 C++ 中为 windows 破坏了一些东西:

时间测量:

//---------------------------------------------------------------------------
double performance_Tms=-1.0,    // perioda citaca [ms]
       performance_tms= 0.0;    // zmerany cas [ms]
//---------------------------------------------------------------------------
void tbeg()
    {
    LARGE_INTEGER i;
    if (performance_Tms<=0.0) { QueryPerformanceFrequency(&i); performance_Tms=1000.0/double(i.QuadPart); }
    QueryPerformanceCounter(&i); performance_tms=double(i.QuadPart);
    }
//---------------------------------------------------------------------------
double tend()
    {
    LARGE_INTEGER i;
    QueryPerformanceCounter(&i); performance_tms=double(i.QuadPart)-performance_tms; performance_tms*=performance_Tms;
    return performance_tms;
    }
//---------------------------------------------------------------------------

基准测试(32 位应用):

//---------------------------------------------------------------------------
DWORD sizes[]=                  // used transfer block sizes
    {
      1<<10,  2<<10,  3<<10,  4<<10,  5<<10,  6<<10,  7<<10,  8<<10,  9<<10,
     10<<10, 11<<10, 12<<10, 13<<10, 14<<10, 15<<10, 16<<10, 17<<10, 18<<10,
     19<<10, 20<<10, 21<<10, 22<<10, 23<<10, 24<<10, 25<<10, 26<<10, 27<<10,
     28<<10, 29<<10, 30<<10, 31<<10, 32<<10, 48<<10, 64<<10, 80<<10, 96<<10,
    112<<10,128<<10,192<<10,256<<10,320<<10,384<<10,448<<10,512<<10,  1<<20,
      2<<20,  3<<20,  4<<20,  5<<20,  6<<20,  7<<20,  8<<20,  9<<20, 10<<20,
     11<<20, 12<<20, 13<<20, 14<<20, 15<<20, 16<<20, 17<<20, 18<<20, 19<<20,
     20<<20, 21<<20, 22<<20, 23<<20, 24<<20, 25<<20, 26<<20, 27<<20, 28<<20,
     29<<20, 30<<20, 31<<20, 32<<20,
    };
const int N=sizeof(sizes)>>2;   // number of used sizes
double pmovsd[N];               // measured transfer rate rep MOVSD [MB/sec]
double pstosd[N];               // measured transfer rate rep STOSD [MB/sec]
//---------------------------------------------------------------------------
void measure()
    {
    int i;
    BYTE *dat;                              // pointer to used memory
    DWORD adr,siz,num;                      // local variables for asm
    double t,t0;
    HANDLE hnd;                             // process handle

    // enable priority change (huge difference)
    #define measure_priority

    // enable critical sections (no difference)
//  #define measure_lock

    for (i=0;i<N;i++) pmovsd[i]=0.0;
    for (i=0;i<N;i++) pstosd[i]=0.0;
    dat=new BYTE[sizes[N-1]+4];             // last DWORD +4 Bytes (should be 3 but i like 4 more)
    if (dat==NULL) return;
    #ifdef measure_priority
    hnd=GetCurrentProcess(); if (hnd!=NULL) { SetPriorityClass(hnd,REALTIME_PRIORITY_CLASS); CloseHandle(hnd); }
    Sleep(200);                             // wait to change take effect
    #endif
    #ifdef measure_lock
    CRITICAL_SECTION lock;                  // lock handle
    InitializeCriticalSectionAndSpinCount(&lock,0x00000400);
    EnterCriticalSection(&lock);
    #endif
    adr=(DWORD)(dat);
    for (i=0;i<N;i++)
        {
        siz=sizes[i];                       // siz = actual block size
        num=(8<<20)/siz;                    // compute n (times to repeat the measurement)
        if (num<4) num=4;
        siz>>=2;                            // size / 4 because of 32bit transfer
        // measure overhead
        tbeg();                             // start time meassurement
        asm {
            push esi
            push edi
            push ecx
            push ebx
            push eax
            mov ebx,num
            mov al,0
    loop0:  mov esi,adr
            mov edi,adr
            mov ecx,siz
//          rep movsd                       // es,ds already set by C++
//          rep stosd                       // es already set by C++
            dec ebx
            jnz loop0
            pop eax
            pop ebx
            pop ecx
            pop edi
            pop esi
            }
        t0=tend();                          // stop time meassurement
        // measurement 1
        tbeg();                             // start time meassurement
        asm {
            push esi
            push edi
            push ecx
            push ebx
            push eax
            mov ebx,num
            mov al,0
    loop1:  mov esi,adr
            mov edi,adr
            mov ecx,siz
            rep movsd                       // es,ds already set by C++
//          rep stosd                       // es already set by C++
            dec ebx
            jnz loop1
            pop eax
            pop ebx
            pop ecx
            pop edi
            pop esi
            }
        t=tend();                           // stop time meassurement
        t-=t0; if (t<1e-6) t=1e-6;          // remove overhead and avoid division by zero
        t=double(siz<<2)*double(num)/t;     // Byte/ms
        pmovsd[i]=t/(1.024*1024.0);         // MByte/s
        // measurement 2
        tbeg();                             // start time meassurement
        asm {
            push esi
            push edi
            push ecx
            push ebx
            push eax
            mov ebx,num
            mov al,0
    loop2:  mov esi,adr
            mov edi,adr
            mov ecx,siz
//          rep movsd                       // es,ds already set by C++
            rep stosd                       // es already set by C++
            dec ebx
            jnz loop2
            pop eax
            pop ebx
            pop ecx
            pop edi
            pop esi
            }
        t=tend();                           // stop time meassurement
        t-=t0; if (t<1e-6) t=1e-6;          // remove overhead and avoid division by zero
        t=double(siz<<2)*double(num)/t;     // Byte/ms
        pstosd[i]=t/(1.024*1024.0);         // MByte/s
        }
    #ifdef measure_lock
    LeaveCriticalSection(&lock);
    DeleteCriticalSection(&lock);
    #endif
    #ifdef measure_priority
    hnd=GetCurrentProcess(); if (hnd!=NULL) { SetPriorityClass(hnd,NORMAL_PRIORITY_CLASS); CloseHandle(hnd); }
    #endif
    delete dat;
    }
//---------------------------------------------------------------------------

数组pmovsd[]pstosd[] 保存测量的32bit 传输速率[MByte/sec]。你可以在 measure 函数开始时通过 use/rem 两个定义来配置代码。

图形输出:

为了最大限度地提高准确性,您可以将进程优先级更改为最大值。因此,创建具有最大优先级的测量线程(我尝试过,但实际上它搞砸了)并将 critical section 添加到它,这样测试就不会经常被 OS 中断(有和没有螺纹没有明显的区别)。如果您想使用Byte 传输,请考虑它仅使用16bit 寄存器,因此您需要添加循环和地址迭代。

附言。

如果您在笔记本电脑上尝试此操作,那么您应该使 CPU 过热,以确保您测量到最高 CPU/Mem 速度。所以没有Sleeps。测量之前的一些愚蠢的循环会这样做,但它们应该至少运行几秒钟。您也可以通过 CPU 频率测量和在上升时循环来同步它。饱和后停止...

asm 指令 RDTSC 最适合这个(但要注意它的含义随着新架构略有变化)。

如果您不在 Windows 下,请将函数 tbeg,tend 更改为您的 OS 等效项

[edit2] 进一步提高准确性

在最终解决了 VCL 影响测量精度的问题之后,我发现感谢这个问题以及更多关于它here 的问题,为了提高准确性,您可以在基准测试之前这样做:

  1. 将进程优先级设置为realtime

  2. 将进程关联设置为单个 CPU

    所以您只测量多核上的单个 CPU

  3. 刷新数据和指令缓存

例如:

    // before mem benchmark
    DWORD process_affinity_mask=0;
    DWORD system_affinity_mask =0;
    HANDLE hnd=GetCurrentProcess();
    if (hnd!=NULL)
        {
        // priority
        SetPriorityClass(hnd,REALTIME_PRIORITY_CLASS);
        // affinity
        GetProcessAffinityMask(hnd,&process_affinity_mask,&system_affinity_mask);
        process_affinity_mask=1;
        SetProcessAffinityMask(hnd,process_affinity_mask);
        GetProcessAffinityMask(hnd,&process_affinity_mask,&system_affinity_mask);
        }
    // flush CACHEs
    for (DWORD i=0;i<sizes[N-1];i+=7)
        {
        dat[i]+=i;
        dat[i]*=i;
        dat[i]&=i;
        }

    // after mem benchmark
    if (hnd!=NULL)
        {
        SetPriorityClass(hnd,NORMAL_PRIORITY_CLASS);
        SetProcessAffinityMask(hnd,system_affinity_mask);
        }

所以更准确的测量看起来像这样:

【讨论】:

  • 临界区并不意味着您的用户空间代码在禁用中断的情况下运行。这只意味着没有其他线程可以进入临界区。 IDK 如果 Windows 内核的调度程序为临界区内的进程提供任何类型的优先级提升,但这种影响必须受到限制,否则任何程序都可以在启动时进入临界区并具有比其他方式允许的更高的优先级请求它运行的整个时间。我不认为 Linux 专门为 futex 提供了优先级提升。
  • 您不需要自己在 inline asm 中推送/弹出寄存器。在 MSVC 风格中,编译器解析你的 asm 以查看它破坏了什么,并发出适当的周围代码。此外,将rep movsd 与重叠缓冲区一起使用很奇怪。我原以为您的 src=dst 案例会很慢。
  • L1D 是“一团糟”,因为您的 Bulldozer 系列 CPU 有一个直写式 L1D 缓存和一个 4kiB 写入组合缓冲区,所以一旦您的写入集更大比 4k,您主要是 L2 存储带宽的瓶颈。缓存 read 测试(例如每 64 个字节读取一个 dword)会发现预期的下降幅度约为 16kiB、realworldtech.com/bulldozer/9stackoverflow.com/a/34143603/224132。 Ryzen 回到了正常的回写式 L1D 设计; Bulldozer L1D 是一个错误。 (我可以判断它是 16k/4-way L1D、64k/2-way L1I 和 2M L2 的 Bulldozer 系列。当然不是 Intel)。
  • @PeterCordes 你的猜测是正确的,它是一个 AMD :) 不确定当时可能是哪个 x3 内核......顺便说一句,它不是 MSVC 编译器,而是 Borland,它有完全不同的 asm {} 行为尤其是在性能方面......但push/pops 主要是为了让我放松
  • @PeterCordes 顺便说一句,我最近移植了这个来测量 HDD .... HDD access + search time calculation algorithm based on read/write speed and HDD buffer size
【解决方案2】:

您的lengthMod 变量并没有按照您的想法执行。您希望它限制数据集的大小,但您有两个问题 -

  • 使用 2 的幂进行按位与运算将屏蔽除打开的位之外的所有位。如果例如lengthMod 是 1k (0x400),然后所有低于 0x400(意味着 i=1 到 63)的索引将简单地映射到索引 0,因此您将始终命中缓存。这可能就是结果如此之快的原因。而是使用lengthMod - 1 创建一个正确的掩码(0x400 --> 0x3ff,这将只掩码高位而保持低位不变)。
  • lengthMod 的某些值不是 2 的幂,因此在此执行 lengthMod-1 将不起作用,因为某些掩码位仍然为零。要么从列表中删除它们,要么完全使用模运算而不是 lengthMod-1。有关类似情况,另请参阅我的回答 here

另一个问题是 16B 的跳转可能不足以跳过一个缓存线,因为大多数常见的 CPU 使用 64 字节缓存线,所以每 4 次迭代只有一次未命中。请改用(i*64)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-26
    • 2021-10-25
    • 2015-05-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多