2015-03-22 1 views
-1

Я хочу, чтобы время вызова функции с помощью rdtsc. Поэтому я измерил его двумя способами следующим образом.rdtsc время для измерения функции

  1. Назовите его в цикле. Совокупность каждой разности rdtsc внутри цикла и деление на количество вызовов. (Предположим, что это N)
  2. Назовите его в цикле. Получите разность rdtsc самого цикла и разделите его на N.

Но я вижу пару непоследовательных поведений.

  1. Когда я увеличиваю N, времена становятся уменьшенными монотонно в обоих методах 1 и 2. Для метода 2 это понятно, поскольку оно амортизирует контрольные надстройки цикла. Но я не уверен, как это делается для метода 1.
  2. Фактически для метода 2 каждый раз, когда я увеличиваю N, значение, которое я получаю для N = 1, кажется, просто делится на новый N каждый раз. Проверка разборки gdb заставила меня понять, что это некоторая оптимизация компилятора на -O2, где цикл пропускается во втором случае. Поэтому я повторил с -O0, где разборка gdb показывает, что фактический цикл существует и для второго случая.

код приведен ниже.

#include <stdio.h> 
    #include <inttypes.h> 
    #include <stdlib.h> 

    typedef unsigned long long ticks; 

    static __inline__ ticks getticks(void) { 
     unsigned a, d; 
     asm volatile("rdtsc" : "=a" (a), "=d" (d)); 
     return ((ticks)a) | (((ticks)d) << 32); 
    } 

    __attribute__ ((noinline)) 
    void bar() { 

    } 

    int main(int argc, char** argv) { 

     long long N = 1000000; 
     N = atoi(argv[1]); 
     int i; 
     long long bar_total = 0; 

     ticks start = 0, end = 0; 

     for (i = 0; i < N; i++) { 
     start = getticks(); 
     bar(); 
     end = getticks(); 
     bar_total += (end - start); 
     } 

     fprintf(stdout, "Total invocations : %lld\n", N); 
     fprintf(stdout, "[regular] bar overhead : %lf\n", ((double)bar_total/ N)); 

     start = getticks(); 
     for (i = 0; i < N; i++) { 
     bar(); 
     } 
     end = getticks(); 

     bar_total = (end - start); 

     fprintf(stdout, "[Loop] bar overhead : %lf\n", ((double)bar_total/ N)); 

     return 0; 

    } 

Любая идея, что здесь происходит? Я также могу установить разборку gdb, если это необходимо. Я использовал реализацию RDTSC от http://dasher.wustl.edu/tinker/distribution/fftw/kernel/cycle.h

Edit: Я буду иметь, чтобы убрать мое второе утверждение, что в -O0 время потеряется прямо пропорциональна N во втором случае. Я предполагаю, что это была некоторая ошибка, которую я совершил во время сборки, в результате чего сохранилась старая версия. Любой, как он все еще опускается несколько вместе с рисунком для метода 1. Здесь приведены некоторые числа для разных значений N.

taskset -c 2 ./example.exe 1 
Total invocations : 1 
[regular] bar overhead : 108.000000 
[Loop] bar overhead : 138.000000 

taskset -c 2 ./example.exe 10 
Total invocations : 10 
[regular] bar overhead : 52.900000 
[Loop] bar overhead : 40.700000 

taskset -c 2 ./example.exe 100 
Total invocations : 100 
[regular] bar overhead : 46.780000 
[Loop] bar overhead : 15.570000 

taskset -c 2 ./example.exe 1000 
Total invocations : 1000 
[regular] bar overhead : 46.069000 
[Loop] bar overhead : 13.669000 

taskset -c 2 ./example.exe 100000 
Total invocations : 10000 
[regular] bar overhead : 46.010100 
[Loop] bar overhead : 13.444900 

taskset -c 2 ./example.exe 100000000 
Total invocations : 100000000 
[regular] bar overhead : 26.970272 
[Loop] bar overhead : 5.201252 

taskset -c 2 ./example.exe 1000000000 
Total invocations : 1000000000 
[regular] bar overhead : 18.853279 
[Loop] bar overhead : 5.218234 

