Разница между 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
. Но 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 ,%%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")
. Элемент "memory"
clobber означает, что GCC не может делать никаких предположений о том, что содержимое памяти остается неизменным в asm, и поэтому не будет переупорядочивать его.
так что мои вопросы являются:
- 1: Это мое понимание
__volatile__
и"memory"
исправить? - 2: делают ли два вторых вызова то же самое?
- 3: с помощью
"memory"
выглядит гораздо проще, чем с помощью другой инструкции сериализации. Зачем кому-то использовать 3-й вариант над 2-м вариантом?
2 ответа:
как уже упоминалось в комментариях, есть разница между барьер компилятора и процессор барьер.
volatile
иmemory
в инструкции asm действует как барьер компилятора, но процессор по-прежнему может переупорядочивать инструкции.процессор барьер-это специальные инструкции, которые должны быть явно заданы, например,
rdtscp, cpuid
, инструкции забора памяти (mfence, lfence,
...) прием.кроме того, при использовании
cpuid
как барьер раньшеrdtsc
является общим, это также может быть очень плохо с точки зрения производительности, так как платформы виртуальных машин часто ловушки и эмулироватьcpuid
инструкция для того, чтобы наложить общий набор функций ЦП на нескольких машинах в кластере (чтобы гарантировать, что динамическая миграция работает). Таким образом, лучше использовать одну из инструкций забора памяти.ядро 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.