Подтвердить что ты не робот

Разница в накладных расходах 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)

Мои вопросы таковы:

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

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

Платформа:

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

SpeedStep отключен (процессор настроен на режим работы и работает на частоте 2,4 ГГц); если работает в режиме ondemand, я получаю два пика при 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
4b9b3361

Ответ 1

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

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

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

Ответ 2

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

Ответ 3

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

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

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

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

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

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