Тест триады Шёнауэра — кэш L1D не виден

Мы, два студента HPC, участвуем в знаменитом тесте Triad Schönauer Triad Benchmark, код C которого приведен здесь вместе с его кратким объяснением:

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

#include <sys/time.h>

#define DEFAULT_NMAX 10000000
#define DEFAULT_NR DEFAULT_NMAX
#define DEFAULT_INC 10
#define DEFAULT_XIDX 0

#define MAX_PATH_LENGTH 1024

// #define WINOS
#define STACKALLOC

#ifdef WINOS 
    #include <windows.h>
#endif

static void dummy(double A[], double B[], double C[], double D[])
{
    return;
}

static double simulation(int N, int R)
{
    int i, j;

    #ifdef STACKALLOC
        double A[N];
        double B[N];
        double C[N];
        double D[N];
    #else
        double * A = malloc(N*sizeof(double));
        double * B = malloc(N*sizeof(double));
        double * C = malloc(N*sizeof(double));
        double * D = malloc(N*sizeof(double));
    #endif

    double elaps;

    for (i = 0; i < N; ++i)
    {
        A[i] = 0.00;
        B[i] = 1.00;
        C[i] = 2.00;
        D[i] = 3.00;
    }

    #ifdef WINOS
        FILETIME tp;
        GetSystemTimePreciseAsFileTime(&tp);
        elaps = - (double)(((ULONGLONG)tp.dwHighDateTime << 32) | (ULONGLONG)tp.dwLowDateTime)/10000000.0;
    #else
        struct timeval tp;
        gettimeofday(&tp, NULL);
        elaps = -(double)(tp.tv_sec + tp.tv_usec/1000000.0);
    #endif

    for(j=0; j<R; ++j)
    {
        for(i=0; i<N; ++i)
            A[i] = B[i] + C[i]*D[i];

        if(A[2] < 0) dummy(A, B, C, D);
    }

    #ifndef STACKALLOC
        free(A);
        free(B); 
        free(C);
        free(D);
    #endif

    #ifdef WINOS
        GetSystemTimePreciseAsFileTime(&tp);
        return elaps + (double)(((ULONGLONG)tp.dwHighDateTime << 32) | (ULONGLONG)tp.dwLowDateTime)/10000000.0;
    #else
        gettimeofday(&tp, NULL);
        return elaps + ((double)(tp.tv_sec + tp.tv_usec/1000000.0));
    #endif
}

int main(int argc, char *argv[])
{
    const int NR = argc > 1 ? atoi(argv[1]) : DEFAULT_NR;
    const int NMAX = argc > 2 ? atoi(argv[2]) : DEFAULT_NMAX;
    const int inc = argc > 3 ? atoi(argv[3]) : DEFAULT_INC;
    const int xidx = argc > 4 ? atoi(argv[4]) : DEFAULT_XIDX;

    int i, j, k;
    FILE * fp;

    printf("\n*** Schonauer Triad benchmark ***\n");

    char csvname[MAX_PATH_LENGTH];
    sprintf(csvname, "data%d.csv", xidx);

    if(!(fp = fopen(csvname, "a+")))
    {
        printf("\nError whilst writing to file\n");
        return 1;
    }

    int R, N;
    double MFLOPS;
    double elaps;

    for(N=1; N<=NMAX; N += inc)
    {
        R = NR/N;
        elaps = simulation(N, R);
        MFLOPS = ((R*N)<<1)/(elaps*1000000);
        fprintf(fp, "%d,%lf\n", N, MFLOPS);
        printf("N = %d, R = %d\n", N, R);
        printf("Elapsed time: %lf\n", elaps);
        printf("MFLOPS: %lf\n", MFLOPS);
    }

    fclose(fp);
    (void) getchar();
    return 0;
}

