Дисперсия в накладных расходах RDTSC

Я создаю микро-эталон для измерения изменений производительности, поскольку я экспериментирую с использованием встроенных инструкций 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 ответа

Решение

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

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

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

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

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

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

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

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

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;

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

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

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

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

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

Другие вопросы по тегам