Какую настройку делает REP?

Цитирование справочного руководства по оптимизации архитектуры Intel® 64 и IA-32, §2.4.6 "Улучшение строки REP":

Характеристики производительности использования строки REP можно отнести к двум компонентам: накладные расходы при запуске и пропускная способность передачи данных.

[...]

Для строки REP с большей степенью детализации при передаче значения ECX накладные расходы при запуске строки REP демонстрируют пошаговое увеличение:

  • Короткая строка (ECX <= 12): задержка REP MOVSW/MOVSD/MOVSQ составляет около 20 циклов,
  • Быстрая строка (ECX >= 76: исключая REP MOVSB): реализация процессора обеспечивает аппаратную оптимизацию, перемещая как можно больше фрагментов данных в 16 байтов. Задержка строки REP будет изменяться, если один из 16-байтовых данных пересекает границу строки кэша:

    • Без разделения: задержка состоит из начальной стоимости около 40 циклов, и каждые 64 байта данных добавляют 4 цикла,
    • Разделение кэша: задержка состоит из начальной стоимости около 35 циклов, и каждые 64 байта данных добавляют 6 циклов.
  • Промежуточные длины строк: задержка REP MOVSW/MOVSD/MOVSQ имеет начальную стоимость около 15 циклов плюс один цикл для каждой итерации перемещения данных в word / dword / qword.

(акцент мой)

Там нет дальнейшего упоминания о такой стоимости запуска. Что это? Что это делает и почему это всегда занимает больше времени?

4 ответа

Решение

rep movs У микрокода есть несколько стратегий на выбор. Если src и dest не пересекаются друг с другом, микрокодированная петля может передавать на 64б куски больше. (Это так называемая функция "быстрых строк", представленная в P6 и иногда перестраиваемая для более поздних процессоров, которые поддерживают более широкие загрузки / сохранения). Но если dest только один байт от src, rep movs должен дать тот же результат, который вы получили бы от этого множества отдельных movs инструкции.

Таким образом, микрокод должен проверять наличие совпадений и, возможно, выравнивание (отдельно src и dest или относительное выравнивание). Вероятно, он также выбирает что-то на основе малых / средних / больших значений счетчика.

Согласно комментариям Энди Глеу к ответу Почему сложный memcpy / memset лучше? условные переходы в микрокоде не подлежат прогнозированию переходов. Таким образом, в циклах запуска есть существенное наказание, если путь по умолчанию, который не был взят, не является фактически взятым, даже для цикла, который использует тот же самый rep movs с одинаковым выравниванием и размером.

Он контролировал начальный rep Строковая реализация в P6, поэтому он должен знать.:)

REP MOVS использует функцию протокола кэширования, которая недоступна для обычного кода. В основном как потоковые хранилища SSE, но способом, который совместим с обычными правилами упорядочения памяти и т. Д. // "Большие накладные расходы на выбор и настройку правильного метода" в основном связаны с отсутствием предсказания ветвления микрокода. Я давно хотел, чтобы я реализовал REP MOVS, используя аппаратный конечный автомат, а не микрокод, который мог бы полностью устранить накладные расходы.

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

У Intel x86 были "быстрые строки" со времен Pentium Pro (P6) в 1996 году, которым я руководил. Быстрые строки P6 принимали REP MOVSB ​​и больше и реализовали их с 64-битной загрузкой и хранением микрокода и протоколом кеширования без RFO. Они не нарушали порядок памяти, в отличие от ERMSB в iVB.

Большая слабость создания быстрых строк в микрокоде заключалась в (а) неправильном прогнозировании ветвей микрокода и (б) микрокоде не соответствовал каждому поколению, становясь все медленнее и медленнее, пока кто-то не нашел способ его исправить. Так же, как в библиотеке люди теряют слух. Я предполагаю, что возможно, что одной из упущенных возможностей было использование 128-битных загрузок и хранилищ, когда они стали доступны, и так далее.

