Почему сложный memcpy/memset лучше?

При отладке я часто заходил в реализацию рукописных сборок memcpy и memset. Обычно они реализуются с использованием инструкций потоковой передачи, если они доступны, циклического развертывания, оптимизации выравнивания и т. Д. Я также недавно столкнулся с этой "ошибкой" из-за оптимизации memcpy в glibc.

Вопрос: почему производители оборудования (Intel, AMD) не могут оптимизировать конкретный случай

rep stos

а также

rep movs

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

6 ответов

Решение

Стоимость.

Стоимость оптимизации memcpy в вашей C-библиотеке это довольно минимально, может быть, несколько недель времени разработчиков здесь и там. Вам придется создавать новую версию каждые несколько лет или около того, когда характеристики процессора изменятся настолько, чтобы оправдать переписывание. Например, GNU glibc и Apple libSystem оба имеют memcpy который специально оптимизирован для SSE3.

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

  • Увеличенное энергопотребление
  • Увеличенная стоимость единицы
  • Увеличенная задержка для определенных подсистем процессора
  • Нижняя максимальная тактовая частота

Теоретически, это может оказать общее негативное влияние как на производительность, так и на удельную стоимость.

Максим: Не делайте это аппаратно, если программное решение достаточно хорошее.

Примечание. Указанная вами ошибка не является glibc по спецификации C. Это сложнее. В основном, ребята говорят, что memcpy ведет себя точно так, как указано в стандарте, а некоторые другие люди жалуются, что memcpy должен быть связан с memmove,

Время для рассказа: это напоминает мне жалобу, которая была у разработчика игр для Mac, когда он запускал свою игру на процессоре 603 вместо 601 (это с 1990-х годов). У 601 была аппаратная поддержка для невыровненных нагрузок и хранилищ с минимальным снижением производительности. 603 просто сгенерировал исключение; разгрузив ядро, я полагаю, что модуль загрузки / хранения можно сделать намного проще, возможно, сделав процессор быстрее и дешевле в процессе. Наноядро Mac OS обработало исключение, выполнив требуемую операцию загрузки / сохранения и вернув управление процессу.

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

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

В целом, эта тенденция, по-видимому, противоположна упомянутому виду аппаратных оптимизаций. В то время как в x86 это легко написать memcpy при сборке некоторые новые архитектуры переносят еще больше работы на программное обеспечение. Особого внимания заслуживают архитектуры VLIW: примерами являются Intel IA64 (Itanium), DSP TI TMS320C64x и Transmeta Efficeon. С VLIW программирование на ассемблере становится намного более сложным: вы должны явно выбирать, какие исполнительные блоки получают, какие команды и какие команды можно выполнять одновременно, что будет делать для вас современный x86 (если это не Atom). Итак, написание memcpy вдруг становится намного, намного сложнее.

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

Одна вещь, которую я хотел бы добавить к другим ответам, это то, что rep movs на самом деле не медленно на всех современных процессорах. Например,

Обычно инструкция REP MOVS имеет большие накладные расходы для выбора и настройки правильного метода. Следовательно, он не оптимален для небольших блоков данных. Для больших блоков данных может быть достаточно эффективно, когда выполняются определенные условия для выравнивания и т. Д. Эти условия зависят от конкретного процессора (см. Стр. 143). На процессорах Intel Nehalem и Sandy Bridge это самый быстрый способ перемещения больших блоков данных, даже если данные не выровнены.

[Подсветка моя.] Ссылка: Agner Fog, Оптимизация подпрограмм на языке ассемблера Руководство по оптимизации для платформ x86.,п. 156 (см. Также раздел 16.10, стр. 143) [версия 2011-06-08].

Общая цель против Специализированной

Одним из факторов является то, что эти инструкции (rep-prefix/string Инструкции) имеют общее назначение, поэтому они будут обрабатывать любое выравнивание, любое количество байтов или слов и будут иметь определенное поведение относительно кэша и / или состояния регистров и т. Д., Т.е. четко определенные побочные эффекты, которые не могут быть изменены.

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

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

Нет смысла заниматься этой специализацией на аппаратном уровне. Слишком много сложностей (= стоимость).

Закон убывающей отдачи

Другой способ думать об этом заключается в том, что когда вводятся новые функции, например, SSE, разработчики вносят архитектурные изменения для поддержки этих функций, например, интерфейс памяти с более широкой или более высокой пропускной способностью, изменения в конвейере, новые исполнительные блоки и т. Д. маловероятно, что в этот момент мы вернемся к "устаревшей" части дизайна, чтобы попытаться довести ее до новейших функций. Это было бы неэффективно. Если вы следуете этой философии, вы можете спросить, зачем нам в первую очередь SIMD, не может ли дизайнер просто заставить узкие инструкции работать так же быстро, как SIMD, в тех случаях, когда кто-то использует SIMD? Ответ обычно заключается в том, что оно того не стоит, потому что его проще добавить в новый исполнительный блок или инструкции.

Давным-давно rep movsb было оптимальным решением.

