Почему медленная инструкция цикла? Не удалось ли Intel эффективно внедрить его?

LOOP (запись в ручном режиме Intel ref) уменьшает ecx/rcx, а затем перескакивает, если отличная от нуля. Это медленное, но не могло ли Intel дешево сделать это быстро? dec/jnz уже макро-предохранители в один uop на семье Sandybridge; с той лишь разницей, что это устанавливает флаги.

loop на различных микроархитектурах, из таблиц инструкций Agner Fog:

  • K8/K10: 7 m-ops
  • Bulldozer-family/Ryzen: 1 m-op (jecxz же стоимость, как и макро-сплавленная тестовая ветвь или jecxz)

  • P4: 4 uops (то же, что и jecxz)

  • P6 (PII/PIII): 8 часов
  • Pentium M, Core2: 11 uops
  • Nehalem: 6 часов. (11 для loope/loopne). Пропускная способность = 4c (loop) или 7c (loope/ne).
  • SnB-family: 7 uops. (11 для loope/loopne). Пропускная способность = один на 5 циклов, как большая часть узкого места, так как ваш счетчик циклов в памяти! jecxz - это всего 2 выхода с одинаковой пропускной способностью, как обычный jcc
  • Silvermont: 7 часов
  • AMD Jaguar (маломощный): 8 часов, пропускная способность 5 с
  • Через Nano3000: 2 uops

Не могли ли декодеры просто декодировать то же, что и lea rcx, [rcx-1]/jrcxz? Это будет 3 часа. По крайней мере, это будет иметь место без префикса размера адреса, в противном случае он должен использовать ecx и обрезать RIP для EIP если EIP переход; возможно, странный выбор размера адреса, управляющего шириной декремента, объясняет многие ошибки?

Или лучше, просто расшифруйте его как плавкую де-и-ветку, которая не устанавливает флаги? dec ecx/jnz на SnB декодирует до одного uop (который устанавливает флаги).

Я знаю, что настоящий код не использует его (потому что он был медленным, так как по крайней мере P5 или что-то еще), но AMD решила, что стоило бы быстро сделать это для Bulldozer. Наверное, потому что это было легко.


  • Было бы легко, чтобы уарм семейства SnB имел быструю loop? Если да, то почему? Если нет, то почему это сложно? Много транзисторов декодера? Или дополнительные биты в объединенном dec & branch uop для записи, что он не устанавливает флаги? Что могли бы сделать эти 7? Это действительно простая инструкция.

  • Что особенного в Bulldozer, который сделал быстрый loop легким/стоит? Или AMD потратила кучу транзисторов на быстрый loop? Если это так, то, по-видимому, кто-то думал, что это хорошая идея.


Если бы loop был быстрым, это было бы идеально для целых циклов adc с произвольной точностью BigInteger, чтобы избегать партитур /adc с частичным флагом (см. Мои комментарии к моему ответу) или любого другого случая, когда вы хотите зацикливать, не касаясь флагов. Он также имеет незначительное преимущество по сравнению с dec/jnz. (И dec/jnz только макро-предохранители на семействе SnB).

На современных процессорах, где dec/jnz в порядке в цикле ADC, loop все равно будет приятным для циклов ADCX/ADOX (для сохранения OF).

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


Это не помешало бы мне разозлиться на все вопросы с плохим 16-битным кодом, который использует loop для каждого цикла, даже если им нужен еще один счетчик внутри цикла. Но, по крайней мере, это было бы не так плохо.

Ответ 1

Теперь, после того как я начал писать свой вопрос, он оказался точным дубликатом одного на comp.arch, который сразу же появился. Я ожидал, что это будет сложно для google (много "почему мой цикл медленный"), но моя первая попытка (why is the x86 loop instruction slow) получила результаты.

Это не хороший или полный ответ.

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


Хорошие сообщения с различными теориями в этом потоке:

Роберт

LOOP стал медленным на некоторых из самых ранних машин (около 486), когда началась значительная конвейерная обработка, и эффективная работа над простейшей инструкцией по трубопроводу была технологически непрактичной. Таким образом, LOOP был медленным для ряда поколений. Поэтому никто не использовал его. Поэтому, когда стало возможно ускорить его, не было никакого реального стимула для этого, поскольку никто на самом деле его не использовал.


Антон Эртл:

IIRC LOOP использовался в некоторых программах для циклов синхронизации; было (важное) программное обеспечение, которое не работало на процессорах, где LOOP был слишком быстрым (это было в начале 90-х или около того). Поэтому производители процессоров научились замедлять работу LOOP.


(Пол и кто-либо еще: вы можете повторно написать свое собственное письмо в качестве своего собственного ответа. Я удалю его из своего ответа и проголосую за вас).

@Paul A. Clayton (случайный плакат SO и разработчик архитектуры процессора) угадал, как вы можете использовать это множество. (Это выглядит как loope/ne который проверяет как счетчик, так и ZF):

