company_banner

Самая медленная инструкция x86

    Все знают и любят ассемблер 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 не предлагать).
    Intel
    224.32
    Company
    Share post

    Comments 27

      +3
      Злодей номер два.


      Не могу повторить (Intel B960).
      Вот этот код выполняется приблизительно одинаковое время при любом значении в fptest.

        for (int b = 0; b < 100000000; b++)
        {
          a += int(2 + fptest);
      00231020  movsd       xmm0,mmword ptr ds:[233018h]  
      00231028  addsd       xmm0,xmm1  
      0023102C  cvttsd2si   ecx,xmm0  
      00231030  add         edi,ecx  
      00231032  dec         edx  
      00231033  jne         main+20h (0231020h)  
        }
      

        +3
        Сейчас проверю последний раз этот пример я запускал 2 года назад. Наверное, -fast-math включился, он отбрасывает denormals. Но код вроде правильный сгенерился.
          +2
          У меня были примеры с денормалами. И если на SB & YB они нормально тормозили, то на haswell все стало быстро как с нормальными числами.
            +1
            да, я что-то код сверху не могу заставить тормозить, исполняю как раз на HSW. Схожу в лабу на SNB проверю, как раз выдран с реального кода на SNB.
              +2
              Ну еще совсем простой пример. David Dice рассказывал про случай, когда просто доступ к чужой NUMA пямяти на 8-сокетной системе (за 2 хопа) занимал > 1000 тактов.
                0
                О, а вы в лабу ногами ходите?! :)
                  0
                  Только чтобы подключить старое железо — старше broadwell и skylake, или редкое/мелкое. Весь зоопарк постоянно держать подключеным места не хватает, да и зачем — очень редко нужно.
          0
          div [memory]?
          Ну хотя тут десятки тактов, даже если кэш-промах.
          Еще вариант — syscall / sysenter. Там сотни тактов, емнип.
            0
            Да, десятки и сотни, все примеры из статьи кроме fy2x — тысячи.
            +1
            FYl2X — я так понимаю, параллелится с обычными инструкциями на ура?
              +3
              Эта инструкция декодируется в длинный микрокод, так что нет (то есть этот микрокод, понятно, ипользует ILP внутри себя, но следующие инструкции ждут).
                0
                Хм. А я думал что FP ядро слегка независимо от целочисленного. Отстал от жизни на пять поколений процессоров, видимо :D
                  +2
                  В Atom еще почти независимое. Но даже там в этом микрокоде полно load/store, которые занимают обычные порты. а в HSW core просто 8 портов, некоторые содержат в том числе execution units, работающие с fp.
                    +1
                    То есть старый трюк «бесплатной» плавающей точки исчез в результате интеграции, зато большинство инструкций стало просто не настолько медленные, так?
                  0
                  Неужели логарифм по основанию 2 так долго считается? Есть ли аналоги для расчёта логарифма? AVX и SSE содержат что-либо для расчёта тригонометрических и экспоненциальных функций? Или только приблизительно можно считать?
                    0
                    AVX и SSE содержат достаточно простые инструкции. Скорее всего, если на них написать свой логарифм, может быть быстрее.
                0
                Неужели эти инструкции медленнее чем WBINVD?
                  +2
                  WBINV надо быть в ring 0, ее неожиданно в пользовательском коде оказаться не может. Кстати, она сама не очень медленная, тормоза начинаются потом, когда оказывается что кэш пустой.
                    +1
                    Вы правы. Остается тогда только CLFLUSH, а он уже не такой медленный.
                    0
                    А вот вопрос, зачем ты в последнем примере делаешь цикл на 8К? ;)
                    Сдается мне, что тут ты сгущаешь краски ;)
                      +2
                      А, это от vtune осталось, иначе ивенты не ловились. Конечно можно один раз померить, будет несколько тысяч циклов. Спасибо, поправлю.
                      +1
                      этот код заставит все остальные ядра тоже остановиться на перекур на срок в несколько тысяч тактов


                      Как это будет себя вести в VM? Можно заДОСсить соседей по VM?
                        0
                        Можно, даже если другие VM работают на других ядрах. Но для многопроцессорного сервера — только соседей по процессору. VM можно запрограмировать это ловить и давать таким гостям совсем мало тактов, но вроде это нигде пока не реализовано.
                        0
                        Насколько я понимаю, что с того момента, когда Intel похоронила SMP, самые дорогие операции — это операции связанные с инвалидацией кешей у «соседей».
                          0
                          Да, выше уже написали, что еще это может быть особенно дорого на Xeon-EX, там NUMA особенно злая.
                          0
                          А какова разница «по скорости» между LOCK CMPXCHG и LOCK XADD?
                            0
                            Если с splitlock как в примере, то несущественная. Если без сплитлока, надо измерять, не знаю так.

                          Only users with full accounts can post comments. Log in, please.