Бенчмаркинг памяти в одном кадре - PullRequest
4 голосов
/ 12 февраля 2020

Whiskey Lake i7-8565U

Я пытаюсь научиться писать эталонные тесты одним выстрелом руками (без использования тестовых платформ) на примере процедуры копирования памяти с регулярными и не временными операциями записи в память WB. и хотел бы попросить какой-то обзор.


Декларация:

void *avx_memcpy_forward_llss(void *restrict, const void *restrict, size_t);

void *avx_nt_memcpy_forward_llss(void *restrict, const void *restrict, size_t);

Определение:

avx_memcpy_forward_llss:
    shr rdx, 0x3
    xor rcx, rcx
avx_memcpy_forward_loop_llss:
    vmovdqa ymm0, [rsi + 8*rcx]
    vmovdqa ymm1, [rsi + 8*rcx + 0x20]
    vmovdqa [rdi + rcx*8], ymm0
    vmovdqa [rdi + rcx*8 + 0x20], ymm1
    add rcx, 0x08
    cmp rdx, rcx
    ja avx_memcpy_forward_loop_llss
    ret

avx_nt_memcpy_forward_llss:
    shr rdx, 0x3
    xor rcx, rcx
avx_nt_memcpy_forward_loop_llss:
    vmovdqa ymm0, [rsi + 8*rcx]
    vmovdqa ymm1, [rsi + 8*rcx + 0x20]
    vmovntdq [rdi + rcx*8], ymm0
    vmovntdq [rdi + rcx*8 + 0x20], ymm1
    add rcx, 0x08
    cmp rdx, rcx
    ja avx_nt_memcpy_forward_loop_llss
    ret

Код эталона:

#include <stdio.h>
#include <inttypes.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <immintrin.h>
#include <x86intrin.h>
#include "memcopy.h"

#define BUF_SIZE 128 * 1024 * 1024

_Alignas(64) char src[BUF_SIZE];
_Alignas(64) char dest[BUF_SIZE];

static inline void warmup(unsigned wa_iterations, void *(*copy_fn)(void *, const void *, size_t));
static inline void cache_flush(char *buf, size_t size);
static inline void generate_data(char *buf, size_t size);

uint64_t run_benchmark(unsigned wa_iteration, void *(*copy_fn)(void *, const void *, size_t)){
    generate_data(src, sizeof src);
    warmup(4, copy_fn); 
    cache_flush(src, sizeof src);
    cache_flush(dest, sizeof dest);
    __asm__ __volatile__("mov $0, %%rax\n cpuid":::"rax", "rbx", "rcx", "rdx", "memory"); 
    uint64_t cycles_start = __rdpmc((1 << 30) + 1); 
    copy_fn(dest, src, sizeof src); 
    __asm__ __volatile__("lfence" ::: "memory"); 
    uint64_t cycles_end = __rdpmc((1 << 30) + 1); 
    return cycles_end - cycles_start; 
}

int main(void){
    uint64_t single_shot_result = run_benchmark(1024, avx_memcpy_forward_llss);
    printf("Core clock cycles = %" PRIu64 "\n", single_shot_result);
}

static inline void warmup(unsigned wa_iterations, void *(*copy_fn)(void *, const void *, size_t)){
    while(wa_iterations --> 0){
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
        copy_fn(dest, src, sizeof src);
    }
}

static inline void generate_data(char *buf, size_t sz){
    int fd = open("/dev/urandom", O_RDONLY);
    read(fd, buf, sz);
}

static inline void cache_flush(char *buf, size_t sz){
    for(size_t i = 0; i < sz; i+=_SC_LEVEL1_DCACHE_LINESIZE){
        _mm_clflush(buf + i);
    }
}

Результаты :

avx_memcpy_forward_llss медиана : 44479368 циклов ядра

UPD: время

real    0m0,217s
user    0m0,093s
sys     0m0,124s

avx_nt_memcpy_forward_llss медиана : 24053086 циклов ядра

UPD: время

real    0m0,184s
user    0m0,056s
sys     0m0,128s

