Разница между 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.