Как команда rep stosb выполняется быстрее, чем эквивалентный цикл?

Как команда rep stosb выполняется быстрее, чем этот код?

    Clear: mov byte [edi],AL       ; Write the value in AL to memory
           inc edi                 ; Bump EDI to next byte in the buffer
           dec ecx                 ; Decrement ECX by one position
           jnz Clear               ; And loop again until ECX is 0

Гарантируется ли это на всех современных процессорах? Должен ли я всегда использовать rep stosb вместо написания цикла вручную?

Ответ 1

В современных процессорах реализация микрокодирования rep stosb и rep movsb фактически использует хранилища, которые шире 1B, поэтому он может rep movsb намного быстрее, чем один байт за такт.

(Обратите внимание, что это применимо только к stos и mov, но не к repe cmpsb или repne scasb. К сожалению, они все еще медленны, как в лучшем случае 2 цикла на байт по сравнению с Skylake, который жалок по сравнению с AVX2 vpcmpeqb для реализации memcmp или memchr. См. https://agner.org/optimize/ для таблиц инструкций и других ссылок на ссылки в теге x86 вики.

См. Почему этот код в 6.5 раз медленнее с включенной оптимизацией? для примера gcc неразумно встроенного repnz scasb или менее плохого скалярного битхака для strlen который, как оказывается, становится большим, и простой альтернативой SIMD.)


rep stos/movs имеет значительные накладные расходы при запуске, но хорошо rep stos/movs для больших memset/memcpy. (См. rep stos оптимизации Intel/AMD для обсуждения того, когда использовать rep stos сравнении с векторизованным циклом для небольших буферов.) Однако без функции ERMSB rep stosb настроен для средних и малых наборов памяти, и оптимально использовать rep stosd или rep stosq (если вы не собираетесь использовать цикл SIMD).

При rep stos отладчике rep stos только одну итерацию (один декремент ecx/rcx), поэтому реализация микрокода никогда не запускается. Не позволяйте этому обмануть вас, думая, что все это может сделать.

Смотрите Какую настройку выполняет REP? некоторые подробности о том, как микроархитектуры семейства Intel P6/SnB реализуют rep movs.

Посмотрите в расширенном REP MOVSB для memcpy информацию о пропускной способности памяти с rep movsb против цикла SSE или AVX, на процессорах Intel с функцией ERMSB. (Обратите особое внимание, что многоядерные процессоры Xeon не могут насытить пропускную способность DRAM только одним потоком из-за ограничений на количество пропусков кэш-памяти одновременно, а также из-за протоколов хранения RFO и не RFO.)


Современный процессор Intel должен выполнять рассматриваемый цикл asm по одной итерации в такт, но ядро семейства AMD бульдозеров, вероятно, не может даже управлять одним магазином в такт. (Узкий на два исполнительных целом число портов обработки инструкции INC/DEC/ветвление. Если условие цикла было CMP/ОКК на edi, сердечник AMD может макро-сплавить сравнить-и-ветвь.)


Одной из основных особенностей так называемых быстрых операций с rep movs (rep movs и rep stos на процессорах Intel P6 и семейства SnB) является то, что они избегают трафика когерентности кэша "чтение для владения" при хранении в ранее не кэшированной памяти. использование хранилищ NT для записи целых строк кэша, но все еще строго упорядоченных (функция ERMSB использует слабо упорядоченные хранилища).

ИДК, насколько хорошая реализация AMD.


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

См. Ресурсы по оптимизации (особенно руководства Agner Fog), связанные с вики-тегом .


Intel IvyBridge, а затем и ERMSB, который позволяет rep stos[b/w/d/q] и rep movs[b/w/d/q] использовать слабо упорядоченные хранилища (например, movnt), позволяя хранилищам фиксировать для кэширования -of заказ. Это является преимуществом, если в кеше L1 не все места назначения уже загружены. Из формулировки документов я считаю, что в конце быстрой строковой операции существует неявный барьер памяти, поэтому любое переупорядочение видно только между хранилищами, созданными строковой операцией, а не между ней и другими хранилищами. то есть вам все еще не нужен sfence после rep movs.

