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

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.