UPD: результат был получен при выполнении теста с taskset -c 1 ./bin

Таким образом, я получил почти почти в 2 раза разницу в основных циклах между реализация подпрограммы копирования памяти. Я интерпретирую это так, как в случае обычных хранилищ в памяти WB у нас есть запросы RFO, конкурирующие по пропускной способности шины, как это указано в IOM / 3.6.12 (подчеркните мой):

Хотя данные Пропускная способность полной записи 64-байтовой шины из-за невременных хранилищ в два раза превышает пропускную способность записи шины в WB-память , при передаче 8-байтовых кусков происходит потеря пропускной способности запроса шины и обеспечивается значительно меньшая пропускная способность данных.

ВОПРОС 1: Как провести анализ производительности в случае одиночного выстрела? Счетчики производительности не кажутся полезными из-за накладных расходов при запуске перфорирования и итераций при прогреве.

ВОПРОС 2: Является ли такой тест верным. Сначала я учел cpuid, чтобы начать измерения с чистыми ресурсами ЦП, чтобы избежать сбоев из-за предыдущих инструкций в полете. Я добавил клобберы памяти как барьер компиляции и lfence, чтобы избежать выполнения rdpmc OoO.

1 Ответ

8 голосов
/ 12 февраля 2020

Когда это возможно, тесты должны сообщать о результатах таким образом, чтобы обеспечить как можно большую «проверку работоспособности». В этом случае несколько способов включить такие проверки включают в себя:

  1. Для тестов, связанных с пропускной способностью основной памяти, результаты должны быть представлены в единицах, которые позволяют прямое сравнение с известной пиковой пропускной способностью DRAM системы. Для типичной конфигурации Core i7-8565U это 2 канала * 8 байт / передача * 2,4 миллиарда передач / с c = 38,4 ГБ / с (см. Также пункт (6) ниже.)
  2. Для тестов, которые предполагают передачу данных в любую точку иерархии памяти, результаты должны включать четкое описание размера «объема памяти» (количество обращений к разным адресам строк кэша, умноженное на размер строки кэша) и количество повторений передача (и). Ваш код легко читается здесь, и его размер вполне приемлем для теста основной памяти.
  3. Для любого временного теста следует включить абсолютное время, чтобы можно было сравнить его с вероятными накладными расходами времени. Использование вами только счетчика CORE_CYCLES_UNHALTED делает невозможным непосредственное вычисление истекшего времени (хотя тест, очевидно, достаточно длинный, чтобы издержки синхронизации были незначительными).

Другие важные принципы "передового опыта":