Код просто перебирает N и для каждого N выполняет NR операции с плавающей запятой, где NR — константа, обозначающая количество постоянные операции, выполняемые на каждой самой внешней итерации, чтобы проводить точные измерения времени даже для слишком коротких значений N. Ядром для анализа, очевидно, является подпрограмма simulation.

У нас есть странные результаты:

Мы начали с тестирования ядра на сервере E4 E9220 2U, состоящем из 8 узлов, каждый из которых оснащен двухпроцессорным Intel Xeon E5-2697 V2 (Ivy Bridge) @ 2,7. ГГц, 12 ядер. Код был скомпилирован с помощью gcc (GCC) 4.8.2 и запущен в Linux CentOS версии 6. Ниже перечислены полученные графики в одном изображении:

Графики N и MFlops: -Ofast (вверху) и -Ofast вдоль -march=native (внизу)< /а>

Легко видеть, что спад L2 и L3 довольно заметен, и они численно в порядке, выполнив некоторые простые расчеты и приняв во внимание проблемы мультипрограммирования, а также тот факт, что L2-L3 являются УНИФИЦИРОВАННЫМИ, а L3 также ОБЩАЕТСЯ между всеми 12 ядрами. На первом графике L1 не виден, а на втором он виден и начинается со значения N, поэтому результирующее значение насыщения L1D составляет ровно 32 КБ в соответствии с размером L1D на ядро. Первый вопрос: почему мы не видим спад L1 без флага специализации архитектуры -march=native?

После некоторых каверзных (явно неверных) самообъяснений мы решили провести тест на Lenovo Z500, оснащенном одним сокетом Intel Core i7-3632QM (Ivy Bridge) @ 2,2 ГГц< /эм>. На этот раз мы использовали gcc (Ubuntu 6.3.0-12ubuntu2) 6.3.0 20170406 (из gcc --version), и полученные графики перечислены ниже:

Графики N и MFlops: -Ofast (вверху) и -Ofast вдоль -march=native (внизу)< /а>

Второй вопрос возникает несколько спонтанно: почему мы видим L1D под гору без -march=native- на этот раз?


person Marco Chiarelli    schedule 28.05.2017    source источник
comment
Существуют аппаратные предварительные выборки для разных уровней кэшей в Intel и AMD. Они будут обнаруживать линейный доступ и выполнять предварительную выборку данных для следующих итераций (их можно отключить software.intel.com/en-us/articles/). Прочтите сообщения Маккалпина, Джона (доктор Бэндвид), автора современной триады STREAM cs.virginia.edu/ поток, cs.virginia.edu/~mccalpin. Также проверьте настройки malloc, NUMA и настройки THP (hugepage) на больших машинах. И сравните реальный ассемблерный код между -Ofast/-Ofast -march=native.   -  person osgx    schedule 28.05.2017
comment
Вы определенно правы. Самое странное, что мы пробовали и другие машины разных семейств Intel, и мы скомпилировали с версиями GCC до 4.8.5. В результате спуск L1D по-прежнему отсутствует при компиляции только с установленным флагом -Ofast. По-видимому, компиляция с помощью GCC 6.3.0 дает ожидаемый результат.   -  person Marco Chiarelli    schedule 28.05.2017
comment
Марко, опубликуйте точную разборку внутреннего цикла как из -Ofast, так и из -Ofast -march=native с версиями GCC до 4.8.5 (и, возможно, gcc 6.3.0). (с perf record ./program потом perf report и в сборе или perf annotate). Мы можем говорить о сборке и компиляторе, а не о невоспроизводимых картинках. Некоторые программы, вероятно, не имеют правильной векторизации, например AVX/AVX2. Что такое N на картинке для точки L1? Эта триада - не лучший вариант STREAM, в ней нет прогревочных итераций - есть лишние pagefaults для выделения стека; выполните 1 прогон симуляции NMAX перед тестом.   -  person osgx    schedule 28.05.2017
comment
Хорошо, osgx, спасибо за простую инструкцию по дизассемблированию. Я разместил на pastebin вывод записи о производительности и аннотации о производительности, все восемь комбинаций: вывод отчета о производительности -Ofast с GCC 4.8 .5 @ Pastebin результат perf annotate -Ofast с GCC 4.8.5 @ Pastebin вывод отчета о производительности -Ofast + -march=native с GCC 4.8.5 вывод производительной аннотации -Ofast + -march=native с GCC 4.8.5   -  person Marco Chiarelli    schedule 29.05.2017
comment
Извините за двойное сообщение, вот вывод отчета о производительности -Ofast с GCC 6.3.0 @ Pastebin результат perf annotate -Ofast с GCC 6.3.0 @ Pastebin вывод отчета о производительности -Ofast + -march=native с GCC 6.3.0 вывод perf annotate -Ofast + -march=native с GCC 6.3.0 Единственное, что я могу сказать, это то, что с -Ofast и -march=native, как в двух версиях исполняемых файлов (GCC 4.8.5 и 6.3. 0 — скомпилировано), есть векторные инструкции (с префиксом 'v' в сборке)   -  person Marco Chiarelli    schedule 29.05.2017