taskset -c 2 ./example.exe 10000000000 
Total invocations : 1410065408 
[regular] bar overhead : 18.540719 
[Loop] bar overhead : 5.216395 

Сейчас я вижу два новых поведения.

  1. Метод 1 сходится медленнее, чем метод 2. Но все же я озадачен тем, почему существует такая резкая разница в значениях для разных параметров N. Возможно, я делаю какую-то основную ошибку, которую я сейчас не вижу.
  2. Значение метода 1 на самом деле больше, чем метод 2 с некоторым запасом. Я ожидал, что он будет на уровне или немного меньше значения метода 2, так как он не содержит накладных расходов на управление контуром.

Вопросы

Таким образом, в итоге мои вопросы

  1. Почему значения, приведенные обоими методами изменить так резко при увеличении N? Специально для метода 1, который не учитывает служебные данные управления циклом.

  2. Почему второй результат метода меньше, чем первый метод, когда первый метод исключает издержки управления циклом в вычислениях?

Edit 2

Что касается предложенного rdtscp решения.

Будучи непосвященным о встроенном сборке, я сделал следующее.

static __inline__ ticks getstart(void) { 
    unsigned cycles_high = 0, cycles_low = 0; 
    asm volatile ("CPUID\n\t" 
      "RDTSC\n\t" 
      "mov %%edx, %0\n\t" 
      "mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low):: 
      "%rax", "%rbx", "%rcx", "%rdx"); 
    return ((ticks)cycles_high) | (((ticks)cycles_low) << 32); 
} 

static __inline__ ticks getend(void) { 
    unsigned cycles_high = 0, cycles_low = 0; 
    asm volatile("RDTSCP\n\t" 
     "mov %%edx, %0\n\t" 
      "mov %%eax, %1\n\t" 
      "CPUID\n\t": "=r" (cycles_high), "=r" (cycles_low):: 
      "%rax", "%rbx", "%rcx", "%rdx"); 
    return ((ticks)cycles_high) | (((ticks)cycles_low) << 32); 
} 

и использованные выше методы до и после вызова функции. Но теперь я получаю нечувствительные результаты, как следует.

Total invocations : 1000000 
[regular] bar overhead : 304743228324.708374 
[Loop] bar overhead : 33145641307.734016 

Какой улов? Я хотел отбросить их как встроенные методы, так как я вижу их использование в нескольких местах.

A. Решение в комментариях.

+0

Можете ли вы опубликовать вывод программы для нескольких N? – osgx

+0

Добавлены некоторые выходы для разных значений N. Пожалуйста, см. Также мою ретракцию. – chamibuddhika

+0

Спасибо. Какой у вас процессор? И в чем вопрос? Вы должны изменить свою функцию rdtsc, чтобы включить 'cpuid' или другую инструкцию по сериализации - проверить https://www.ccsl.carleton.ca/~jamuir/rdtscpm1.pdf page 3« 3.1. Внеочередное исполнение »; или переключиться на 'rdtscp': http://stackoverflow.com/questions/12631856/difference-between-rdtscp-rdtsc-memory-and-cpuid-rdtsc?rq=1 – osgx

ответ

1

Вы используете обычную команду rdtsc, которая может работать некорректно на процессорах вне порядка, таких как Xeons и Cores. Вы должны добавить некоторые инструкции сериализации или переключатель rdtscp instruction:

http://en.wikipedia.org/wiki/Time_Stamp_Counter

Начиная с Pro, процессоры Pentium Intel поддержали внеочередное исполнение, где инструкции не обязательно выполняются в порядке их появления в исполняемом файле. Это может привести к тому, что RDTSC будет выполнен позже, чем ожидалось, что приведет к ошибочному подсчету цикла. [3] Эту проблему можно решить, выполнив инструкцию сериализации, такую ​​как CPUID, чтобы принудительно завершить каждую предыдущую команду, прежде чем разрешить программе продолжить работу, или с помощью команды RDTSCP, которая является вариантом сериализации инструкции RDTSC.

Intel имеет недавнее руководство по эксплуатации с использованием RDTSC/rdtscp - How to Benchmark Code Execution Times on Intel IA-32 and IA-64 Instruction Set Architectures (IA-32-IA-64 бенчмарк-код-выполнения-paper.pdf, 324264-001, 2010). Они рекомендуют CPUID + RDTSC для запуска и rdtscp для конечных таймеров:

Решение проблемы, представленной в разделе 0, чтобы добавить команду CPUID только после RDTPSCP и двух mov инструкций (хранить в памяти значение edx и eax). Реализация выглядит следующим образом:

asm volatile ("CPUID\n\t" 
"RDTSC\n\t" 
"mov %%edx, %0\n\t" 
"mov %%eax, %1\n\t": "=r" (cycles_high), "=r" (cycles_low):: 
"%rax", "%rbx", "%rcx", "%rdx"); 
/***********************************/ 
/*call the function to measure here*/ 
/***********************************/ 
asm volatile("RDTSCP\n\t" 
"mov %%edx, %0\n\t" 
"mov %%eax, %1\n\t" 
"CPUID\n\t": "=r" (cycles_high1), "=r" (cycles_low1):: 
"%rax", "%rbx", "%rcx", "%rdx"); 

start = (((uint64_t)cycles_high << 32) | cycles_low); 
end = (((uint64_t)cycles_high1 << 32) | cycles_low1); 

В приведенной выше коде первого CPUID вызова реализует барьер, чтобы избежать испорченного выполнения указанной выше и ниже RDTSC инструкции инструкции. Тем не менее, этот вызов не влияет на измерение, так как он доходит до RDTSC (то есть, прежде чем будет зарегистрирован регистр метки времени). Первый RDTSC затем считывает регистр метки времени, и значение сохраняется в памяти . Затем выполняется код, который мы хотим измерить. Если код является вызовом функции , рекомендуется объявить такую ​​функцию как «inline», чтобы с точки зрения сборки не было накладных расходов при вызове самой функции. Инструкция RDTSCP считывает регистр временной метки во второй раз, а гарантирует, что выполнение всего кода, который мы хотим измерить, завершено.

Вы, в пример, не очень корректны; вы пытаетесь измерить пустую функцию bar(), но она настолько коротка, что вы измеряете служебные данные rdtsc в методе 1 (for() { rdtsc; bar(); rdtsc)). Согласно таблице Agner Fog для haswell - http://www.agner.org/optimize/instruction_tables.pdf страница 191 (длинный стол «Intel Haswell Список таймингов команды и пробой μop», в самом конце) RDTSC имеет 15 uops (без возможности слияния) и латентность 24 тиков ; RDTSCP (для более старой микроархитектуры Sandy Bridge имеет 23 UOP и 36 тиков латентности против 21 uops и 28 тиков для rdtsc). Таким образом, вы не можете использовать простой rdtsc (или rdtscp) для непосредственного измерения такого короткого кода.

+0

Интересно. Я попробую это и отчитаю. Кстати, просто чтение сборника не было бы двумя ходами сразу после включения rdtscp в такт? – chamibuddhika

+0

chamibuddhika, они будут запущены, и они будут использовать два или три исполнительных порта процессора; но в широких суперскалярных ходах cpu из-за порядка почти не повлияет на ваш результат - они не мешают вашим инструкциям запускаться CPU. – osgx

+0

См. Мое второе редактирование. Спасибо за информационные материалы. – chamibuddhika

1

Вы попробовали clock_gettime(CLOCK_MONOTONIC, &tp)? Должно быть достаточно близко к чтению счетчика циклов вручную, также имейте в виду, что счетчик циклов может не синхронизироваться между ядрами процессора.

+1

Нет, нет. Я тоже попробую. Я использовал набор задач, чтобы связать процесс, чтобы предотвратить такой случай, поэтому я предполагаю, что это учитывается. – chamibuddhika

+0

[Я думаю] (http://stackoverflow.com/questions/3388134/rdtsc-accuracy-across-cpu-cores), что tsc не будет синхронизироваться только на некоторых более старых NUMA (multi-socket) материнских платах; Linux пытается синхронизировать их: http://lxr.free-electrons.com/source/arch/x86/kernel/tsc_sync.c. И tsc рассчитывает не циклы процессора, а некоторый монотонный таймер («Invariant TSC»). PS: а что насчет 'CLOCK_MONOTONIC_RAW'? – osgx