Оглядываясь назад, я должен был написать самонастраивающуюся инфраструктуру, чтобы получать достаточно хороший микрокод для каждого поколения. Но это не помогло бы использовать новые, более широкие грузы и магазины, когда они стали доступны. // Кажется, ядро ​​Linux имеет такую ​​инфраструктуру автонастройки, которая запускается при загрузке. // В целом, однако, я рекомендую аппаратные конечные автоматы, которые могут плавно переходить между режимами, не вызывая ошибочных прогнозов ветвления. // Это спорно, будет ли предсказание хорошего микрокода филиала устранит это.

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

Поскольку Intel не публикует полную информацию, лучшее, что мы можем сделать, - это измерения количества циклов для разных размеров и выравниваний в "черном ящике". К счастью, это все, что нам нужно, чтобы сделать правильный выбор. Руководство Intel и http://agner.org/optimize/ содержат полезную информацию о том, как использовать rep movs,


Интересный факт: без ERMSB (IvB и позже): rep movsb оптимизирован для маленьких копий. Запуск занимает больше времени, чем rep movsd или же rep movsq для больших (я думаю, больше пары сотен байт) копий, и даже после этого, возможно, не будет достигнута та же пропускная способность.

Оптимальная последовательность для больших выровненных копий без ERMSB и без SSE/AVX (например, в коде ядра) может быть rep movsq а затем очистить что-то вроде неприсоединения mov который копирует последние 8 байтов буфера, возможно, перекрывая последний выровненный фрагмент чего rep movsq сделал. (в основном используйте маленькую копию glibc memcpy стратегия). Но если размер может быть меньше, чем 8 байт, вам нужно выполнить ветвление, если безопасно копировать больше байтов, чем необходимо. Или же rep movsb это опция для очистки, если маленький размер кода важнее производительности. (rep скопирует 0 байтов, если RCX = 0).

SIMD векторный цикл часто, по крайней мере, немного быстрее, чем rep movsb даже на процессорах с Enhanced Rep Move/Stos B. Особенно, если выравнивание не гарантировано. ( Улучшен REP MOVSB ​​для memcpy и см. Также руководство по оптимизации Intel. Ссылки в вики-теге x86)

Приведенная вами цитата относится только к микроархитектуре Nehalem (процессоры Intel Core i5, i7 и Xeon, выпущенные в 2009 и 2010 годах), и Intel недвусмысленно говорит об этом.

До Nehalem REP MOVSB ​​был еще медленнее. Intel молчит о том, что произошло в последующих микроархитектурах, но затем, с микроархитектурой Ivy Bridge (процессоры выпущены в 2012 и 2013 годах), Intel представила Enhanced REP MOVSB ​​(нам все еще нужно проверить соответствующий бит CPUID), что позволило нам копировать память быстрая.

Самые дешевые версии более поздних процессоров - Kaby Lake "Celeron" и "Pentium", выпущенные в 2017 году, не имеют AVX, который можно было бы использовать для быстрого копирования в память, но у них все еще есть Enhanced REP MOVSB. Именно поэтому REP MOVSB ​​очень выгоден для процессоров, выпущенных с 2013 года.

Удивительно, но процессоры Nehalem имели довольно быструю реализацию REP MOVSD/MOVSQ (но не REP MOVSW/MOVSB) для блоков очень большого размера - всего 4 цикла для копирования каждых последующих 64 байтов данных (если данные выровнены по границам строк кэша) после мы заплатили за запуск 40 циклов - это прекрасно, когда мы копируем 256 байтов и более, и вам не нужно использовать регистры XMM!

Таким образом, в микроархитектуре Nehalem REP MOVSB ​​/MOVSW практически бесполезен, но REP MOVSD/MOVSQ превосходен, когда нам нужно скопировать более 256 байтов данных, и данные выровнены по границам строк кэша.

На предыдущих микроархитектурах Intel (до 2008 года) затраты на запуск еще выше.

Вот тесты REP MOVS*, когда источник и назначение находились в кеше L1, блоков, достаточно больших, чтобы не подвергаться серьезному влиянию затрат на запуск, но не настолько больших, чтобы превышать размер кэша L1. Источник: http://users.atw.hu/instlatx64/