Таким образом, для больших выровненных буферов на Intel IvB и более поздних rep stos реализация memset может превзойти любую другую реализацию. Тот, который использует movnt хранилища (которые не оставляют данные в кэше), также должен быть близок к насыщению полосы пропускания записи в основную память, но на практике может не совсем соответствовать. Смотрите комментарии для обсуждения этого, но я не смог найти ни одного номера.

Для небольших буферов разные подходы имеют очень разные объемы служебной информации. Микробенчмарки могут сделать циклы копирования SSE/AVX лучше, чем они есть, потому что выполнение копии с одинаковым размером и выравниванием каждый раз позволяет избежать ошибочных прогнозов ветвления в коде запуска/очистки. IIRC, он рекомендовал использовать векторизованный цикл для копий под 128B на процессорах Intel (не rep movs). Пороговое значение может быть выше, в зависимости от процессора и окружающего кода.

В руководстве по оптимизации Intel также обсуждаются издержки для различных реализаций memcpy, и что rep movsb имеет больший штраф за смещение, чем movdqu.


См. Код для оптимизированной реализации memset/memcpy для получения дополнительной информации о том, что делается на практике. (например, библиотека Agner Fog).

Ответ 2

Если ваш процессор имеет бит CPUID ERMSB, то команды rep movsb и rep stosb выполняются иначе, чем на старых процессорах.

См. Справочное руководство по оптимизации Intel, раздел 3.7.6 Расширенные операции REP MOVSB и REP STOSB (ERMSB).

http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf

И руководство, и мои тесты показывают, что преимущества REP STOSB проявляются только в больших блоках памяти размером более 128 байт. На меньших блоках, таких как 5 байт, показанный вами код (mov byte [edi], al; inc edi; dec ecx; jnz Clear) будет намного быстрее, поскольку затраты на запуск REP STOSB очень высоки - около 35 циклы.

Чтобы получить преимущества REP STOSB на новых процессорах с битом CPUID ERMSB, должны выполняться следующие условия: - целевой буфер должен быть выровнен по 16-байтовой границе; - если длина кратна 64, это может привести к еще большей производительности; - бит направления должен быть установлен "вперед" (CLD).

ERMSB начинает превосходить другие методы, когда длина составляет не менее 128 байт, потому что, как я писал, в ERMSB наблюдается высокий внутренний запуск - около 35 циклов. ERMSB начинает явно превосходить другие методы, когда длина превышает 2048 байтов.

Когда буфер назначения выровнен на 16 байтов, REP STOSB с использованием ERMSB может работать лучше, чем подходы SIMD. При неправильном выравнивании буфера назначения производительность memset() с использованием ERMSB может снизиться примерно на 20% по сравнению с выровненным регистром для процессоров, основанных на кодовом имени микроархитектуры Intel Ivy Bridge. Напротив, реализация SIMD REP STOSB будет испытывать меньшую деградацию при неправильном выравнивании пункта назначения.

У меня есть процессор Intel Core i5 6600 с 32 КБ L1, 256 КБ L2 и 6 МБ L3, и я мог получить ~ 100 ГБ/с на REP STOSB с 32K блоками.