Я мог представить себе возможную разумную версию 6- μop:

virtual_cc = cc; 
temp = test (cc); 
rCX = rCX - temp; // also setting cc 
cc = temp & cc; // assumes branch handling is not 
       // substantially changed for the sake of LOOP 
branch 
cc = virtual_cc 

(Обратите внимание, что это 6 uops, а не SnB 11 для LOOPE/LOOPNE, и это общее предположение, даже не пытаясь принять во внимание все, что известно из счетчиков SnB perf.)

Тогда Павел сказал:

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

Резюме: дизайнеры хотели, чтобы loop поддерживался только через микрокод, без каких-либо корректировок для собственно оборудования.

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

(Мое мнение: Intel, вероятно, все еще делает это медленно, и не позаботился переписать свой микрокод для него в течение длительного времени. Современные процессоры, вероятно, слишком быстр для чего-либо, используя loop наивным образом, чтобы работать правильно.)

... Павел продолжает:

Архитекторы позади Nano, возможно, обнаружили, что избегать специальной оболочки LOOP упростили их дизайн с точки зрения площади или мощности. Или у них могут быть стимулы для встроенных пользователей, чтобы обеспечить быструю реализацию (для преимуществ плотности кода). Это всего лишь УИЛД.

Если оптимизация LOOP выпала из других оптимизаций (например, слияние сравнения и ветвления), было бы легче настроить LOOP на инструкцию быстрого пути, чем обрабатывать его в микрокоде, даже если производительность LOOP была несущественной.

Я подозреваю, что такие решения основаны на конкретных деталях реализации. Информация о таких деталях, как представляется, не является общедоступной, и интерпретация такой информации будет выше уровня квалификации большинства людей. (Я не разработчик аппаратного обеспечения - и никогда не играл на телевидении или не оставался в Holiday Inn Express. :-)


Затем нить пошла в тему AMD в духе нашей единственной возможности почистить крип в кодировке команд x86. Трудно обвинить их, поскольку каждое изменение - это случай, когда декодеры не могут делиться транзисторами. И до того, как Intel приняла x8 6- 64, было даже не ясно, что это будет ловить. AMD не хотела обременять свои процессоры аппаратурой, которую никто не использовал, если AMD64 не поймал.

Но все же, есть так много мелочей: setcc мог быть изменен до 32 бит. (Обычно вам нужно использовать xor-zero/test/setcc, чтобы избежать ложных зависимостей, или потому, что вам нужен реестр с нулевой расширенностью). Shift может иметь безоговорочно записанные флаги, даже с нулевым сдвигом (удаление зависимости входных данных от eflags для смены переменных-счетчиков для исполнения LLC). В прошлый раз, когда я набрал этот список домашних животных, я думаю, что был третий... О да, bt/bts и т.д. С операндами памяти имеет адрес, зависящий от верхних бит индекса (битовая строка, а не только бит внутри машинное слово).

bts очень полезны для бит-полей и медленнее, чем они должны быть, поэтому вы почти всегда хотите загрузить в регистр, а затем использовать это. (Обычно он быстрее bts [mem], reg/маскирует, чтобы получить адрес самостоятельно, вместо использования 10 bts [mem], reg на Skylake, но он требует дополнительных инструкций, поэтому он имеет смысл на 386, но не на K8). Атомная бит-манипуляция должна использовать форму памяти-dest, но в версии с lock версией в любом случае нужно много uops. Он все еще медленнее, чем если бы он не мог получить доступ за пределами dword им слова.

Ответ 2

Пожалуйста, посмотрите хорошую статью Абраша, Майкла, опубликованную в журнале Dr. Dobb Journal March 1991 v16 n3 p16 (8): http://archive.gamedev.net/archive/reference/articles/article369.html

Резюме статьи следующее:

Оптимизация кода для микропроцессоров 8088, 80286, 80386 и 80486 сложно, потому что чипы используют значительно различную память архитектуры и время выполнения команд. Код не может быть оптимизированный для семейства 80x86; скорее, код должен быть разработан для обеспечивают хорошую производительность в ряде систем или оптимизированы для конкретные комбинации процессоров и памяти. Программисты должны избегайте необычных инструкций, поддерживаемых 8088, которые потеряли их преимущество в последующих чипах. Строковые инструкции следует использовать, но не полагаться. Регистры следует использовать скорее чем операции с памятью. Ветвление также медленное для всех четырех процессоры. Доступ к памяти должен быть выровнен для улучшения представление. Как правило, для оптимизации 80486 требуется точно противоположные шаги по оптимизации 8088.

Под "необычными инструкциями, поддерживаемыми 8088" автор также означает "цикл":