Ответы (1)


Есть фрагменты сборки внутреннего цикла "TRIAD" (A[i] = B[i] + C[i]*D[i]: за i итерации 2 флопа double_precision, 3 чтения double, 1 запись double).

Точные проценты от perf annotate были не очень полезны, так как вы профилировали все регионы с разной производительностью за один прогон. И длинный отчет о производительности вообще не нужен, обычно нужны только первые 5-10 строк после #. Вы можете попытаться ограничить тест интересующей областью 4 * N * sizeof (double) ‹ sizeof (L1d_cache) и вспомнить аннотацию производительности, а также получить результаты perf stat ./program и perf stat -d ./program (а также узнать о специфичной для Intel обертке производительности ocperf.py - https://github.com/andikleen/pmu-tools и другие инструменты).

От gcc-6.3.0 -Ofast – 128-битные (2 двойных) регистры XMM и SSE2 movupd/movups используются (SSE2 является FPU по умолчанию для процессора x86_64), 2 итерации i для каждого цикла ассемблера ( movupd загружает 2 дубля из памяти)

         :                              A[i] = B[i] + C[i]*D[i];
    0.03 :        d70:       movupd (%r11,%rax,1),%xmm1    # load C[i:i+1] into xmm1
   14.87 :        d76:       add    $0x1,%ecx              # advance 'i/2' loop counter by 1
    0.10 :        d79:       movupd (%r10,%rax,1),%xmm0    # load D[i:i+1] into xmm0
   14.59 :        d7f:       mulpd  %xmm1,%xmm0            # multiply them into xmm0
    2.78 :        d83:       addpd  (%r14,%rax,1),%xmm0    # load B[i:i+1] and add to xmm0
   17.69 :        d89:       movups %xmm0,(%rsi,%rax,1)    # store into A[i:i+1]
    2.71 :        d8d:       add    $0x10,%rax             # advance array pointer by 2 doubles (0x10=16=2*8)
    1.68 :        d91:       cmp    %edi,%ecx              # check for end of loop (edi is N/2)
    0.00 :        d93:       jb     d70 <main+0x4c0>       # if not, jump to 0xd70

