Дисперсия в накладных расходах RDTSC - PullRequest
13 голосов
/ 22 июня 2011

Я создаю микро-эталон для измерения изменений производительности, поскольку я экспериментирую с использованием встроенных инструкций SIMD в некоторых примитивных операциях обработки изображений.Однако написание полезных микро-тестов затруднительно, поэтому я хотел бы сначала понять (и, если возможно, устранить) как можно больше источников вариаций и ошибок.

Один фактор, который я должен учитывать, - этонакладные расходы самого кода измерения.Я измеряю с помощью RDTSC и использую следующий код, чтобы найти накладные расходы на измерения:

extern inline unsigned long long __attribute__((always_inline)) rdtsc64() {
    unsigned int hi, lo;
        __asm__ __volatile__(
            "xorl %%eax, %%eax\n\t"
            "cpuid\n\t"
            "rdtsc"
        : "=a"(lo), "=d"(hi)
        : /* no inputs */
        : "rbx", "rcx");
    return ((unsigned long long)hi << 32ull) | (unsigned long long)lo;
}

unsigned int find_rdtsc_overhead() {
    const int trials = 1000000;

    std::vector<unsigned long long> times;
    times.resize(trials, 0.0);

    for (int i = 0; i < trials; ++i) {
        unsigned long long t_begin = rdtsc64();
        unsigned long long t_end = rdtsc64();
        times[i] = (t_end - t_begin);
    }

    // print frequencies of cycle counts
}

При запуске этого кода я получаю вывод, подобный этому:

Frequency of occurrence (for 1000000 trials):
234 cycles (counted 28 times)
243 cycles (counted 875703 times)
252 cycles (counted 124194 times)
261 cycles (counted 37 times)
270 cycles (counted 2 times)
693 cycles (counted 1 times)
1611 cycles (counted 1 times)
1665 cycles (counted 1 times)
... (a bunch of larger times each only seen once)

Вот мои вопросы:

  1. Каковы возможные причины бимодального распределения счетчиков циклов, генерируемых приведенным выше кодом?
  2. Почемусамое быстрое время (234 цикла) происходит лишь несколько раз - какое весьма необычное обстоятельство может уменьшить число?

Дополнительная информация

Платформа:

  • Linux 2.6.32 (Ubuntu 10.04)
  • g ++ 4.4.3
  • Core 2 Duo (E6600);имеет постоянную скорость TSC.

SpeedStep выключен (процессор установлен в режим производительности и работает на частоте 2,4 ГГц);если я работаю в режиме «по требованию», я получаю два пика при 243 и 252 циклах и два (предположительно соответствующих) пика при 360 и 369 циклах.

Я использую sched_setaffinity, чтобы привязать процесс к одномуядро.Если я запускаю тест на каждом ядре по очереди (то есть блокирую на ядро ​​0 и запускаю, затем блокирую на ядро ​​1 и запускаю), я получаю аналогичные результаты для двух ядер, за исключением того, что самое короткое время из 234 циклов имеет тенденцию происходить незначительнона ядре 1 меньше, чем на ядре 0.

Команда компиляции:

g++ -Wall -mssse3 -mtune=core2 -O3 -o test.bin test.cpp

Код, сгенерированный GCC для цикла ядра:

.L105:
#APP
# 27 "test.cpp" 1
    xorl %eax, %eax
    cpuid
    rdtsc
# 0 "" 2
#NO_APP
    movl    %edx, %ebp
    movl    %eax, %edi
#APP
# 27 "test.cpp" 1
    xorl %eax, %eax
    cpuid
    rdtsc
# 0 "" 2
#NO_APP
    salq    $32, %rdx
    salq    $32, %rbp
    mov %eax, %eax
    mov %edi, %edi
    orq %rax, %rdx
    orq %rdi, %rbp
    subq    %rbp, %rdx
    movq    %rdx, (%r8,%rsi)
    addq    $8, %rsi
    cmpq    $8000000, %rsi
    jne .L105

Ответы [ 3 ]

7 голосов
/ 22 июня 2011

RDTSC может возвращать противоречивые результаты по ряду причин:

  • На некоторых процессорах (особенно на некоторых старых Opteron) TSC не синхронизируется между ядрами. Похоже, вы уже справляетесь с этим, используя sched_setaffinity - хорошо!
  • Если прерывание таймера ОС сработает во время работы вашего кода, будет введена задержка во время его выполнения. Нет практического способа избежать этого; просто выбрасывайте необычно высокие значения.
  • Конвейерные артефакты в ЦП иногда могут сбить вас на несколько циклов в любом направлении в узкие петли. Вполне возможно, что некоторые циклы выполняются с нецелым числом тактов.
  • Кэш! В зависимости от капризов кеша процессора, операции с памятью (например, запись в times[]) могут различаться по скорости. В этом случае вам повезло, что используемая реализация std::vector - это просто плоский массив; даже при том, что запись может скинуть вещи. Это, вероятно, самый важный фактор для этого кода.

