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

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

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

rep stos

и

rep movs

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

Ответ 1

Стоимость.

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

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

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

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

Общее назначение против специализированного

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

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

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

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

Закон убывающих результатов

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

Ответ 4

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

Ответ 5

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

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

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

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

Другая причина заключается в том, что кеши меняют изображение. ваш драм доступен только в фиксированном размере, 32 бита 64 бит, что-то вроде этого, любые прямые обращения, меньшие, чем это, являются огромным хитом производительности. Поместите кеш перед тем, как производительность снижается, любая запись чтения-изменения-записи происходит в кеше с модификацией, позволяющей изменять mulitple для одного чтения и записи драм. Вы по-прежнему хотите уменьшить количество циклов памяти в кеше, да, и вы все еще можете увидеть прирост производительности, сгладив это с помощью механизма переключения передач (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-разрядную передачу, которую вы не можете использовать для этой адресной памяти. Не поймите, почему вы все равно попробуете что-то подобное.

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

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

Ответ 6

Время от времени rep movsb было оптимальным решением.

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

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

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