Из gcc-6.3.0 -Ofast -march=native: vmovupd — это не просто вектор (SSE2 somethingpd тоже является вектором), они Инструкции AVX, которые могут использовать 2-кратные регистры YMM (256 бит, 4 двойных значения на регистр). Цикл длиннее, но за одну итерацию цикла обрабатывается 4 i итераций.

    0.02 :        db6:       vmovupd (%r10,%rdx,1),%xmm0   # load C[i:i+1] into xmm0 (low part of ymm0)
    8.42 :        dbc:       vinsertf128 $0x1,0x10(%r10,%rdx,1),%ymm0,%ymm1  # load C[i+2:i+3] into high part of ymm1 and copy xmm0 into lower part; ymm1 is C[i:i+3]
    7.37 :        dc4:       add    $0x1,%esi              # loop counter ++
    0.06 :        dc7:       vmovupd (%r9,%rdx,1),%xmm0    # load D[i:i+1] -> xmm0
   15.05 :        dcd:       vinsertf128 $0x1,0x10(%r9,%rdx,1),%ymm0,%ymm0  # load D[i+2:i+3] and get D[i:i+3] in ymm0
    0.85 :        dd5:       vmulpd %ymm0,%ymm1,%ymm0      # mul C[i:i+3] and D[i:i+3] into ymm0
    1.65 :        dd9:       vaddpd (%r11,%rdx,1),%ymm0,%ymm0  # soad 4 doubles of B[i:i+3] and add to ymm0
   21.18 :        ddf:       vmovups %xmm0,(%r8,%rdx,1)    # store low 2 doubles to A[i:i+1]
    1.24 :        de5:       vextractf128 $0x1,%ymm0,0x10(%r8,%rdx,1)  # store high 2 doubles to A[i+2:i+3]
    2.04 :        ded:       add    $0x20,%rdx             # advance array pointer by 4 doubles
    0.02 :        df1:       cmp    -0x460(%rbp),%esi      # loop cmp
    0.00 :        df7:       jb     db6 <main+0x506>       # loop jump to 0xdb6

Код с включенным AVX (с -march=native) лучше, так как он лучше использует развертывание, но использует узкую загрузку из 2 двойников. При большем количестве реальных тестов массивы будут лучше выровнены, и компилятор сможет выбрать самый широкий 256-битный vmovupd в ymm, без необходимости инструкций вставки/извлечения.

Код, который у вас есть сейчас, вероятно, может быть настолько медленным, что он не может полностью загрузить (насытить) интерфейс к кешу данных L1< /strong> в большинстве случаев с короткими массивами. Другая возможность — плохое выравнивание между массивами.

У вас короткий всплеск высокой пропускной способности на нижнем графике в https://i.stack.imgur.com/2ovxm.png - 6 "GFLOPS" и это странно. Выполните расчет, чтобы преобразовать это в Гбайт/с, и найдите пропускную способность L1d Ivy Bridge и ограничения частоты проблем с нагрузкой... что-то вроде https://software.intel.com/en-us/forums/программнаянастройка-производительность-оптимизация-платформы-мониторинг/тема/532346 "Ядро Haswell может выполнять только две загрузки за цикл, поэтому они должны быть 256-битными загрузками AVX, чтобы иметь шанс на достижение скорости 64 байта/цикл. (слово эксперта по TRIAD и автора STREAM, John D. McCalpin, PhD "Dr. Bandwidth", сделайте поиск по его сообщениям) и http://www.overclock.net/t/1541624/сколько-пропускнаяспособность-находится-в-кеше-процессора-и-как-это-рассчитывается "Пропускная способность L1 зависит от инструкций на такт и шага инструкций (AVX = 256-бит, SSE = 128-бит и т. д.). IIRC, у Sandy Bridge 1 инструкция за тик"

person osgx    schedule 29.05.2017
comment
Проверьте часть изображения Sandy Bridge (Ivy имеет такие же параметры): extremetech .com/wp-content/uploads/2013/06/cachebw.jpg и bdti.com/sites/default/files/insidesp/articlepix/201205/ — загрузка L1 до 32 байт/цикл; BW хранилища L1 составляет до 16 байт/цикл. Также пересчитайте результаты теста из ГБ/с в байт/тик (получите реальную частоту из вывода perf stat ./program). 32B/s ld составляют 1 vmovupd ymm (4 удвоения) за тик, 3 цикла требуется для чтения B, C, D с 4-кратным развертыванием/векторизацией; 16B/c st — это 1 vmovups xmm, поэтому 2 цикла для A[i:i+3] store. Mul/add может перекрываться - person osgx; 29.05.2017