Оригинальный IBM PC имел процессор 8088 с 8-битной шиной данных и без кешей. Тогда самой быстрой программой обычно была та, с наименьшим количеством байтов инструкций. Наличие специальных инструкций помогло.

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

Intel и AMD хранят старые инструкции в основном для обратной совместимости.

Во встроенных системах обычно имеется специализированное оборудование, которое выполняет memcpy/memset. Обычно это не специальная инструкция процессора, а периферийное устройство DMA, которое находится на шине памяти. Вы пишете пару регистров, чтобы сообщить адреса, а HW делает все остальное. Это на самом деле не требует специальных инструкций процессора, так как на самом деле это просто проблема с интерфейсом памяти, в которой нет необходимости задействовать процессор.

Если это не сломалось, не исправить это. Это не сломалось.

Основная проблема - непривязанные доступы. Они переходят от плохого к действительно плохому, в зависимости от того, на какой архитектуре вы работаете. Во многом это связано с программистами, некоторые с компиляторами.

Самый дешевый способ исправить memcpy - это не использовать его, выровнять данные по хорошим границам и использовать или создать альтернативу memcpy, которая поддерживает только хорошо выровненные, блочные копии. Еще лучше было бы иметь переключатель компилятора, чтобы пожертвовать программным пространством и оперативной памятью ради скорости. у людей или языков, которые используют много структур, таких, что компилятор внутренне генерирует вызовы memcpy или любого другого языкового эквивалента, их структуры будут расти так, что между ними будет отступ или заполнение. 59-байтовая структура может стать 64 байтами вместо этого. malloc или альтернатива, которая только дает указатели на адрес, выровненный как указано. и т. д.

Гораздо проще сделать все это самостоятельно. Выровненный malloc, структуры, кратные размеру выравнивания. Ваш собственный memcpy, который выровнен, и т. Д. С тем, что это так просто, почему аппаратные ребята испортили бы свои проекты, компиляторы и пользователей? для этого нет бизнес-обоснования.

Другая причина в том, что тайники изменили картину. Ваш драм доступен только в фиксированном размере, 32 бита, 64 бита, что-то в этом роде, любые прямые обращения меньше, чем это огромный удар по производительности. Поместите кеш перед тем, как падение производительности идет вниз, любое чтение-изменение-запись происходит в кеше, а модификация допускает множественные модификации для одного чтения и записи драм. Вы по-прежнему хотите уменьшить количество циклов памяти для кэша, да, и вы все еще можете увидеть выигрыш в производительности, сгладив его с помощью функции переключения передач (8-битная первая передача, 16-битная вторая передача, 32-битная третья передача, 64 крейсерская скорость, 32 бита вниз, 16 бит вниз, 8 бит вниз)

Я не могу говорить за Intel, но знаю, что такие люди, как ARM, сделали то, что вы просите

ldmia r0!,{r2,r3,r4,r5}

например, все еще четыре 32-битные передачи, если ядро ​​использует 32-битный интерфейс. но для 64-битных интерфейсов, если выровнены по 64-битной границе, это становится 64-битной передачей с длиной два, один набор согласований между сторонами и два 64-битных слова перемещаются. Если он не выровнен по 64-битной границе, он становится тремя передачами: один 32-битный, один 64-битный, а затем 32-битный. Вы должны быть осторожны: если это аппаратные регистры, которые могут не работать в зависимости от конструкции логики регистра, если она поддерживает только 32-битные передачи, вы не можете использовать эту инструкцию для этого адресного пространства. Понятия не имею, почему вы все равно попробуете что-то подобное.

Последний комментарий... это больно, когда я делаю это... ну, не делай этого. Не делайте ни единого шага в копиях памяти. следствием этого является то, что никто не сможет изменить конструкцию аппаратного обеспечения, чтобы упростить для пользователя пошаговую копию памяти, этот вариант использования настолько мал, что его не существует. Возьмите все компьютеры, использующие этот процессор, работающие на полной скорости днем ​​и ночью, по сравнению со всеми компьютерами, которые были пошагово прошли через копии памяти и другой оптимизированный по производительности код. Это похоже на сравнение песчинки с шириной земли. Если вы один шаг, вам все равно придется пройти один шаг, каким бы ни было новое решение, если оно было. чтобы избежать огромных задержек прерываний, настроенный вручную memcpy по-прежнему будет начинаться с if-then-else (если слишком малая копия просто перейдет в небольшой набор развернутого кода или в цикл байтового копирования), а затем перейдет к серии копий блоков в некоторая оптимальная скорость без ужасного размера задержки. Вам все равно придется пройти через это.

чтобы выполнить пошаговую отладку, вы должны все равно скомпилировать запутанный, медленный код, самый простой способ решить один шаг через проблему memcpy, - это иметь компилятор и компоновщик, когда ему говорят, что нужно строить для отладки, собирать и связывать с не -оптимизированная memcpy или альтернативная неоптимизированная библиотека в целом. GNU / GCC и llvm с открытым исходным кодом, вы можете заставить их делать все, что вы хотите.

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