Любой тест, в котором используются инструкции RDPM C, должен быть привязан к одному логическому процессору. Результаты должны быть представлены таким образом, чтобы подтвердить читателю, что такое связывание было использовано. Распространенные способы принудительного применения такой привязки в Linux включают использование команд «taskset» или «numactl --physcpubind = [n]», или включение встроенного вызова «sched_setaffinity ()» с одним разрешенным логическим процессором, или установку Переменная окружения, которая заставляет библиотеку времени выполнения (например, OpenMP) связывать поток с одним логическим процессором. При использовании аппаратных счетчиков производительности требуется дополнительная осторожность, чтобы гарантировать, что все данные конфигурации для счетчиков доступно и описано правильно. Приведенный выше код использует RDPM C для чтения IA32_PERF_FIXED_CTR1 с именем события CPU_CLK_UNHALTED. Модификатор имени события зависит от программирования битов 7: 4 IA32_FIXED_CTR_CTRL (MSR 0x38d). Не существует общепринятого способа отображения всех возможных битов управления на модификаторы имени события, поэтому лучше предоставить полное содержимое IA32_FIXED_CTR_CTRL вместе с результатами. Событие счетчика производительности CPU_CLK_UNHALTED является правильным использовать для эталонных тестов частей процессора, поведение которых напрямую зависит от частоты ядра процессора, таких как выполнение команд и передача данных с использованием только кэшей L1 и L2. Пропускная способность памяти включает части процессора, производительность которых не масштабируется напрямую с частотой процессора. В частности, использование CPU_CLK_UNHALTED без принудительного выполнения операции с фиксированной частотой делает невозможным вычисление истекшего времени (требуемого согласно (1) и (3) выше). В вашем случае RDTSCP был бы проще, чем RDPM C - RDTS C не требует привязки процессов к одному логическому процессору, на него не влияют MSR других конфигураций и он позволяет напрямую вычислять истекшее время в секундах. Дополнительно: для тестов, связанных с передачей данных в иерархии памяти, полезно контролировать содержимое кэша и состояние (чистое или грязное) содержимого кэша, а также предоставлять явные описания «до» и «после» состояния вместе с результатами. Учитывая размеры ваших массивов, ваш код должен полностью заполнить все уровни кэша некоторой комбинацией частей массивов источника и назначения, а затем выполнить flu sh всех этих адресов, оставив иерархию кэша, которая (почти) полностью заполнено недопустимыми (чистыми) записями. Дополнительно: использование CPUID в качестве инструкции сериализации практически никогда не помогает при сравнительном тестировании. Хотя это гарантирует порядок, выполнение также занимает много времени - «Таблицы инструкций» Агнера Фога сообщают об этом за 100-250 циклов (предположительно в зависимости от входных аргументов). (Обновление: измерения через короткие интервалы всегда очень сложны. Инструкция CPUID имеет длительное и переменное время выполнения, и неясно, какое влияние микрокодированная реализация оказывает на внутреннее состояние процессора. Это может быть полезно в в определенных случаях c, но это не должно рассматриваться как нечто, автоматически включаемое в тесты. Для измерений через большие интервалы обработка по порядку вне границ измерения незначительна, поэтому CPUID не требуется.) Дополнительно: использование LFENCE в тестах применимо только в том случае, если вы измеряете с очень мелкой гранулярностью - менее нескольких сотен циклов. Дополнительные примечания по этой теме c на http://sites.utexas.edu/jdm4372/2018/07/23/comments-on-timing-short-code-sections-on-intel-processors/

Если я предполагаю, что ваш процессор работал на максимальной частоте турбо-частоты 4,6 ГГц во время теста, то количество циклов соответствует 9,67 миллисекундам и 5,23 миллисекундам соответственно. Включение их в «проверку работоспособности» показывает:

  • При условии, что в первом случае выполняется одно чтение, одно распределение и одна обратная запись (каждый 128 МБ), соответствующие скорости трафика DRAM c составляют 27,8 ГБ. / с + 13,9 ГБ / с = 41,6 ГБ / с == 108% от пика.
  • При условии, что во втором случае выполняется одно чтение и одно потоковое хранилище (каждое 128 МБ), соответствующие трафики DRAM c скорости 25,7 ГБ / с + 25,7 ГБ / с = 51,3 ГБ / с = 134% от пикового значения.

Ошибка этих «проверок работоспособности» говорит нам о том, что частота не могла быть такой высокой, как 4,6 ГГц (и, вероятно, не выше 3,0 ГГц), но в основном указывает на необходимость однозначного измерения истекшего времени ....

Ваша цитата из руководства по оптимизации о неэффективности потоковых хранилищ применима только в случаях, которые не могут быть объединены в полную передачу строк кэша. Ваш код сохраняется в каждом элементе строк выходного кэша в соответствии с рекомендациями «наилучшей практики» (все инструкции хранилища, записывающие в одну и ту же строку, выполняются последовательно и генерируют только один поток хранилищ на l oop). Невозможно полностью предотвратить аппаратное разрушение потоковых хранилищ, но в вашем случае это должно быть крайне редко - возможно, несколько из миллиона. Обнаружение хранилищ частичной потоковой передачи является очень продвинутой задачей c, требующей использования плохо документированных счетчиков производительности при «неосновном» и / или косвенном обнаружении хранилищ частичной потоковой передачи с помощью поиска повышенных счетчиков DRAM CAS (что может быть связано с другими причины). Больше записей о потоковых магазинах можно найти по http://sites.utexas.edu/jdm4372/2018/01/01/notes-on-non-temporal-aka-streaming-stores/

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...