Вот результаты реализации REP STOSB memset():

  • 1297920000 блокам данных по 16 байт потребовалось 13,6022 секунды для обработки с помощью memset(); 1455,9999 мегабайт в секунду
  • 648960000 блоков данных по 32 байта заняли 6,7840 секунд для обработки с помощью memset(); 2919,3058 мегабайт в секунду
  • 1622400000 блоков данных размером 64 байта заняли 16,9762 секунды для обработки с помощью memset(); 5833,0883 мегабайт в секунду
  • 817587402 блокам данных по 127 байт потребовалось 8,5698 секунд для обработки с помощью memset(); 11554,8914 мегабайт в секунду
  • 811200000 блоков данных размером 128 байт заняли 8,5197 секунд, чтобы обработать с помощью memset(); 11622,9306 мегабайт в секунду
  • 804911628 блоков данных по 129 байт заняли 9,1513 секунд для обработки с помощью memset(); 10820,6427 мегабайт в секунду
  • 407190588 блоков данных по 255 байтов заняли 5,4656 секунд для обработки с помощью memset(); 18117,7029 мегабайт в секунду
  • 405600000 блоков данных по 256 байт заняли 5,0314 секунды для обработки с помощью memset(); 19681,1544 мегабайт в секунду
  • 202800000 блоков данных по 512 байт заняло 2,7403 секунды для обработки с помощью memset(); 36135,8273 мегабайт в секунду
  • 101400000 блоков данных по 1024 байта заняли 1,6704 секунды для обработки с помощью memset(); 59279,5229 мегабайт в секунду
  • 3168750 блоков данных размером 32768 байт заняло 0,9525 секунды для обработки с помощью memset(); 103957,8488 мегабайт в секунду
  • 2028000 блоков данных по 51200 байт заняли 1,5321 секунды для обработки с помощью memset(); 64633,5697 мегабайт в секунду
  • 413878 блокам данных 250880 байт потребовалось 1,7737 секунды для обработки с помощью memset(); 55828,1341 мегабайт в секунду
  • 19805 блоки данных размером 5242880 байт заняли 2.6009 секунд, чтобы обработать memset(); 38073,0694 мегабайт в секунду

Вот результаты реализации memset(), которая использует MOVDQA [RCX], XMM0:

  • 1297920000 блокам данных по 16 байтов потребовалось 3,5795 секунды для обработки с помощью memset(); 5532,7798 мегабайт в секунду
  • 648960000 блокам данных по 32 байта потребовалось 5,5538 секунд для обработки с помощью memset(); 3565,9727 мегабайт в секунду
  • 1622400000 блоков данных размером 64 байта заняли 15,7489 секунд для обработки с помощью memset(); 6287,6436 мегабайт в секунду
  • 817587402 блокам данных по 127 байт потребовалось 9,6637 секунд для обработки с помощью memset(); 10246,9173 мегабайт в секунду
  • 811200000 блоков данных размером 128 байт заняли 9,6236 секунд, чтобы обработать с помощью memset(); 10289,6215 мегабайт в секунду
  • 804911628 блоков данных по 129 байт заняли 9,4852 секунды, чтобы обработать с помощью memset(); 10439,7473 мегабайт в секунду
  • 407190588 блоков данных по 255 байт заняли 6,6156 секунд, чтобы обработать с помощью memset(); 14968,1754 мегабайт в секунду
  • 405600000 блоков данных по 256 байт заняли 6,6437 секунд для обработки с помощью memset(); 14904,9230 мегабайт в секунду
  • 202800000 блоков данных по 512 байт заняли 5,0695 секунды для обработки с помощью memset(); 19533,2299 мегабайт в секунду
  • 101400000 блоков данных по 1024 байта заняли 4,3506 секунды для обработки с помощью memset(); 22761,0460 мегабайт в секунду
  • 3168750 блоков данных размером 32768 байт потребовалось 3,7269 секунды для обработки с помощью memset(); 26569,8145 мегабайт в секунду
  • 2028000 блоков данных по 51200 байт заняли 4,0538 секунды для обработки с помощью memset(); 24427,4096 мегабайт в секунду
  • 413878 блокам данных 250880 байт потребовалось 3,9936 секунды для обработки с помощью memset(); 24795,5548 мегабайт в секунду
  • 19805 блокам данных 5242880 байт потребовалось 4,55892 секунды для обработки с помощью memset(); 21577,7860 мегабайт в секунду

Как видите, в 64-битных блоках REP MOVSB работает медленнее, но, начиная с 128-байтовых блоков, REP MOVSB начинает превосходить другие методы, и разница очень значительна, начиная с блоков по 512 байт и более.