Разница между rdtscp, RDTSC: memory и cpuid / rdtsc?
Предположим, мы пытаемся использовать tsc для мониторинга производительности, и мы хотим предотвратить переупорядочение инструкций.
вот наши варианты:
1: rdtscp
- это сериализация вызовов. Это предотвращает переупорядочивание вокруг вызова rdtscp.
__asm__ __volatile__("rdtscp; " // serializing read of tsc
"shl ,%%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
. Но!--5--> не сериализуется, поэтому использование его в одиночку не помешает процессору перегруппировав его.
Итак, мы можем использовать любой из этих двух вариантов, чтобы предотвратить переопределение:
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 ,%%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 ,%%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
мое понимание для 3-го варианта выглядит следующим образом:
звонить __volatile__
предотвращает оптимизатор от удаления asm или перемещения его по любым инструкциям, которые могут потребоваться результаты (или изменить входные данные) asm. Однако он все еще может перенести его в отношении несвязанных операций. Так что __volatile__
не хватает.
скажите, что память компилятора блокируется:: "memory")
. The "memory"
clobber означает, что GCC не может делать никаких предположений о содержимом памяти, остающемся неизменным в asm, и, таким образом, не будет переупорядочивать его.
Итак, мои вопросы являются:
- 1: Является ли мое понимание
__volatile__
и"memory"
исправить? - 2: делают ли вторые два вызова то же самое?
- 3: с помощью
"memory"
выглядит намного проще, чем с помощью другой инструкции сериализации. Зачем кому-то использовать 3-й вариант над 2-м вариантом?
2 ответов
как упоминалось в комментарии, есть разница между барьер компилятора и процессор барьер. volatile
и memory
в инструкции asm действуют как барьер компилятора, но процессор по-прежнему свободен для изменения порядка инструкций.
процессор барьер-это специальные инструкции, которые должны быть явно заданы, например,rdtscp, cpuid
память загородки инструкции (mfence, lfence,
...) п.
кроме того, при использовании cpuid
в качестве барьера до rdtsc
распространено, это также может быть очень плохо с точки зрения производительности, так как платформы виртуальных машин часто ловят и эмулируют cpuid
инструкция для того, чтобы наложить общий набор функций процессора на нескольких компьютерах в кластере (чтобы гарантировать, что живая миграция работает). Таким образом, лучше использовать одну из инструкций Memory fence.
ядро Linux использует mfence;rdtsc
на платформах AMD и lfence;rdtsc
на Intel. Если вы не хотите утруждать себя различением эти, mfence;rdtsc
работает на обоих, хотя это немного медленнее, как mfence
является более сильным барьером, чем lfence
.
вы можете использовать его, как показано ниже:
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.