Коэффициент попадания в кэш профилирования функции программы на C

Я хочу получить процент попаданий в кеш для конкретной функции программы C / C ++ (foo), запущенной на машине Linux. Я использую gcc и не оптимизирую компилятор. С помощью perf я могу получить процент попаданий для всей программы, используя следующую команду.

perf stat -e L1-dcache-load, L1-dcache-load-misses, L1-dcache-store, L1-dcache-store-misses ./a.out

Но меня интересует только ядро ​​foo.

Есть ли способ получить процент попаданий только для foo с помощью perf или любого другого инструмента?

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>


#define NI 192
#define NJ NI

#ifndef DATA_TYPE
    #define DATA_TYPE float
#endif


static 
void* xmalloc(size_t num)
{
    void * nnew = NULL;
    int ret = posix_memalign (&nnew, 32, num);
    if(!nnew || ret)
    {
        fprintf(stderr, "Can not allocate Memory\n");
        exit(1);
    }
    return nnew;
}

void* alloc_data(unsigned long long int n, int elt_size)
{
    size_t val = n;
    val *= elt_size;
    void* ret = xmalloc(val);
    return ret;
}


/* Array initialization. */
static
void init_array(int ni, int nj,
        DATA_TYPE A[NI][NJ],
        DATA_TYPE R[NJ][NJ],
        DATA_TYPE Q[NI][NJ])
{
  int i, j;

  for (i = 0; i < ni; i++)
    for (j = 0; j < nj; j++) {
      A[i][j] = ((DATA_TYPE) i*j) / ni;
      Q[i][j] = ((DATA_TYPE) i*(j+1)) / nj;
    }
  for (i = 0; i < nj; i++)
    for (j = 0; j < nj; j++)
      R[i][j] = ((DATA_TYPE) i*(j+2)) / nj;
}


/* Main computational kernel.*/

static
void foo(int ni, int nj,
        DATA_TYPE A[NI][NJ],
        DATA_TYPE R[NJ][NJ],
        DATA_TYPE Q[NI][NJ])
{
  int i, j, k;

  DATA_TYPE nrm;
  for (k = 0; k < nj; k++)
  {
    nrm = 0;
    for (i = 0; i < ni; i++)
      nrm += A[i][k] * A[i][k];
    R[k][k] = sqrt(nrm);
    for (i = 0; i < ni; i++)
      Q[i][k] = A[i][k] / R[k][k];
    for (j = k + 1; j < nj; j++)
    {
      R[k][j] = 0;
      for (i = 0; i < ni; i++)
        R[k][j] += Q[i][k] * A[i][j];
      for (i = 0; i < ni; i++)
        A[i][j] = A[i][j] - Q[i][k] * R[k][j];
    }
  }
}


int main(int argc, char** argv)
{
  /* Retrieve problem size. */
  int ni = NI;
  int nj = NJ;

  /* Variable declaration/allocation. */
  DATA_TYPE (*A)[NI][NJ];
  DATA_TYPE (*R)[NI][NJ];
  DATA_TYPE (*Q)[NI][NJ];

  A = ((DATA_TYPE (*)[NI][NJ])(alloc_data((NI*NJ), (sizeof(DATA_TYPE)))));
  R = ((DATA_TYPE (*)[NI][NJ])(alloc_data((NI*NJ), (sizeof(DATA_TYPE)))));
  Q = ((DATA_TYPE (*)[NI][NJ])(alloc_data((NI*NJ), (sizeof(DATA_TYPE)))));
  
/* Initialize array(s). */
  init_array (ni, nj,
          (*A),
          (*R),
          (*Q));


  /* Run kernel. */
  foo (ni, nj, *A, *R, *Q);

  /* Be clean. */
  free((void *)A);
  free((void *)R);
  free((void *)Q);

  return 0;
}

