Предположим, мы пытаемся использовать 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, и, следовательно, не будет переупорядочиваться вокруг него.
Итак, мои вопросы:
__volatile__
а также "memory"
правильный?"memory"
выглядит намного проще, чем использование другой инструкции сериализации. Зачем кому-то использовать 3-й вариант над 2-й вариант?Как уже упоминалось в комментарии, есть разница между барьер компилятора и процессорный барьер. volatile
а также memory
в операторе asm действует как барьер компилятора, но процессор все еще может переупорядочивать инструкции.
Барьер процессора — это специальные инструкции, которые должны быть заданы явно, например, rdtscp, cpuid
, память забора инструкции (mfence, lfence,
…) так далее.
В сторону, при использовании cpuid
как барьер перед rdtsc
это часто бывает очень плохо с точки зрения производительности, поскольку платформы виртуальных машин часто перехватывают и эмулируют cpuid
инструкция для наложения общего набора функций ЦП на несколько машин в кластере (чтобы убедиться, что живая миграция работает). Таким образом, лучше использовать одну из инструкций по забору памяти.
Ядро Linux использует mfence;rdtsc
на платформах AMD и lfence;rdtsc
на интеле. Если вы не хотите заниматься различием между ними, 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.