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