Йона (2006-2008)

    REP MOVSB 10.91 B/c
    REP MOVSW 10.85 B/c
    REP MOVSD 11.05 B/c

Нехалем (2009-2010)

    REP MOVSB 25.32 B/c
    REP MOVSW 19.72 B/c
    REP MOVSD 27.56 B/c
    REP MOVSQ 27.54 B/c

Вестмер (2010-2011)

    REP MOVSB 21.14 B/c
    REP MOVSW 19.11 B/c
    REP MOVSD 24.27 B/c

Ivy Bridge (2012-2013) - с улучшенным REP MOVSB

    REP MOVSB 28.72 B/c
    REP MOVSW 19.40 B/c
    REP MOVSD 27.96 B/c
    REP MOVSQ 27.89 B/c

SkyLake (2015-2016) - с расширенным REP MOVSB

    REP MOVSB 57.59 B/c
    REP MOVSW 58.20 B/c
    REP MOVSD 58.10 B/c
    REP MOVSQ 57.59 B/c

Озеро Кабы (2016-2017) - с расширенным РЭП МОВСБ

    REP MOVSB 58.00 B/c
    REP MOVSW 57.69 B/c
    REP MOVSD 58.00 B/c
    REP MOVSQ 57.89 B/c

Как видите, реализация REP MOVS существенно отличается от одной микроархитектуры к другой.

По данным Intel, в Nehalem затраты на запуск REP MOVSB ​​для строк размером более 9 байт составляют 50 циклов, но для REP MOVSW/MOVSD/MOVSQ они составляют от 35 до 40 циклов - поэтому REP MOVSB ​​имеет более высокие затраты на запуск; тесты показали, что общая производительность хуже для REP MOVSW, а не REP MOVSB ​​для Nehalem и Westmere.

На Ivy Bridge, SkyLake и Kaby Lake результаты противоположны для этих инструкций: REP MOVSB ​​быстрее чем REP MOVSW/MOVSD/MOVSQ, хотя и незначительно. На Ivy Bridge REP MOVSW все еще отстает, но на SkyLake и Kaby Lake REP MOVSW не хуже, чем REP MOVSD/MOVSQ.

Обратите внимание, что я представил результаты тестов для SkyLake и Kaby Lake, взятые с сайта instaltx64 только для подтверждения - эти архитектуры имеют одинаковые данные о цикле на инструкцию.

Вывод: вы можете использовать MOVSD / MOVSQ для очень больших блоков памяти, поскольку он дает достаточные результаты на всех микроархитектурах Intel от Yohan до Kaby Lake. Хотя на архитектурах Yonan и более ранних версиях копия SSE может давать лучшие результаты, чем REP MOVSD, но ради универсальности REP MOVSD предпочтительнее. Кроме того, REP MOVS* может внутренне использовать различные алгоритмы для работы с кешем, который недоступен для обычных инструкций.

Что касается REP MOVSB ​​для очень маленьких строк (менее 9 байтов или менее 4 байтов) - я бы даже не рекомендовал его. На озере Кабы, один MOVSB даже без REP 4 цикла, на Йохане 5 циклов. В зависимости от контекста, вы можете добиться большего успеха, просто используя обычные MOV.

