Оценка размера кеша в вашей системе?

Я получил эту программу из этой ссылки (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 

Ответ 1

  • вам нужен прямой доступ к памяти

    Я не имею в виду DMA. Разумеется, доступ к памяти должен выполняться с помощью ЦП (в противном случае вы не измеряете CACHE), но так же, как это может быть... поэтому измерения, вероятно, будут не очень точными на Windows/Linux, поскольку службы и другие процессы могут работать с кэшами во время выполнения. Измерьте много раз и среднее значение для получения лучших результатов (или используйте самое быстрое время или фильтруйте его вместе). Для лучшей точности используйте DOS и asm, например

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

    , чтобы вы измеряли передачу памяти, а не что-то еще, как в вашем коде!!!

  • измерять время сырой передачи и строить график

    • x ось - размер блока переноса.
    • y ось - скорость передачи.

    graph

    с той же скоростью передачи согласуются с соответствующим слоем CACHE

[Edit1] не смог найти мой старый исходный код для этого, поэтому я сейчас что-то потерял в С++ для 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;
    }
//---------------------------------------------------------------------------

Benchmark (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]. Вы можете настроить код с помощью функции /rem two в начале функции измерения.

Графический выход:

memory benchmark measured data

Чтобы максимизировать точность, вы можете максимально изменить класс приоритета процесса. Итак, создайте поток измерения с максимальным приоритетом (я попробую, но на самом деле все испортит) и добавьте к нему критический раздел, чтобы тест не прерывался OS так часто ( без видимых различий с потоками и без них). Если вы хотите использовать передачи Byte, тогда учтите, что он использует только регистры 16bit, поэтому вам нужно добавить итерации циклов и адресов.

PS.

Если вы попробуете это на ноутбуке, вы должны перегреться ЦП, чтобы убедиться, что вы измеряете максимальную скорость CPU/Mem. Поэтому no Sleep s. Некоторые глупые петли перед измерением будут делать это, но они должны работать как минимум несколько секунд. Также вы можете синхронизировать это с помощью измерения ЦП, а цикл увеличивается. Остановитесь после того, как он насытится...

asm инструкция RDTSC лучше для этого (но будьте осторожны, ее значение немного изменилось с новыми архитектурами).

Если вы не находитесь под Windows, измените функции tbeg,tend на ОС эквиваленты

[edit2] дальнейшее улучшение точности

Ну, наконец, после решения проблемы с VCL, влияющей на точность измерения, которую я обнаружил благодаря этому вопросу и более об этом здесь, чтобы повысить точность вы можете до этого сделать следующее:

  • установить класс приоритета процесса на realtime

  • установить близость процесса к одиночному процессору

    поэтому вы измеряете только один CPU на многоядерных

  • скрытые DATA и инструкции CACHE

Например:

    // 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);
        }

Таким образом, более точное измерение выглядит следующим образом:

more accurate output

Ответ 2

Ваша переменная lengthMod не делает то, что вы думаете. Вы хотите, чтобы он ограничивал размер вашего набора данных, но у вас есть 2 проблемы -

  • Выполнение побитового И с мощностью 2 замаскирует все биты, кроме того, что включено. Если, например, lengthMod равно 1k (0x400), тогда все индексы ниже 0x400 (то есть я = от 1 до 63) будут просто сопоставляться с индексом 0, поэтому вы всегда будете попадать в кеш. Вероятно, поэтому результаты настолько быстры. Вместо этого используйте lengthMod - 1 для создания правильной маски (0x400 → 0x3ff, которая будет маскировать только верхние биты и оставить нижние не повреждены).
  • Некоторые значения для lengthMod не являются степенью 2, поэтому lengthMod-1 не будет работать там, так как некоторые из бит маски все равно будут нулями. Либо удалите их из списка, либо выполните операцию modulo вместо lengthMod-1 в целом. См. Также мой ответ здесь для аналогичного случая.

Другая проблема заключается в том, что переходов 16B, вероятно, недостаточно, чтобы пропустить cachline, поскольку большинство распространенных процессоров работают с кеш-байтами на 64 байта, поэтому вы получаете только одну пропущенную ошибку на каждые 4 итерации. Вместо этого используйте (i*64).