Мне не хватает гуру в микроархитектуре Core2, чтобы точно сказать, почему вы получаете этот бимодальный дистрибутив или как ваш код работает быстрее в эти 28 раз, но, вероятно, это связано с одной из приведенных выше причин. .

2 голосов
/ 14 декабря 2011

Руководство по программированию Intel рекомендует использовать lfence;rdtsc или rdtscp, если вы хотите убедиться, что инструкции до rdtsc действительно выполнялись.Это потому, что rdtsc сама по себе не является командой сериализации.

1 голос
/ 17 августа 2011

Вы должны убедиться, что функция регулирования частоты / зеленый отключена на уровне ОС.Перезагрузите машину.В противном случае может возникнуть ситуация, когда ядра имеют несинхронизированные значения счетчика меток времени.

Чтение 243 является наиболее распространенным, что является одной из причин его использования.С другой стороны, предположим, что у вас есть истекшее время <249: вы вычитаете накладные расходы и получаете недостаточное количество.Так как арифметика без знака, вы получите огромный результат.Этот факт говорит о том, что вместо этого используется самое низкое значение (243).Чрезвычайно сложно точно измерить последовательности длиной всего несколько циклов.На типичном x86 с частотой несколько ГГц я бы рекомендовал использовать временные последовательности короче 10 нс, и даже при такой длине они, как правило, будут далеки от идеального. </p>

Остальная часть моего ответа - то, что я делаю,как я оперирую результатами и моими рассуждениями по предмету.

Что касается накладных расходов, проще всего использовать такой код, как этот

unsigned __int64 rdtsc_inline (void);
unsigned __int64 rdtsc_function (void);

Первая форма выдает инструкцию rdtsc всгенерированный код (как в вашем коде).Второй вызовет вызов функции, выполнение rdtsc и инструкцию возврата.Возможно, он будет генерировать кадры стека.Очевидно, что вторая форма намного медленнее первой.

Затем можно написать код (C) для расчета накладных расходов

unsigned __int64 start_cycle,end_cycle;    /* place these @ the module level*/

unsigned __int64 overhead;

/* place this code inside a function */

start_cycle=rdtsc_inline();
  end_cycle=rdtsc_inline();
overhead=end_cycle-start_cycle;

Если вы используете встроенный вариант, вы получите низкий(э) накладные расходы.Вы также рискуете рассчитать накладные расходы, которые больше, чем «должно» быть (особенно для функциональной формы), что, в свою очередь, означает, что если вы измеряете очень короткие / быстрые последовательности, вы можете столкнуться с ранее рассчитанными накладными расходами, которыебольше, чем само измерение.Когда вы пытаетесь приспособиться к накладным расходам, вы получите недостаточный уровень, который приведет к грязным условиям.Лучший способ справиться с этим состоит в том, чтобы

  1. время накладных расходов несколько раз и всегда использовать наименьшее достигнутое значение,
  2. не измерять действительно короткие последовательности кода, поскольку вы можете столкнуться с эффектами конвейерной обработки, которыепотребуются грязные инструкции синхронизации перед инструкцией rdtsc и
  3. , если вам нужно измерить очень короткие последовательности, рассматривайте результаты как индикаторы, а не как факты

Я ранее обсуждал, что я делаю срезультаты в этой теме .

Другая вещь, которую я делаю, - это интеграция измерительного кода в приложение.Накладные расходы незначительны.После вычисления результата я отправляю его в специальную структуру, где подсчитываю количество измерений, суммирую значения x и x ^ 2 и определяю минимальные и максимальные измерения.Позже я могу использовать данные для расчета среднего и стандартного отклонения.Сама структура индексируется, и я могу измерять различные аспекты производительности, такие как отдельные функции приложения («функциональная производительность»), время, затрачиваемое на процессор, чтение / запись на диск, чтение / запись по сети («нефункциональная производительность») и т. Д.

Если приложение инструментируется таким образом и контролируется с самого начала, я ожидаю, что риск возникновения проблем с производительностью в течение срока его службы будет значительно снижен.

...