Любой программист 8088 инстинктивно заменит: DEC CX JNZ LOOPTOP с: LOOP LOOPTOP, потому что LOOP значительно быстрее на 8088. LOOP также работает быстрее на 286. На 386, однако, LOOP на самом деле два цикла медленнее, чем DEC/JNZ. Маятник качается еще дальше 486, где LOOP примерно в два раза медленнее, чем DEC/JNZ, - и, разумеется, мы говорим о том, что изначально возможно было наиболее очевидным оптимизация во всем наборе команд 80x86.

Это очень хорошая статья, и я очень рекомендую ее. Несмотря на то, что он был опубликован в 1991 году, на сегодня это удивительно актуально.

Но эта статья просто дает советы, она рекомендует проверять скорость выполнения и выбирать более быстрые варианты. Он не объясняет, почему некоторые команды становятся очень медленными, поэтому он не полностью решает ваш вопрос.

Ответ заключается в том, что более ранние процессоры, такие как 80386 (выпущенные в 1985 году) и ранее, последовательно выполняли команды поочередно.

Позже процессоры начали использовать конвейерную обработку команд - изначально, просто, для 804086 и, наконец, Pentium Pro (выпущенный в 1995 году) вводили принципиально другой внутренний трубопровод, называя его ядром Out Of Order (OOO), где инструкции были преобразованы к небольшим фрагментам операций, называемым микрооперациями или μops, а затем все микрооперации разных инструкций были помещены в большой пул микроопераций, где они должны были выполняться одновременно, пока они не зависят друг от друга. Этот принцип трубопровода ООО по-прежнему практически не меняется на современных процессорах. Вы можете найти дополнительную информацию о конвейерной обработке инструкций в этой блестящей статье: https://www.gamedev.net/resources/_/technical/general-programming/a-journey-through-the-cpu-pipeline-r3115

Чтобы упростить разработку чипов, Intel решила построить процессоры таким образом, чтобы одна инструкция очень эффективно преображалась в микрооперации, а другие - нет.

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

Например, в "Справочном руководстве по оптимизации архитектуры Intel® http://download.intel.com/design/PentiumII/manuals/24512701.pdf упоминается следующее:" Избегайте использования сложных инструкций (например, введите, оставьте, или цикл), которые обычно имеют более четырех микрофонов и требуют нескольких циклов для декодирования. Вместо этого используйте последовательности простых инструкций. "

Итак, Intel почему-то решила, что инструкция "loop" является "сложной", и с тех пор она стала очень медленной. Тем не менее, нет официальной ссылки Intel на разбиение команд: сколько микроопераций каждая инструкция производит и сколько циклов требуется для ее декодирования.

Вы также можете прочитать о движке Ex-of-Order Execution Engine в "Справочном руководстве по оптимизации архитектур Intel® 64 и IA-32" http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf раздел 2.1.2.

Ответ 3

В 1988 году IBM-коллега Гленн Генри только что появился на борту в Dell, в то время в нем было несколько сотен сотрудников, и в первый же месяц он дал техническую беседу о 386 внутренних лицах. Многим из нас, программистов-программистов, было интересно, почему LOOP был медленнее, чем DEC/JNZ, поэтому в разделе вопросов/ответов кто-то задал вопрос.

Его ответ имел смысл. Это связано с подкачкой.

LOOP состоит из двух частей: декрементация CX, а затем прыжка, если CX не равна нулю. Первая часть не может вызвать исключение процессора, тогда как часть перехода может. Во-первых, вы можете прыгать (или проваливаться) на адрес за пределами границ сегмента, вызывая SEGFAULT. Для двоих вы можете перейти на страницу, которая была заменена.

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

Перезапуск означает восстановление состояния процесса до того, что было непосредственно перед нарушением инструкции. В частности, в случае команды LOOP это означало восстановление значения регистра CX. Можно подумать, что вы можете просто добавить 1 в CX, так как мы знаем, что CX уменьшилось, но, по-видимому, это не так просто. Например, проверьте эту ошибку от Intel:

Нарушения, связанные с защитой, обычно указывают на вероятную ошибку программного обеспечения, и перезагрузка нежелательна, если происходит одно из этих нарушений. В системе 80286 с защищенным режимом с состояниями ожидания во время любых циклов шины, когда определенные нарушения защиты обнаруживаются компонентом 80286, а компонент передает управление процедуре обработки исключений, содержимое регистра CX может быть ненадежным. (Изменяется ли содержимое CX, является функцией активности шины в то время, когда внутренний микрокод обнаруживает нарушение защиты.)

Чтобы быть в безопасности, им нужно было сохранить значение CX на каждой итерации команды LOOP, чтобы надежно восстановить ее, если это необходимо.

Это лишнее бремя экономии CX, которое сделало LOOP настолько медленным.

Intel, как и все остальные в то время, становилась все более и более RISC. Старые инструкции CISC (LOOP, ENTER, LEAVE, BOUND) постепенно прекращались. Мы все еще использовали их в сборке с ручной кодировкой, но компиляторы полностью их игнорировали.