Вывод команды lscpu:

Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                16
On-line CPU(s) list:   0-15 
Thread(s) per core:    2
Core(s) per socket:    8
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel 
CPU family:            6
Model:                 63
Model name:            Intel(R) Core(TM) i7-5960X CPU @ 3.00GHz
Stepping:              2
CPU max MHz:           3500.0000
CPU min MHz:           1200.0000
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              20480K
NUMA node0 CPU(s):     0-15

person Atanu Barai    schedule 29.10.2020    source источник
comment
Написать программу, которая запускает только foo(), и измерить ее?   -  person hyde    schedule 29.10.2020
comment
Что вам нужно, так это измерить калибр: счетчик запусков перед вызовом foo () и счетчик остановок в конце foo (). Чтобы сделать это, вам нужно будет инструментировать код и перестроить его. Возможность получения счетчиков зависит от архитектуры процессора и его PMU. Способ получения счетчиков зависит от продавца. Вот почему полезны библиотеки, такие как papi, поскольку они прозрачно поддерживают несколько архитектур процессоров / PMU. Почему вы не могли использовать папи?   -  person Rachid K.    schedule 29.10.2020
comment
@hyde: это будет включать счетчики для динамической компоновки и для части выделения / инициализации. Вы можете подсчитывать только пространство пользователя, используя perf stat --all-user (или с более старыми perf, с event:u,event:u,...). Так что да, вы можете просто рассчитать время для всей программы, если можете повторять foo много раз, чтобы заглушить фоновый шум работы инициализации; если его можно запускать несколько раз без повторения инициализации. Но это может оказаться непрактичным, если вы хотите запустить foo с большим массивом, который требует много времени инициализации.   -  person Peter Cordes    schedule 29.10.2020
comment
@PeterCordes Может использовать статическое связывание. Мог предварительно рассчитать массив.   -  person hyde    schedule 29.10.2020
comment
@hyde, да, но с большими статическими данными первый запуск foo() приведет к сбою страниц, вместо чтения недавно записанной памяти, которая, вероятно, горячая в TLB, и, скорее всего, по крайней мере, не будет с ошибкой страницы, поэтому некоторая предварительная выборка HW и OoO exec через границы страницы будут доступны, даже если он просто зацикливается на большом непрерывном массиве. Если это структура данных, основанная на указателях (например, дерево), mmaping с MAP_FIXED правдоподобно, но создание данных и их использование является большой проблемой. И не жизнеспособно, если foo задействует free() в любых инициализированных данных.   -  person Peter Cordes    schedule 29.10.2020
comment
@ Ричард К. Я в основном пробовал для этого PAPI. Пытался использовать эту формулу L1-D-Cache Hit rate = 1 - PAPI_L1_DCM / (PAPI_LD_INS + PAPI_SR_INS)   -  person Atanu Barai    schedule 29.10.2020
comment
Но это возвращает мне код ошибки -8 (событие существует, но не может быть подсчитано из-за ограничений ресурса счетчика), когда я пытаюсь добавить эти события с помощью функции PAPI_add_event. Не получается, когда я пытаюсь добавить три события. Если я добавлю только два события, все будет нормально.   -  person Atanu Barai    schedule 29.10.2020
comment
@PeterCordes Конечно. Зависит от конкретной цели, важно это или нет. В многопользовательской многозадачной ОС общего назначения в любом случае будет много шума на общем ресурсе, таком как кеш процессора. Также может быть полезен высокий приоритет процесса.   -  person hyde    schedule 29.10.2020
comment
@AtanuBarai: Когда вы используете perf из командной строки, позволяет ли это использовать более двух событий (кроме инструкций или циклов с фиксированными счетчиками) без необходимости мультиплексировать счетчики? (дополнительный столбец% времени, в течение которого этот счетчик был активен). Вы отключили сторожевой таймер NMI, который использует один программируемый счетчик?   -  person Peter Cordes    schedule 30.10.2020
comment
@Peter Cordes Нет, нет. Думаю, для этого в ЦП не хватает счетчиков. Таким образом, одним решением может быть добавление только двух событий за раз, получение значений счетчика, а в следующем запуске добавление другого счетчика и получение значения счетчика. Отключение NMI-сторожевого таймера - хорошая идея. Я попробую. Спасибо   -  person Atanu Barai    schedule 31.10.2020
comment
@AtanuBarai: Какая у вас архитектура процессора? Если это Intel, можете ли вы отредактировать свой вопрос и добавить результат команды cpuid -1? Последнее является подробным, просто поделитесь заголовком (идентификация процессора) и частью PMU (записи функций мониторинга производительности архитектуры).   -  person Rachid K.    schedule 31.10.2020
comment
Не понимаю, почему закрыли этот вопрос: понятно ...   -  person Rachid K.    schedule 31.10.2020
comment
@ RachidK. Я добавил сюда вывод команды lscpu.   -  person Atanu Barai    schedule 01.11.2020
comment
С помощью cpuid вы также получите информацию о PMU (особенно о количестве счетчиков в разделах. Функции мониторинга производительности архитектуры.   -  person Rachid K.    schedule 01.11.2020
comment
Согласно Документация Intel: семейство процессоров Intel Core i7 поддерживает мониторинг архитектурной производительности, предоставляет четыре счетчика производительности общего назначения (IA32_PMC0, IA32_PMC1, IA32_PMC2, IA32_PMC3) и три фиксированных производительности счетчики (IA32_FIXED_CTR0, IA32_FIXED_CTR1, IA32_FIXED_CTR2) в ядре процессора.   -  person Rachid K.    schedule 01.11.2020


Ответы (3)


Вы также можете использовать Likwid и его Marker-API. Это позволяет очень легко инструментировать определенные области вашего кода. Вы можете использовать предопределенную группу производительности ICACHE в архитектуре haswell для частоты промахов кэша L1 или определить свою собственную группу производительности для частоты совпадений L1.

#include likwid.h
LIKWID_MARKER_INIT;
LIKWID_MARKER_START("region foo");

foo();

LIKWID_MARKER_STOP("region foo");
LIKWID_MARKER_CLOSE;

запустить приложение с помощью:

./likwid-perfctr -g ICACHE -m <your application>

Убедитесь, что вы скомпилировали с -DLIKWID-PERFMON и добавили Likwid include, путь к библиотеке и линковали библиотеку Likwid: -L$LIKWID_LIB -I$LIKWID_INCLUDE -llikwid. Все очень хорошо документировано в их github wiki

person breiters    schedule 05.02.2021

Возможно, вас заинтересует gprof (1). Он не будет измерять частоту попаданий в кеш (это не имеет смысла, поскольку некоторые вызовы foo могут быть встроенными, как только GCC вызывается с включенной оптимизацией).

Вы можете использовать libbacktrace в своем коде. См. Также time (7) и signal (7).

Вы можете скомпилировать свой код с gcc -Wall -Wextra -O2 -g -pg, а затем использовать libbacktrace (например, GCC или RefPerSys) внутри него, а позже gprof (1) с gdb (1).

С усилиями (прочтите Advanced Linux Programming, затем системные вызовы (2) и signal-security (7)) вы можете использовать setitimer (2) с sigaction (2) и / или profil (3).

Также рассмотрите возможность генерации некоторого кода C (например, с помощью GPP и / или GNU bison в вашем собственном генераторе кода C) и см. этот ответ. Книга Дж. Питрата Искусственные существа: совесть человека Сознательная машина (ISBN-13: 978-1848211018) могла бы вдохновить. Вы можете захотеть сгенерировать некоторый код C для дополнительных инструментов.

Вы можете сгенерировать некоторый код в подключаемом модуле (например, с помощью libgccjit или GNU lightning ...) во время выполнения, затем dlopen (3) и dlsym (3) it. Узнайте больше о частичной оценке и просмотрите мой _ 4_ пример, а более серьезно - исходный код Ocaml или SBCL.

Вы можете написать свой плагин GCC для автоматического создания некоторых измерений на более умном чем то, что делает опция -pg GCC. Ваш плагин GCC преобразует (на уровне GIMPLE) большинство вызовов функций во что-то более сложное выполнение некоторых тестов (так -pg работает внутри GCC, и вы можете изучить исходный код GCC). Попробуйте скомпилировать свой foo.c как gcc -Wall -Wextra -O2 -pg -S -fverbose-asm foo.c и изучить сгенерированный foo.s, возможно, добавив дополнительные оптимизации, или статический анализ или параметры инструментария.

Возможно, вас заинтересуют последние статьи о ACM SIGPLAN.

Наконец, тестирование программы на C, скомпилированной без оптимизации, не имеет смысла. Вместо этого рассмотрите возможность компиляции и связывания вашей программы как минимум с gcc -flto -O2 -Wall

В своем foo вы можете с умом использовать clock_gettime (2) для измерения процессорного времени.

И если производительность очень важна и вам разрешено потратить недели на ее улучшение, вы можете рассмотреть возможность использования OpenCL (или, возможно, CUDA) для вычисления вашего ядра на мощном GPGPU. Конечно, вам понадобится специальное оборудование. В противном случае рассмотрите возможность использования OpenMP или OpenACC (или, возможно, MPI). Некоторые недавние компиляторы GCC (по крайней мере, GCC 10 в октябре 2020 г.) могли их поддерживать. Конечно, прочтите документацию по Вызов GCC .

person Basile Starynkevitch    schedule 29.10.2020
comment
Спасибо за Ваш ответ. Но я не думаю, что это имеет отношение к вопросу. - person Atanu Barai; 29.10.2020
comment
Мы не понимаем вашего вопроса. Подумайте об улучшении этого с помощью некоторого минимального воспроизводимого примера, написанного на C. И вы должны использовать profil (3) - person Basile Starynkevitch; 29.10.2020

Во-первых, обратите внимание, что L1-dcache-store-misses не поддерживается вашим процессором. perf stat сообщит вам об этом в выводе.

perf stat не позволяет профилировать только выбранные области кода. Для этого вам необходимо вручную настроить код так, чтобы указанные события управлялись по интересующим областям по желанию.

Невозможно подсчитать события L1-dcache-loads, L1-dcache-load-misses и L1-dcache-stores без мультиплексирования на вашем процессоре (Haswell). Они сопоставлены с собственными событиями MEM_UOPS_RETIRED.ALL_LOADS, L1D.REPLACEMENT и MEM_UOPS_RETIRED.ALL_STORES соответственно. Каждое из этих событий может быть засчитано только первыми четырьмя универсальными жетонами. Кроме того, есть ошибка, которая не задокументирована в документе обновления спецификации i7-5960X, но существует в i7-5960X (она задокументирована в документах обновления спецификации других процессоров Haswell и процессоров некоторых других микроархитектур). Эта ошибка по-разному обрабатывается в разных версиях perf. Начиная с версии ядра 4.1-rc7, если на логическом ядре разрешено одно из событий, на которое влияет ошибка, и если во время загрузки включена гиперпоточность, логическое ядро ​​может использовать только до двух из четырех универсальных счетчики. MEM_UOPS_RETIRED.* события относятся к числу событий, затронутых ошибкой. Единственное, что вы можете сделать, - это отключить гиперпоточность.

Важно понимать, какую частоту попаданий в кеш можно измерить с помощью этих событий. Вероятно, вы не хотите измерять то, что не имеет смысла. Одно из соотношений, которое может иметь смысл, - это L1-dcache-load-misses / (L1-dcache-loads + L1-dcache-stores), которое представляет количество замен L1D (строк, заполненных в кэше, которые вызывают выселение других) по любой причине, деленное на количество отключенных мопов загрузки и сохранения. Не все промахи вызывают замену, и значительная часть всех промахов может попасть в LFB, что также не вызывает замен. Кроме того, не все замены вызваны обращениями мопов, которые в конечном итоге удаляются.

person Hadi Brais    schedule 02.12.2020