Стоимость запуска не увеличивается с увеличением размера, как вы написали. Это задержка всей инструкции для завершения всей последовательности байтов, которая увеличивается - что довольно очевидно - чем больше байтов вам нужно скопировать, тем больше циклов требуется, т. Е. Общая задержка, а не только стоимость запуска. Intel не раскрыла стоимость запуска для небольших строк, она указала только для строки размером 76 байт и более, для Nehalem. Например, возьмите эти данные о Нехалеме:

  • Задержка для MOVSB ​​составляет 9 циклов, если ECX < 4. Таким образом, это означает, что для копирования любой строки требуется ровно 9 циклов, как только эта строка имеет 1 байт, 2 байта или 3 байта. Это не так уж плохо - например, если вам нужно скопировать хвост, и вы не хотите использовать или перекрывающиеся магазины. Всего 9 циклов для определения размера (от 1 до 3) и фактического копирования данных - этого трудно добиться с помощью обычных инструкций и всего этого ветвления - и для 3-байтовой копии, если вы не копировали предыдущие данные, вам придется использовать 2 загрузки и 2 хранилища (слово + байт), и, поскольку у нас не более одного хранилища, мы не будем делать это намного быстрее с обычными инструкциями MOV.
  • Intel молчит о том, какая задержка имеет значение REP MOVSB, если ECX находится между 4 и 9
  • Короткая строка (ECX <= 12): задержка REP MOVSW/MOVSD/MOVSQ составляет около 20 циклов для копирования всей строки, а не только стоимость запуска 20 циклов. Таким образом, для копирования всей строки размером <= 12 байт требуется около 20 циклов, поэтому мы имеем более высокую скорость вывода на байт, чем с REP MOVSB ​​с ECX < 4.
  • ECX> = 76 с REP MOVSD/MOVSQ - да, здесь у нас действительно стартовая стоимость 40 циклов, но это более чем разумно, поскольку позже мы будем использовать копирование каждых 64 байтов данных всего за 4 цикла. Я не инженер Intel, уполномоченный отвечать на вопросы ПОЧЕМУ затраты на запуск, но я полагаю, что это потому, что для этих строк используется REP MOVS* (согласно комментариям Энди Глеу к ответу "Почему сложны улучшенные memcpy/memset? из ответа Питера Кордеса) функция протокола кэширования, недоступная обычному коду. И в этой цитате есть объяснение: "Большие затраты на выбор и настройку правильного метода в основном связаны с отсутствием предсказания ветвей микрокода". Также было интересно отметить, что Pentium Pro (P6) в 1996 году внедрил REP MOVS* с 64-битной загрузкой и хранением микрокодов и протоколом кэширования без RFO - они не нарушали упорядочение памяти, в отличие от ERMSB в Ivy Bridge.

Этот патент показывает, что декодер может определить, был ли последний переход к немедленному или он был изменен таким образом, что значение in неизвестно в декодере. Он делает это, устанавливая бит при декодировании немедленного mov, а также называет это «бит быстрой строки». Бит очищается, когда он декодирует инструкцию, изменяющую rcx in an unknown manner. If the bit is set then it jumps to a position in a separate microcode routine which might be a size of 12 repetitions. If might jump to repetition 7 if rcx = 5. This is a fast implementation that doesn't contain microbranches.

I would assume that the micro-branch in the regular MSROM procedure would be statically predicted taken by the uop itself (in the opcode), since this is a loop that's going to execute multiple times and mispredict once. This fast method would therefore only eliminate the branch misprediction at the end of the sequence as well as the micro-branch instruction per iteration, which reduces the number of uops. The main bulk of misprediction happen in the setup Peter mentions, before it can select the fast or slow route.

The microbranch misprediction is costly because it has to flush the whole pipeline, refetch the <tcode id="50528"></tcode> instruction and then resume decoding at the mispredicted micro-ip, returning to the MSROM procedure after it may have already finished decoding and other uops were being decoded behind it. The BOB can likely be used with microbranch mispredictions too, where it would be more beneficial than with a macrobranch misprediction. The RAT snapshot is likely associated with the ROB entry of every branch instruction.

Просто из описания мне кажется, что существует оптимальный размер передачи 16 байтов, поэтому, если вы переносите 79 байтов, то есть 4*16 + 15, так что не зная больше о выравнивании, это может означать, что для 15 байтов либо впереди, либо в конце (или разделены), а 4 16-байтовые передачи быстрее, чем дроби 16. Вроде как высокая передача в вашем автомобиле против переключения вверх через передачи на высокую передачу.

Посмотрите на оптимизированный memcpy в glibc или gcc или других местах. Они передают до нескольких отдельных байтов, затем они могут выполнять 16-битные передачи, пока не доберутся до оптимального выровненного размера для 32-битного, 64-битного и 128-битного адресов, а затем они могут выполнять многословные передачи для большая часть копии, затем они уменьшаются, может быть одна 32-битная вещь, может быть один 16 или 1 байт, чтобы покрыть отсутствие выравнивания на серверной части.

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

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