PMC для подсчета попаданий программной предварительной выборки в кеш L1

Я пытаюсь найти PMC (счетчик мониторинга производительности), который будет отображать количество раз, когда инструкция prefetcht0 попадает в dcache L1 (или пропускает).

icelake-client: процессор Intel (R) Core (TM) i7-1065G7 @ 1,30 ГГц

Я пытаюсь сделать это мелкое зерно, т.е. (примечание должно включать lfence около prefetcht0)

    xorl %ecx, %ecx
    rdpmc
    movl %eax, %edi
    prefetcht0 (%rsi)
    rdpmc
    testl %eax, %edi
    // jump depending on if it was a miss or not

Цель состоит в том, чтобы проверить, попала ли предварительная выборка в L1. Если не выполнил какой-то готовый код, в противном случае продолжайте.

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

Я пробовал несколько событий из libpfm4 и руководства Intel безуспешно:

L1-DCACHE-LOAD-MISSES, emask=0x00, umask=0x10000
L1D.REPLACEMENT, emask=0x51, umask=0x1 
L2_RQSTS.SWPF_HIT, emask=0x24, umask=0xc8
L2_RQSTS.SWPF_MISS, emask=0x24, umask=0x28
LOAD_HIT_PREFETCH.SWPF, emask=0x01, umask=0x4c  (this very misleadingly is non-sw prefetch hits)

L1D.REPLACEMENT и L1-DCACHE-LOAD-MISSES вроде работают, это работает, если я задерживаю rdpmc, но если они идут один за другим, это в лучшем случае кажется ненадежным. Остальные - полные переборы.

Вопросов:

  1. Должно ли что-либо из этого работать для обнаружения попадания предварительной выборки в dcache L1? (т.е. мое тестирование плохое)
  2. Если не. Какие события можно использовать для определения попадания предварительной выборки в dcache L1?

Изменить: MEM_LOAD_RETIRED.L1_HIT, похоже, не работает для предварительной выборки программного обеспечения.

Вот код, который я использую для тестирования:

#include <asm/unistd.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/perf_event.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>


#define HIT  0
#define MISS 1

#define TODO MISS


#define PAGE_SIZE 4096

// to force hit make TSIZE low
#define TSIZE     10000

#define err_assert(cond)                                                       \
    if (__builtin_expect(!(cond), 0)) {                                        \
        fprintf(stderr, "%d:%d: %s\n", __LINE__, errno, strerror(errno));      \
        exit(-1);                                                              \
    }


uint64_t
get_addr() {
    uint8_t * addr =
        (uint8_t *)mmap(NULL, TSIZE * PAGE_SIZE, PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    err_assert(addr != NULL);


    for (uint32_t i = 0; i < TSIZE; ++i) {
        addr[i * PAGE_SIZE + (PAGE_SIZE - 1)] = 0;
        #if TODO == HIT
        addr[i * PAGE_SIZE] = 0;
        #endif
    }

    return uint64_t(addr);
}

int
perf_event_open(struct perf_event_attr * hw_event,
                pid_t                    pid,
                int                      cpu,
                int                      group_fd,
                unsigned long            flags) {
    int ret;

    ret = syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags);
    return ret;
}

void
init_perf_event_struct(struct perf_event_attr * pe,
                       const uint32_t           type,
                       const uint64_t           ev_config,
                       int                      lead) {
    __builtin_memset(pe, 0, sizeof(struct perf_event_attr));

    pe->type           = type;
    pe->size           = sizeof(struct perf_event_attr);
    pe->config         = ev_config;
    pe->disabled       = !!lead;
    pe->exclude_kernel = 1;
    pe->exclude_hv     = 1;
}


/* Fixed Counters */
static constexpr uint32_t core_instruction_ev  = 0x003c;
static constexpr uint32_t core_instruction_idx = (1 << 30) + 0;

static constexpr uint32_t core_cycles_ev  = 0x00c0;
static constexpr uint32_t core_cycles_idx = (1 << 30) + 1;

