Разница между rdtscp, rdtsc: памятью и процессором / rdtsc?

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

Вот наши варианты:

1: rdtscp это сериализованный вызов. Это предотвращает переупорядочение вокруг вызова rdtscp.

__asm__ __volatile__("rdtscp; "         // serializing read of tsc
"shl $32,%%rdx; "  // shift higher 32 bits stored in rdx up
"or %%rdx,%%rax"   // and or onto rax
: "=a"(tsc)        // output to tsc variable
:
: "%rcx", "%rdx"); // rcx and rdx are clobbered

Тем не мение, rdtscp доступно только на новых процессорах. Так что в этом случае мы должны использовать rdtsc, Но rdtsc не сериализован, поэтому его использование не помешает процессору переупорядочить его.

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

2: Это призыв к cpuid а потом rdtsc, cpuid это сериализованный вызов.

volatile int dont_remove __attribute__((unused)); // volatile to stop optimizing
unsigned tmp;
__cpuid(0, tmp, tmp, tmp, tmp);                   // cpuid is a serialising call
dont_remove = tmp;                                // prevent optimizing out cpuid

__asm__ __volatile__("rdtsc; "          // read of tsc
"shl $32,%%rdx; "  // shift higher 32 bits stored in rdx up
"or %%rdx,%%rax"   // and or onto rax
: "=a"(tsc)        // output to tsc
:
: "%rcx", "%rdx"); // rcx and rdx are clobbered

3: Это призыв к rdtsc с memory в списке clobber, который предотвращает переупорядочение

__asm__ __volatile__("rdtsc; "          // read of tsc
"shl $32,%%rdx; "  // shift higher 32 bits stored in rdx up
"or %%rdx,%%rax"   // and or onto rax
: "=a"(tsc)        // output to tsc
:
: "%rcx", "%rdx", "memory"); // rcx and rdx are clobbered
// memory to prevent reordering

Мое понимание третьего варианта таково:

Звонить __volatile__ препятствует тому, чтобы оптимизатор удалил asm или переместил его через любые инструкции, которые могли бы нуждаться в результатах (или изменить входные данные) asm. Однако это все еще может переместить это относительно несвязанных операций. Так __volatile__ недостаточно.

Скажите, что память компилятора забита: : "memory"), "memory" Clobber означает, что GCC не может делать какие-либо предположения о том, что содержимое памяти остается неизменным по всему asm, и, следовательно, не будет переупорядочиваться вокруг него.

Итак, мои вопросы:

  • 1: мое понимание __volatile__ а также "memory" правильный?
  • 2: вторые два вызова делают то же самое?
  • 3: Использование "memory" выглядит намного проще, чем использование другой инструкции сериализации. Зачем кому-то использовать 3-й вариант над 2-й вариант?

54

Решение

Как уже упоминалось в комментарии, есть разница между барьер компилятора и процессорный барьер. volatile а также memory в операторе asm действует как барьер компилятора, но процессор все еще может переупорядочивать инструкции.

Барьер процессора — это специальные инструкции, которые должны быть заданы явно, например, rdtscp, cpuid, память забора инструкции (mfence, lfence, …) так далее.

В сторону, при использовании cpuid как барьер перед rdtsc это часто бывает очень плохо с точки зрения производительности, поскольку платформы виртуальных машин часто перехватывают и эмулируют cpuid инструкция для наложения общего набора функций ЦП на несколько машин в кластере (чтобы убедиться, что живая миграция работает). Таким образом, лучше использовать одну из инструкций по забору памяти.

Ядро Linux использует mfence;rdtsc на платформах AMD и lfence;rdtsc на интеле. Если вы не хотите заниматься различием между ними, mfence;rdtsc работает на обоих, хотя это немного медленнее, как mfence является более сильным барьером, чем lfence,

40

Другие решения

Вы можете использовать его, как показано ниже:

asm volatile (
"CPUID\n\t"/*serialize*/
"RDTSC\n\t"/*read the clock*/
"mov %%edx, %0\n\t""mov %%eax, %1\n\t": "=r" (cycles_high), "=r"(cycles_low):: "%rax", "%rbx", "%rcx", "%rdx");
/*
Call the function to benchmark
*/
asm volatile (
"RDTSCP\n\t"/*read the clock*/
"mov %%edx, %0\n\t""mov %%eax, %1\n\t""CPUID\n\t": "=r" (cycles_high1), "=r"(cycles_low1):: "%rax", "%rbx", "%rcx", "%rdx");

В приведенном выше коде первый вызов CPUID реализует барьер, чтобы избежать неупорядоченного выполнения инструкций выше и ниже инструкции RDTSC. С помощью этого метода мы избегаем вызова инструкции CPUID между чтениями регистров реального времени.

Первый RDTSC затем считывает регистр метки времени, и значение сохраняется в
объем памяти. Затем выполняется код, который мы хотим измерить. Инструкция RDTSCP во второй раз считывает регистр метки времени и гарантирует, что выполнение всего кода, который мы хотели измерить, завершено. Следующие две инструкции «mov» сохраняют значения регистров edx и eax в памяти. Наконец, вызов CPUID гарантирует, что барьер будет снова реализован, так что невозможно, чтобы любая последующая инструкция выполнялась до самого CPUID.

5