您的系统上缓存大小的估算?

10

我从这个链接 (https://gist.github.com/jiewmeng/3787223) 获取了这个程序。我一直在网上搜索,想更好地理解处理器缓存(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中绘制这些图表,因为这将给您提供更好的图片。 - Inisheer
它告诉我发生了一些奇怪的事情。处理那个循环的1024次迭代不应该超过1秒钟! - Oliver Charlesworth
你的代码中有几个bug,主要是你一直访问相同的地址而没有遍历整个数据集。请看下面我的答案。 - Leeor
2个回答

10
  1. you need direct access to memory

    I am not meaning DMA transfer by this. Memory must be accessed by CPU of course (otherwise you are not measuring CACHEs) but as directly as it can be ... so measurements will probably not be very accurate on Windows/Linux because services and other processes can mess with caches during runtime. Measure many times and average for better results (or use the fastest time or filter it together). For best accuracy use DOS and asm for example

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

    so you measure the memory transfer and not something else like in your code !!!

  2. measure the raw transfer times and plot a graph

    • x axis is transfer block size
    • y axis is transfer speed

    graph

    zones with the same transfer rate are consistent with appropriate CACHE layer

[编辑1]我没找到旧的源代码,所以现在我用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[]数组保存了测得的32位传输速率[MB/s]。您可以通过在测量函数开头使用/取消使用两个宏定义来配置代码。

图形输出:

memory benchmark measured data

为了最大化准确性,您可以将进程优先级类别设置为最高。因此创建具有最大优先级的测量线程(我尝试过,但实际上会出现混乱),并向其添加关键段,以便测试不会受操作系统的干扰(有无线程没有明显差异)。如果要使用Byte传输,则需要考虑它仅使用16位寄存器,因此需要添加循环和地址迭代。

PS。

如果您在笔记本电脑上尝试此操作,则应过热CPU以确保您在CPU/Mem速度方面进行测量。所以不要使用Sleep。一些愚蠢的循环在测量之前就可以完成,但它们应该运行至少几秒钟。此外,您可以通过CPU频率测量同步此操作,并在其上升时循环。停止后它饱和...

asm指令RDTSC最适合此操作(但请注意,随着新架构的到来,其意义已略有改变)。

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

[edit2]提高准确性的进一步改进

好吧,在终于解决了影响测量精度的VCL问题之后(我通过这个问题发现了更多信息,详情请点击此处),为了提高准确性,您可以在进行基准测试之前执行以下操作:

  1. 将进程优先级类别设置为实时

  2. 将进程亲和力设置为单CPU

    这样您就可以在多核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);
        }

所以更准确的测量结果如下所示:

更准确的输出


1
一个关键区域并不意味着您的用户空间代码在中断禁用时运行。它只意味着没有其他线程可以进入关键区域。我不知道Windows内核的调度程序是否会为处于关键区域内的进程提供任何类型的优先级提升,但该效果必须受到限制,否则任何程序都可以在启动时进入关键区域,并具有比其允许请求的更高优先级整个运行时间。我不认为Linux专门为futex提供优先级提升。 - Peter Cordes
在内联汇编中,您不需要自己推入/弹出寄存器。在MSVC风格中,编译器会解析您的汇编代码以查看它破坏了什么,并发出适当的周围代码。此外,使用rep movsd与重叠缓冲区很奇怪。我本来期望您的src = dst情况很慢。 - Peter Cordes
1
L1D是“一团糟”,因为您的Bulldozer系列CPU具有带有4kiB写组合缓冲区的写通L1D高速缓存,因此一旦您的写入集大于4k,您主要会受到L2存储器带宽的限制。缓存读取测试(例如每64个字节读取一个dword)应在大约16kiB处发现预期的下降,https://www.realworldtech.com/bulldozer/9/,https://dev59.com/0WnWa4cB1Zd3GeqPzVNY#34143603。Ryzen回到了正常的写回L1D设计; Bulldozer L1D是一个错误。(我可以从16k / 4路L1D,64k / 2路L1I和2M L2中看出这是Bulldozer系列)。 - Peter Cordes
@PeterCordes,你的猜测是正确的,它是一台AMD :) 不确定是哪种,可能是当时的某个x3核心... 顺便说一下,它不是MSVC编译器,而是Borland,其asm {}行为完全不同,特别是在性能方面... 但push/pops主要是为了让我放心。 - Spektre
@PeterCordes 顺便说一下,我最近将这个移植到了测量硬盘驱动器的程序中.... 基于读/写速度和硬盘缓存大小的HDD访问+搜索时间计算算法 - Spektre

4
您的lengthMod变量并没有达到您期望的效果。您希望它限制数据集的大小,但有两个问题 -
  • 使用2的幂进行按位与运算将掩码除了一个为1以外的所有位。例如,如果lengthMod是1k (0x400),那么所有小于0x400 (也就是i=1到63)的索引都会简单地映射到索引0,因此您总是命中缓存。这可能就是结果非常快的原因。相反,使用lengthMod - 1来创建正确的掩码(0x400 --> 0x3ff,这将只掩盖上位比特并保留低位不变)。
  • 一些lengthMod的值不是2的幂,因此对lengthMod-1的操作不会奏效,因为一些掩码位仍然为零。要么将它们从列表中删除,要么完全使用模操作代替lengthMod-1。参见我在这里的类似情况的回答。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接