Разница между rdtscp, rdtsc: памятью и cpuid/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"). Clobber "memory" означает, что GCC не может делать какие-либо предположения о том, что содержимое памяти остается неизменным в asm и, следовательно, не будет изменять порядок вокруг него.

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

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

Ответ 1

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

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

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

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

Ответ 2

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

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.