static constexpr uint32_t ref_cycles_ev  = 0x0300;
static constexpr uint32_t ref_cycles_idx = (1 << 30) + 2;

/* programmable counters */
static constexpr uint32_t mem_load_retired_l1_hit  = 0x01d1;
static constexpr uint32_t mem_load_retired_l1_miss = 0x08d1;


int
init_perf_tracking() {
    struct perf_event_attr pe;

    init_perf_event_struct(&pe, PERF_TYPE_RAW, core_instruction_ev, 1);
    int leadfd = perf_event_open(&pe, 0, -1, -1, 0);
    err_assert(leadfd >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, core_cycles_ev, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, ref_cycles_ev, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);


    init_perf_event_struct(&pe, PERF_TYPE_RAW, mem_load_retired_l1_hit, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

    return leadfd;
}

void
start_perf_tracking(int leadfd) {
    ioctl(leadfd, PERF_EVENT_IOC_RESET, 0);
    ioctl(leadfd, PERF_EVENT_IOC_ENABLE, 0);
}

#define _V_TO_STR(X) #X
#define V_TO_STR(X)  _V_TO_STR(X)

//#define DO_PREFETCH
#ifdef DO_PREFETCH
#define DO_MEMORY_OP(addr) "prefetcht0 (%[" V_TO_STR(addr) "])\n\t"
#else
#define DO_MEMORY_OP(addr) "movl (%[" V_TO_STR(addr) "]), %%eax\n\t"
#endif


int
main() {
    int fd = init_perf_tracking();
    start_perf_tracking(fd);

    uint64_t addr = get_addr();

    uint32_t prefetch_miss, cycles_to_detect;
    asm volatile(
        "lfence\n\t"
        "movl %[core_cycles_idx], %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[cycles_to_detect]\n\t"
        "xorl %%ecx, %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[prefetch_miss]\n\t"
        "lfence\n\t"
        DO_MEMORY_OP(prefetch_addr)
        "lfence\n\t"
        "xorl %%ecx, %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[prefetch_miss], %%eax\n\t"
        "movl %%eax, %[prefetch_miss]\n\t"
        "movl %[core_cycles_idx], %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[cycles_to_detect], %%eax\n\t"
        "movl %%eax, %[cycles_to_detect]\n\t"
        "lfence\n\t"
        : [ prefetch_miss ] "=&r"(prefetch_miss),
          [ cycles_to_detect ] "=&r"(cycles_to_detect)
        : [ prefetch_addr ] "r"(addr), [ core_cycles_idx ] "i"(core_cycles_idx)
        : "eax", "edx", "ecx");

    fprintf(stderr, "Hit    : %d\n", prefetch_miss);
    fprintf(stderr, "Cycles : %d\n", cycles_to_detect);
}

если я определю DO_PREFETCH, результаты для MEM_LOAD_RETIRED.L1_HIT всегда будут равны 1 (всегда кажется, что попадание получено). Если я закомментирую DO_PREFETCH, результаты будут соответствовать тому, что я ожидал (когда адрес явно отсутствует в отчетах о пропуске кэша, когда он явно попадает в отчеты о попадании).

С DO_PREFETCH:

g++ -DDO_PREFETCH -O3 -march=native -mtune=native prefetch_hits.cc -o prefetch_hits
$> ./prefetch_hits
Hit    : 1
Cycles : 554

и без DO_PREFETCH

g++ -DDO_PREFETCH -O3 -march=native -mtune=native prefetch_hits.cc -o prefetch_hits
$> ./prefetch_hits
Hit    : 0
Cycles : 888

С L2_RQSTS.SWPF_HIT и L2_RQSTS.SWPF_MISS удалось заставить его работать. Большое спасибо Хади Брайс. Стоит отметить, что причина, по которой L1D_PEND_MISS.PENDING не работает, может быть связана с Icelake. Хади Брайс сообщил, что он заставил его работать для прогнозирования промахов кэширования L1D на Haswell.

Чтобы попытаться определить, почему L1_PEND_MISS.PENDING и MEM_LOAD_RETIRED.L1_HIT не работают, разместил точный код, который я использую для их тестирования:

#include <asm/unistd.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/perf_event.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>


#define HIT  0
#define MISS 1

#define TODO MISS


#define PAGE_SIZE 4096

#define TSIZE 1000

#define err_assert(cond)                                                       \
    if (__builtin_expect(!(cond), 0)) {                                        \
        fprintf(stderr, "%d:%d: %s\n", __LINE__, errno, strerror(errno));      \
        exit(-1);                                                              \
    }


uint64_t
get_addr() {
    uint8_t * addr =
        (uint8_t *)mmap(NULL, TSIZE * PAGE_SIZE, PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    err_assert(addr != NULL);
    __builtin_memset(addr, -1, TSIZE * PAGE_SIZE);
    return uint64_t(addr);
}

int
perf_event_open(struct perf_event_attr * hw_event,
                pid_t                    pid,
                int                      cpu,
                int                      group_fd,
                unsigned long            flags) {
    int ret;

    ret = syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags);
    return ret;
}

void
init_perf_event_struct(struct perf_event_attr * pe,
                       const uint32_t           type,
                       const uint64_t           ev_config,
                       int                      lead) {
    __builtin_memset(pe, 0, sizeof(struct perf_event_attr));

    pe->type           = type;
    pe->size           = sizeof(struct perf_event_attr);
    pe->config         = ev_config;
    pe->disabled       = !!lead;
    pe->exclude_kernel = 1;
    pe->exclude_hv     = 1;
}


/* Fixed Counters */
static constexpr uint32_t core_instruction_ev  = 0x003c;
static constexpr uint32_t core_instruction_idx = (1 << 30) + 0;

static constexpr uint32_t core_cycles_ev  = 0x00c0;
static constexpr uint32_t core_cycles_idx = (1 << 30) + 1;

static constexpr uint32_t ref_cycles_ev  = 0x0300;
static constexpr uint32_t ref_cycles_idx = (1 << 30) + 2;

/* programmable counters */
static constexpr uint32_t mem_load_retired_l1_hit  = 0x01d1;
static constexpr uint32_t mem_load_retired_l1_miss = 0x08d1;
static constexpr uint32_t l1d_pending              = 0x0148;
static constexpr uint32_t swpf_hit                 = 0xc824;
static constexpr uint32_t swpf_miss                = 0x2824;
static constexpr uint32_t ev0                      = l1d_pending;

#define NEVENTS 1
#if NEVENTS > 1
static constexpr uint32_t ev1 = swpf_miss;
#endif

int
init_perf_tracking() {
    struct perf_event_attr pe;

    init_perf_event_struct(&pe, PERF_TYPE_RAW, core_instruction_ev, 1);
    int leadfd = perf_event_open(&pe, 0, -1, -1, 0);
    err_assert(leadfd >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, core_cycles_ev, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, ref_cycles_ev, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

    init_perf_event_struct(&pe, PERF_TYPE_RAW, ev0, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);

#if NEVENTS > 1
    init_perf_event_struct(&pe, PERF_TYPE_RAW, ev1, 0);
    err_assert(perf_event_open(&pe, 0, -1, leadfd, 0) >= 0);
#endif

    return leadfd;
}

void
start_perf_tracking(int leadfd) {
    ioctl(leadfd, PERF_EVENT_IOC_RESET, 0);
    ioctl(leadfd, PERF_EVENT_IOC_ENABLE, 0);
}

#define _V_TO_STR(X) #X
#define V_TO_STR(X)  _V_TO_STR(X)

//#define LFENCE
#ifdef LFENCE
#define SERIALIZER() "lfence\n\t"
#else
#define SERIALIZER()                                                           \
    "xorl %%ecx, %%ecx\n\t"                                                    \
    "xorl %%eax, %%eax\n\t"                                                    \
    "cpuid\n\t"

#endif

#define DO_PREFETCH

#ifdef DO_PREFETCH
#define DO_MEMORY_OP(addr) "prefetcht0 (%[" V_TO_STR(addr) "])\n\t"
#else
#define DO_MEMORY_OP(addr) "movl (%[" V_TO_STR(addr) "]), %%eax\n\t"
#endif


int
main() {
    int fd = init_perf_tracking();
    start_perf_tracking(fd);

    uint64_t addr = get_addr();

    // to ensure page in TLB
    *((volatile uint64_t *)(addr + (PAGE_SIZE - 8))) = 0;
    
#if TODO == HIT
    // loading from 0 offset to check cache miss / hit
    *((volatile uint64_t *)addr) = 0;
#endif

    uint32_t ecount0 = 0, ecount1 = 0, cycles_to_detect = 0;
    asm volatile(
        SERIALIZER()
        "movl %[core_cycles_idx], %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[cycles_to_detect]\n\t"
        "xorl %%ecx, %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[ecount0]\n\t"
#if NEVENTS > 1
        "movl $1, %%ecx\n\t"
        "rdpmc\n\t"
        "movl %%eax, %[ecount1]\n\t"
#endif
        SERIALIZER()
        DO_MEMORY_OP(prefetch_addr)
        SERIALIZER()
        "xorl %%ecx, %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[ecount0], %%eax\n\t"
        "movl %%eax, %[ecount0]\n\t"
#if NEVENTS > 1
        "movl $1, %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[ecount1], %%eax\n\t"
        "movl %%eax, %[ecount1]\n\t"
#endif
        "movl %[core_cycles_idx], %%ecx\n\t"
        "rdpmc\n\t"
        "subl %[cycles_to_detect], %%eax\n\t"
        "movl %%eax, %[cycles_to_detect]\n\t"
        SERIALIZER()
        : [ ecount0 ] "=&r"(ecount0),
#if NEVENTS > 1
          [ ecount1 ] "=&r"(ecount1),
#endif
          [ cycles_to_detect ] "=&r"(cycles_to_detect)
        : [ prefetch_addr ] "r"(addr), [ core_cycles_idx ] "i"(core_cycles_idx)
        : "eax", "edx", "ecx");

    fprintf(stderr, "E0     : %d\n", ecount0);
    fprintf(stderr, "E1     : %d\n", ecount1);
    fprintf(stderr, "Cycles : %d\n", cycles_to_detect);
}

person Noah    schedule 16.01.2021    source источник
comment
Это не требует рекомендации по книге, инструменту или библиотеке. Он спрашивает, какая аппаратная функция и алгоритм, если таковые имеются, могут выполнить конкретную задачу. Я не думаю, что его нужно закрывать.   -  person Nate Eldredge    schedule 16.01.2021
comment
слегка отредактировал вопрос, чтобы он звучал не столько как запрос предложения, а как запрос ответа на общий вопрос.   -  person Noah    schedule 16.01.2021
comment
Вы собираетесь использовать это в качестве меры профилирования? Или на самом деле в качестве оптимизации эффективности, с идеей, что, если предварительная выборка не сработала, программа может выполнить некоторую другую работу до того, как фактически попытается загрузить? Последнее не похоже на предполагаемую цель ЧВК, и мне было бы интересно, считают ли эксперты такой подход вообще разумным.   -  person Nate Eldredge    schedule 16.01.2021
comment
@NateEldredge Мне интересно использовать его для оптимизации. Я знаю, что это не по назначению, но похоже, что стоит хотя бы попробовать. Мне тоже интересно узнать, имеет ли эта идея хоть какой-то вес.   -  person Noah    schedule 16.01.2021
comment
Я склонен предположить, что нормальный способ добиться этого - это структурировать код так, чтобы выполнение вне очереди могло запускать вашу другую работу в ожидании завершения загрузки, а не фактически изменять путь выполнения. Но это предполагает, что другая работа в конечном итоге должна быть выполнена в любом случае, что, возможно, неверно, если вы просто хотите тратить время простоя на выполнение чего-то необязательного (сложить некоторые белки и т. Д.).   -  person Nate Eldredge    schedule 16.01.2021
comment
@NateEldredge обычно так и есть. Мой конечный вариант использования для этого - что-то вроде нанокорум. Идея заключалась бы в том, что вместо того, чтобы просто слепо выполнять предварительную выборку, а затем переключать выполнение, вы могли бы сначала проверить, является ли промах, потому что накладные расходы на переключение кадра не являются незначительными. По большей части кода я склонен согласиться.   -  person Noah    schedule 17.01.2021
comment
Я не проверял это на процессорах IceLake, но то, что вы пытаетесь сделать, вряд ли будет полезно по ряду причин .... Инструкция RDPMC стоит дорого - самая быстрая реализация, которую я видел, требует ~ 25 циклов, поэтому ваши накладные расходы составляют 50 циклов плюс вероятное неверное предсказание ветвления. Не совсем полная задержка памяти, но определенно большой кусок.   -  person John D McCalpin    schedule 18.01.2021


Ответы (1)


rdpmc не упорядочен с событиями, которые могут произойти до него или после него в программном порядке. Инструкция по полной сериализации, такая как cpuid, требуется для получения желаемых гарантий упорядочивания в отношении prefetcht0. Код должен быть следующим:

xor  %eax, %eax         # CPUID leaf eax=0 should be fast.  Doing this before each CPUID might be a good idea, but omitted for clarity
cpuid
xorl %ecx, %ecx
rdpmc
movl %eax, %edi         # save RDPMC result before CPUID overwrites EAX..EDX
cpuid
prefetcht0 (%rsi)
cpuid
xorl %ecx, %ecx
rdpmc
testl %eax, %edi        # CPUID doesn't affect FLAGS
cpuid

Каждая из rdpmc инструкций зажата между cpuid инструкциями. Это гарантирует, что учитываются любые события и только те события, которые происходят между двумя rdpmc инструкциями.

Операцию предварительной выборки инструкции prefetcht0 можно либо игнорировать, либо выполнять. Если это было выполнено, оно может либо попасть в строку кэша, которая находится в допустимом состоянии в L1D, либо нет. Это случаи, которые необходимо учитывать.

Сумму L2_RQSTS.SWPF_HIT и L2_RQSTS.SWPF_MISS нельзя использовать для подсчета или получения количества prefetcht0 совпадений в L1D, но их сумму можно вычесть из SW_PREFETCH_ACCESS.T0, чтобы получить верхнюю границу количества prefetcht0 совпадений в L1D. С правильно сериализованной последовательностью, показанной выше, я думаю, что единственный случай, когда неотмеченный prefetcht0 не попадает в L1D и не учитывается суммой _15 _ + _ 16_, - это если операция предварительной выборки программного обеспечения попадает в LFB, выделенный для оборудования. предварительная выборка.

L1-DCACHE-LOAD-MISSES - это просто еще одно название для L1D.REPLACEMENT. Код события и маска umask, которые вы указали для L1-DCACHE-LOAD-MISSES, неверны. Событие L1D.REPLACEMENT возникает только в том случае, если операция предварительной выборки не выполняется в L1D (что вызывает отправку запроса в L2) и вызывает замену действующей строки в L1D. Обычно большинство заполнений вызывают замену, но это событие по-прежнему нельзя использовать для различения prefetcht0, попадающего в L1D, prefetcht0, попадающего в LFB, выделенного для аппаратной предварительной выборки, и игнорируемого prefetcht0.

Событие LOAD_HIT_PREFETCH.SWPF происходит, когда нагрузка по запросу попадает в LFB, выделенный для предварительной выборки программного обеспечения. Очевидно, здесь это бесполезно.

Событие L1D_PEND_MISS.PENDING (event = 0x48, umask = 0x01) должно работать. Согласно документации, это событие увеличивает счетчик на количество ожидающих промахов L1D в каждом цикле. Я думаю, что это работает для загрузки по запросу и предварительной выборки. Это действительно приблизительное значение, поэтому оно может учитываться, даже если ожидающих промахов L1D ноль. Но я думаю, что его все еще можно использовать для определения с очень высокой степенью уверенности, пропущен ли один prefetcht0 в L1D, выполнив следующие действия:

  • Сначала добавьте строку uint64_t value = *(volatile uint64_t*)addr; непосредственно перед встроенной сборкой. Это должно увеличить вероятность почти до 100% того, что строка для предварительной выборки находится в L1D.
  • Во-вторых, измерьте минимальное приращение L1D_PEND_MISS.PENDING для prefetcht0, которое с большой вероятностью попадет в L1D.
  • Выполните эксперимент много раз, чтобы получить высокую уверенность в том, что минимальное приращение очень стабильно в той степени, в которой одно и то же точное значение наблюдается почти в каждом прогоне.
  • Закомментируйте строку, добавленную на первом шаге, чтобы prefetcht0 пропустили, и убедитесь, что изменение количества событий всегда или почти всегда больше, чем минимальное приращение, измеренное ранее.

До сих пор я интересовался только различием между предварительной выборкой, которая попадает в L1D, и не игнорируемой предварительной выборкой, которая отсутствует как в L1D, так и в LFB. Теперь рассмотрю остальные случаи:

  • Если предварительная выборка приводит к сбою страницы или если тип памяти целевой строки кэша - WC или UC, предварительная выборка игнорируется. Я не знаю, можно ли использовать событие L1D_PEND_MISS.PENDING, чтобы отличить попадание от этого случая. Вы можете запустить эксперимент, в котором целевой адрес инструкции предварительной выборки находится на виртуальной странице без допустимого сопоставления или сопоставлен со страницей ядра. С высокой вероятностью проверьте, является ли изменение количества событий уникальным.
  • Если LFB недоступны, предварительная выборка игнорируется. Этот случай можно устранить, отключив логическое ядро ​​того же уровня и используя cpuid вместо lfence перед первым rdpmc.
  • Если предварительная выборка попадает в LFB, выделенный для RFO, ItoM или аппаратного запроса предварительной выборки, то предварительная выборка фактически избыточна. Для всех этих типов запросов изменение счетчика L1D_PEND_MISS.PENDING может отличаться или не отличаться от попадания в L1D. Этот случай можно устранить, используя cpuid вместо lfence перед первым rdpmc и отключив два аппаратных устройства предварительной выборки L1D.
  • Я не думаю, что предварительная выборка из памяти с возможностью предварительной загрузки может ударить по WCB, потому что изменение типа памяти для местоположения является полностью сериализуемой операцией, поэтому в этом случае не проблема.

Одно очевидное преимущество использования L1D_PEND_MISS.PENDING вместо суммы _40 _ + _ 41_ - меньшее количество событий. Еще одно преимущество состоит в том, что L1D_PEND_MISS.PENDING поддерживается некоторыми из более ранних микроархитектур. Кроме того, как обсуждалось выше, он может быть более мощным. На моем Haswell работает с порогом 69-70 циклов.

Если изменения события L1D_PEND_MISS.PENDING в разных случаях не различимы, то можно использовать сумму _44 _ + _ 45_. Эти два события происходят в L2, поэтому они только говорят вам, пропущена ли предварительная выборка в L1D, а запрос отправлен и принят L2. Если запрос отклонен или попадает в SQ L2, ни одно из двух событий не может произойти. Кроме того, все вышеупомянутые случаи невозможно отличить от попадания L1D.

Для нормальных нагрузок можно использовать MEM_LOAD_RETIRED.L1_HIT. Если нагрузка попадает в L1D, возникает единственный L1_HIT. В противном случае, в любом другом случае, событий L1_HIT не произойдет, если предположить, что никакая другая инструкция между двумя rdpmc, например cpuid, не может генерировать L1_HIT события. Вам нужно будет убедиться, что cpuid не генерирует L1_HIT событий. Не забывайте подсчитывать только события пользовательского режима, потому что между любыми двумя инструкциями может произойти прерывание, а обработчик прерывания может сгенерировать одно или несколько событий L1_HIT в режиме ядра. Хотя это очень маловероятно, но если вы хотите быть уверены на 100%, проверьте также, не вызывает ли само возникновение прерывания L1_HIT событий.

person Hadi Brais    schedule 16.01.2021
comment
Здесь недостаточно сериализации выполнения инструкций с lfence? Вам также нужно слить буфер хранилища и все остальное с помощью cpuid? - person Peter Cordes; 16.01.2021
comment
@PeterCordes Это зависит от инструкций в интересующей области. В этом случае prefetcht0 упорядочивается только инструкциями полной сериализации, а не lfence или какой-либо другой инструкцией частичной сериализации. Здесь также имеет значение, очищается SB или нет, потому что более раннее хранилище может быть отключено, но все еще имеет невыполненный запрос с LFB, выделенным во время выполнения предварительной выборки, поэтому попадание в тот же самый LFB может произойти, если случится так, что они к той же строке. Если SB сброшен, такое хранилище принесет строку в L1D, а вместо этого упреждающая выборка попадет в L1D. - person Hadi Brais; 16.01.2021
comment
Я не удивлюсь, если lfence на практике будет достаточно на реальном оборудовании, чтобы заказать prefetcht0, по крайней мере, для определения попадания или отсутствия L1. Но интересный момент о магазине без кеш-памяти, ожидающем прибытия очереди. Однако OP может захотеть обнаруживать случаи, когда неинструментированный код будет видеть промах L1 из предварительной выборки, даже если предварительная выборка будет почти бесполезной, если она не будет запущена значительно до того, как RFO исчезнет. В любом случае, вероятно, не стоит усложнять свой ответ этим. - person Peter Cordes; 16.01.2021
comment
@PeterCordes Да, на практике я согласен с тем, что на существующих процессорах обе rdpmc инструкции могут быть зажаты lfence вместо cpuid (при условии, что вы не хотите заказывать магазины), но мне хотелось воспользоваться руководством здесь. - person Hadi Brais; 16.01.2021
comment
@PeterCordes Я подумал, что для этой цели мне также понадобится lfence перед первым `` rdpmc '', поэтому он не был переупорядочен с любыми другими нагрузками, которые могли бы поразить L1 dcache и потенциально дать ложное срабатывание на prefetcht0. это лишнее? - person Noah; 16.01.2021
comment
@Noah: код примера Хади имеет CPUID (полностью сериализованный) перед первым RDPMC и после последнего RDPMC. Вы можете уменьшить это значение до LFENCE, особенно после некоторого тестирования процессора, который вы хотите измерить, чтобы увидеть, как он себя ведет. Но не нужно даже дополнительных барьеров, чем CPUID. - person Peter Cordes; 16.01.2021
comment
Похоже, что код забыл про CPUID, перезаписывающий EAX..EDX. movl %eax, %edi после CPUID странно; Я думаю, нам нужен результат RDPMC, а не CPUID. Также перед RDPMC необходимо выполнить обнуление xor. (Выбор известного листа CPUID - хорошая идея, чтобы поддерживать постоянную скорость.) - person Peter Cordes; 16.01.2021
comment
@HadiBrais Я пробовал MEM_LOAD_RETIRED.L1_HIT. Кажется, он каждый раз сообщает о попадании в предварительную выборку. Однако он отлично работает для реальной загрузки с адреса. Любые идеи? Я разместил код, который использую для тестирования. - person Noah; 16.01.2021
comment
@ Ной Верно. В документации действительно сказано, что L1_HIT происходит для любых предварительных выборок программного обеспечения, и я также задокументировал это в другом месте для других микроархитектур. Но я забыл об этом, извините. Я обновил ответ, чтобы предложить другой подход. Я не думаю, что есть простой способ сделать это. - person Hadi Brais; 17.01.2021
comment
@HadiBrais по-прежнему не повезло с предварительной выборкой SW, но работает точно так, как ожидалось, с истинной загрузкой. Это действительно удивительно, потому что документация по MEM_LOAD_RETIRED.L1_HIT и L1D_PEND_MISS.PENDING довольно явно включает предварительную выборку SW. Изменил сериализованный на cpuid, но ничего не изменил. Такова жизнь. - person Noah; 17.01.2021
comment
@ Использование L1D_PEND_MISS.PENDING отлично работает на моем Haswell. В любом случае, вы можете попробовать еще один подход - использовать сумму двух событий SWPF_HIT + SWPF_MISS. Смотрите обновление моего ответа. - person Hadi Brais; 18.01.2021
comment
@HadiBrais так L2_RQSTS.SWPF_HIT + L2_RQSTS.SWPF_MISS работает! Большое спасибо! Я до сих пор не понимаю, почему L1D_PEND_MISS.PENDING и MEM_LOAD_RETIRED.L1_HIT мне не подходят. Не могли бы вы опубликовать код, с которым можно L1D_PEND_MISS.PENDING работать? Хочу увидеть, что я напортачил - person Noah; 18.01.2021
comment
@Noah Что касается MEM_LOAD_RETIRED.L1_HIT, в документации сказано: This event includes all SW prefetches and lock instructions regardless of the data source. В нем ясно сказано, что предварительная выборка программного обеспечения учитывается независимо от того, попали ли они в L1D или что-то еще. Что касается L1D_PEND_MISS.PENDING, я использовал ваш код и выполнил точные шаги, упомянутые в моем ответе. Возможно, на Ice Lake изменился способ подсчета этого события. Какие ценности вы получаете на втором этапе? Это нули или что-то еще? Вы уверены, что на четвертом шаге упреждающая выборка не выполняется? - person Hadi Brais; 18.01.2021
comment
@HadiBrais для L1D_PEND_MISS.PENDING Я получаю 0 как за попадания, так и за промахи. Единственный случай, когда я не получаю 0 с предварительной выборкой SW, - это когда я получаю промах DTLB. Я бы подумал, что это ошибка в коде, но при стандартной загрузке я получаю ожидаемые результаты. с MEM_LOAD_RETIRED.L1_HIT я получаю отчет о попаданиях как для попаданий, так и для промахов с предварительной выборкой SW. И снова стандартная загрузка дает ожидаемые результаты. Переместил нагрузку прямо перед встроенным asm. - person Noah; 18.01.2021
comment
@HadiBrais также, поскольку вы получаете ожидаемые результаты с моим кодом, может быть что-то с icelake. (но я, вероятно, более склонен ошибиться, чем разведка). Однако L2_RQSTS.SWPF_HIT + L2_RQSTS.SWPF_MISS работает точно так, как ожидалось. Добавляет около ~ 10 циклов, но по сравнению с сериализацией это немного. - person Noah; 18.01.2021
comment
@Noah При промахе DTLB обходчик страниц действительно проходит тот же путь, что и обычная загрузка (то есть через L1D), и в документации действительно сказано, что L1D_PEND_MISS.PENDING увеличивается для доступа к обходчику страниц, но это не ясно для программной и аппаратной предварительной выборки. Я не думаю, что вы ошиблись. Это тоже не ошибка Ice Lake. Просто способ работы мероприятия немного изменился. Большинство событий не являются архитектурными, и Intel может изменить их точное значение в разных процессорах. - person Hadi Brais; 18.01.2021
comment
@HadiBrais добавил точный код, который я использую для тестирования (используя cpuid для сериализации и сохранения прямо перед asm). #define TODO может быть MISS или HIT. #define DO_PREFETCH uncommened запустит предварительную выборку, в противном случае запустит загрузку по требованию. ev0 можно установить на любое событие для отслеживания, например l1d_pending для L1D_PEND_MISS.PENDING или MEM_LOAD_RETIRED.L1_HIT. - person Noah; 18.01.2021
comment
@HadiBrais Понятно. Хотя в руководстве прямо сейчас (в разделе icelake) формулировки для L1D_PEND_MISS.PENDING и MEM_LOAD_RETIRED.L1_HIT предварительных выборок SW явно разрешены. Странный. Огромное спасибо за помощь! на вашей машине haswell вы сказали, что вам нужно L1D_PEND_MISS.PENDING работать. Вы заставили MEM_LOAD_RETIRED.L1_HIT работать? - person Noah; 18.01.2021