Все знают и любят ассемблер x86. Большинство его инструкций современный процессор исполняет за единицы или доли наносекунд. Некоторые операции, которые декодируются в длинную последовательность микрокода, или ожидающие доступа к памяти могут исполняться намного дольше — до сотен наносекунд. Этот пост — о рекордсменах. Хит парад из четырех инструкций под катом, но для тех, кому лень читать весь текст, я напишу здесь, что главный злодей — [memory]++ при определенных условиях.

image

КПДВ взята из документа Агнера Фога, который, наряду с двумя документами от Intel (optimization guide и architecture software development manual) содержат много полезного и интересного по теме.

Начну с того, что есть команды, которые, ожидаемо, исполняются в течение микросекунд. Например, IN, OUT или RSM (возврат из SMM). VMEXIT очень сильно ускорился за последние годы, и на новых процессорах длится доли микросекунды. Есть MWAIT, которая по определению исполняется насколько возможно долго. Вообще, в ринг 0 есть много «тяжелых» инструкций, сплошь состоящих из микрокода — WRMSR, CPUID, установка контрольных регистров и т.д. Примеры, которые я приведу ниже, могут исполняться с привилегиями ринг 3, то есть в любой обычной программе. Даже на С программировать не обязательно — виртуальные машины некоторых популярных языков способны генерировать код, содержащий эти операции. Это не какие-то специальные команды процессора, а обычные инструкции, иногда в особых условиях.

Так как исполняются они долго, то любой вменяемый профилировщик их обнаружит обычной профилировкой по времени, если, конечно, они встречаются в достаточно «горячем» коде. Еще бывают отдельные счетчики производительности (регистры PMU), которые реагируют исключительно на подобные случаи, с их помощью можно найти эти операции в большой программе, даже если они не занимают много абсолютного времени (только зачем?). Самые популярные инструменты для этого — Vtune и Linux perf. Также можно воспользоваться PCM, но он не покажет, где находится инструкция.

Злодей номер четыре. Команда x86 (на самом деле, x87), которая может исполняться почти 700 тактов — FYl2X. Вычисляет двоичный логарифм, умноженный на второй операнд. В SSE ее аналога нет, поэтому до сих пор встречается в природе. Особенного счетчика нет.

Злодей номер три. Возможно, немного искусственный пример, но используется часто. К счастью, в основном, в драйверах.
Команда MFENCE (или ее подмножества — LFENCE, SFENCE. Кстати, LFENCE + SFENCE != MFENCE). Если до MFENCE выполнялась длинная операция с памятью или PCIe write, например, операция с non-temporal (MOVNTI, MOVNTPS, MASKMOVDQU и т.д.) или с операндом, находящимся в write through/write combined области памяти, то сам «забор» будет исполняться почти микросекунду или дольше. Счетчик производительности для этой ситуации существует, но находится не в ядре процессора, а в «uncore», с ним проще работать через PCM.

Злодей номер два. Вот очень простой код.
double fptest = 3000000000.0f; // Same with float.
//TSC1
int inttest = 2 + fptest;
//TSC2
time = TSC2 - TSC1;

Как вы думаете, чему примерно будет равно time? (Не важно, скомпилируется этот код в x87 или скалярный SSE). Исполняться эта единственная инструкция будет 1-2 микросекунды. Это так называемая denormal операция, особый случай, обрабатываемый длинной последовательностью микрокода. Ловится легко, регистр PMU — счетчик производительности называется FP_ASSIST.ALL. Кстати, совершенно очевидно, что измерять разницу TSC при исполнении одной (или даже нескольких десятков) инструкций почти всегда бессмысленно. Этот случай — исключение, мы меряем длинный микрокод.

Главный злодей.
static unsigned char array[128];
for (int i = 0; i < 64; i++) if ((int)(array + i) % 64 == 63) break;
lock = (unsigned int*)(array + i);
for (i = 0; i < 1024; i++) *(lock)++; // prime
// TSC1
   asm volatile (
    "lock xaddl %1, (%0)\n"
    : // no output
    : "r" (lock), "r" (1));
   // or in Windows, just InterlockedIncrement(lock);
// TSC2
time = TSC2 - TSC1;

Ну и бонус — в отличие от других участников хит парада, этот код заставит все остальные ядра тоже остановиться на перекур на срок в несколько тысяч тактов.
Это тоже ловится при помощи Vtune, perf, PCM и т.д. при помощи счетчика LOCK_CYCLES.SPLIT_LOCK_UC_LOCK_DURATION. Пример может показаться надуманным, но за последний год я встречал эту проблему у своих клиентов два раза. В одном из случаев LOCK_CYCLES.SPLIT_LOCK_UC_LOCK_DURATION зашкаливал при инициализации огромной программы, написанной на .net. Я тогда так и не разобрался, рантайм или код клиента расположил мутекс так неудачно, но производительность проседала серьезно — другая, независимая программа, работающая на другом ядре, замедлялась в тридцать раз.

Кто-нибудь знает еще более медленную инструкцию? (REP MOV не предлагать).

Комментарии (27)


  1. amarao
    22.05.2015 22:51

    Насколько я понимаю, что с того момента, когда Intel похоронила SMP, самые дорогие операции — это операции связанные с инвалидацией кешей у «соседей».


    1. izard Автор
      22.05.2015 23:39

      Да, выше уже написали, что еще это может быть особенно дорого на Xeon-EX, там NUMA особенно злая.


  1. acDev
    25.05.2015 11:14

    А какова разница «по скорости» между LOCK CMPXCHG и LOCK XADD?


    1. izard Автор
      08.06.2015 14:26

      Если с splitlock как в примере, то несущественная. Если без сплитлока, надо измерять, не знаю так.