Использование ассемблера в C / C ++ - PullRequest
43 голосов
/ 17 ноября 2010

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

1002 Эта практика еще продолжается?и Как это сделать? Не слишком ли громоздко и архаично писать на ассемблере? Когда мы компилируем код C (с флагом -O3 или без него), компилятор делаетнекоторая оптимизация кода и связывание всех библиотек и преобразование кода в двоичный объектный файл.Поэтому, когда мы запускаем программу, она уже находится в ее самой основной форме, то есть в двоичном виде.Так как же помогает создание языка ассемблера?

Я пытаюсь понять эту концепцию, и любая помощь или ссылки приветствуются.

ОБНОВЛЕНИЕ: Перефразированиепункт 3 в соответствии с просьбой dbemerlin - поскольку вы, возможно, сможете написать более эффективный код сборки, чем генерирует компилятор, но если вы не являетесь экспертом по ассемблеру, ваш код будет работать медленнее, потому что часто компилятор оптимизирует код лучше, чем большинство людей.*

Ответы [ 13 ]

28 голосов
/ 17 ноября 2010

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

  • инструкции ЦП не имеют функциональных эквивалентов в C ++ (например, инструкции с одной инструкцией и несколькими данными, BCDили десятичные арифметические операции)

    ИЛИ

  • по какой-то необъяснимой причине - оптимизатору не удается использоватьлучшие инструкции для CPU

... AND ...

  • использование этих инструкций CPU дало бы значительную и полезную производительностьповысить код до узкого места.

Простое использование встроенной сборки для выполнения операции, которая можетn легко выразить в C ++ - как добавление двух значений или поиск в строке - активно контрпродуктивно, потому что:

  • компилятор знает, как сделать это одинаково хорошо
    • , чтобы проверить этопосмотрите на вывод его сборки (например, gcc -S) или разберите машинный код
  • , вы искусственно ограничиваете его выбор в отношении выделения регистров, инструкций процессора и т. д., так что это может занять больше времени.чтобы подготовить регистры процессора со значениями, необходимыми для выполнения вашей жестко запрограммированной инструкции, затем дольше вернуться к оптимальному распределению для будущих инструкций
    • оптимизаторы компилятора могут выбирать между инструкциями эквивалентной производительности, задающими разные регистры, чтобы минимизировать копирование между нимии может выбирать регистры таким образом, чтобы одно ядро ​​могло обрабатывать несколько инструкций в течение одного цикла, в то время как форсирование всего через определенные регистры, если честно, сериализовало бы его
      • , у GCC есть способы выразить потребности в определенныхIC типов регистров, не ограничивая процессор точным регистром, но все же допускает такую ​​оптимизацию, но это единственная встроенная сборка, которую я когда-либо видел, которая обращается к этому
  • если в следующем году выйдет новая модель процессора с другой инструкцией, которая на 1000% быстрее для той же логической операции, то поставщик компилятора с большей вероятностью обновит свой компилятор, чтобы использовать эту инструкцию, и, следовательно, ваша программа получит выгоду после перекомпиляции, чем вы.(или кто будет поддерживать программное обеспечение в таком случае)
  • компилятор выберет оптимальный подход для целевой архитектуры, о которой ему сказано: если вы жестко закодируете одно решение, то оно должно быть с наименьшим общим знаменателем или #ifdef -ed для ваших платформ
  • язык ассемблера не так переносим, ​​как C ++, как между процессорами, так и между компиляторами, и даже если вы, казалось бы, портируете инструкцию, можно ошибиться при регистрации регистров, которые безопасны дляClobber, соглашения о передаче аргументов и т. д.
  • другие программисты могут не знать или не довольствоваться сборкой

Одна перспектива, которую, я думаю, стоит иметь в виду, состоит в том, что, когда был представлен C, ему пришлось завоевать внимание многих программистов на жестком ассемблере, которые разбирались в генерируемом машинном коде.Тогда у машин было меньше ресурсов процессора и оперативной памяти, и вы можете поспорить, что люди суетятся из-за крошечной вещи.Оптимизаторы стали очень сложными и продолжали совершенствоваться, в то время как языки ассемблера процессоров, таких как x86, становились все более сложными, как и их конвейеры выполнения, кэши и другие факторы, влияющие на их производительность.Вы больше не можете просто добавлять значения из таблицы циклов на инструкцию.Авторы компиляторов тратят время на рассмотрение всех этих тонких факторов (особенно тех, которые работают на производителей процессоров, но это также увеличивает нагрузку на другие компиляторы).Сейчас программистам на ассемблере нецелесообразно усреднять - по любому нетривиальному приложению - значительно лучшую эффективность кода, чем сгенерированный хорошим оптимизирующим компилятором, и они в подавляющем большинстве случаев будут работать хуже.Таким образом, использование сборки должно быть ограничено временем, когда оно действительно измеримо и полезно, стоит затрат на соединение и обслуживание.

14 голосов
/ 17 ноября 2010

Прежде всего, вам необходимо профилировать вашу программу. Затем вы оптимизируете наиболее часто используемые пути в коде C или C ++. Если преимущества не очевидны, вы не переписываете на ассемблере . Использование ассемблера делает ваш код сложным в обслуживании и намного менее переносимым - это того не стоит, за исключением очень редких ситуаций.

10 голосов
/ 17 ноября 2010

(1) Да, самый простой способ попробовать это - использовать встроенную сборку, это зависит от компилятора, но обычно выглядит примерно так:

__asm
{
    mov eax, ebx
}

(2) Это очень субъективно

(3) Поскольку вы могли бы написать более эффективный код сборки, чем генерирует компилятор.

5 голосов
/ 17 ноября 2010

Вы должны прочитать классическую книгу Zen of Code Optimization и продолжение Zen of Graphics Programming от Майкла Абраша .

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

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

Еще одна причина в том, что компиляторы в настоящее время достаточно хороши и агрессивно оптимизируют, как правило, гораздо больше производительности для работы над алгоритмами, которые преобразуют код C в ассемблер.Даже для программирования на GPU (графических картах) вы можете сделать это с помощью C, используя cuda или OpenCL.

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

4 голосов
/ 17 ноября 2010

В наши дни очень мало причин использовать язык ассемблера, даже низкоуровневые конструкции, такие как SSE и более старый MMX, имеют встроенные встроенные функции как в gcc, так и в MSVC (я уверен, что icc тоже никогда не использовал).

Честно говоря, оптимизаторы в наши дни настолько безумно агрессивны, что большинство людей не может сравниться даже с половиной своей производительности при написании кода на ассемблере. Вы можете изменить порядок упорядочения данных в памяти (для локальности) или рассказать компилятору больше о вашем коде (через #pragma), но на самом деле писать ассемблерный код ... сомневаюсь, что вы получите от него что-то дополнительное.

@ VJo, обратите внимание, что использование встроенных функций в коде высокого уровня C позволит вам выполнять те же оптимизации без использования одной инструкции сборки.

И что бы это ни стоило, обсуждались следующий компилятор Microsoft C ++ и то, как они будут удалять встроенную сборку из него. Это говорит о необходимости этого.

3 голосов
/ 17 ноября 2010

Не думаю, что вы указали процессор.Различные ответы в зависимости от процессора и среды.Общий ответ: да, это все еще сделано, это не архаично, конечно.Основная причина - компиляторы, иногда они хорошо справляются с оптимизацией в целом, но не очень хорошо для конкретных целей.Некоторые действительно хороши в одной цели и не так хороши в других.В большинстве случаев это достаточно хорошо, в большинстве случаев вам нужен переносимый код на C, а не непереносимый ассемблер.Но вы по-прежнему обнаруживаете, что библиотеки C будут по-прежнему вручную оптимизировать memcpy и другие подпрограммы, которые компилятор просто не может понять, что существует очень быстрый способ его реализации.Частично, потому что этот угловой случай не стоит тратить время на оптимизацию компилятора, просто решите его в ассемблере, и система сборки имеет много, если эта цель использует C, если эта цель использует C, если эта цель использует asm, если этоцелевое использование asm.Так что это все еще происходит, и я утверждаю, что должно продолжаться вечно в некоторых областях.

X86 - это собственный зверь с большой историей, мы находимся в точке, где вы действительно не можете на практике написать один шарик ассемблераэто всегда быстрее, вы можете определенно оптимизировать подпрограммы для конкретного процессора на конкретной машине в определенный день и не выполнять компилятор.За исключением некоторых конкретных случаев, это, как правило, бесполезно.Образовательный, но в целом не стоит времени.Также обратите внимание, что процессор больше не является узким местом, поэтому небрежный универсальный компилятор C достаточно хорош, найдите производительность в другом месте.

Другие платформы, которые часто означают встроенные, arm, mips, avr, msp430, pic и т. Д.Вы можете запускать или не запускать операционную систему, вы можете запускать или не использовать кеш или другие подобные вещи, которые есть на вашем рабочем столе.Так что слабые стороны компилятора покажут.Также обратите внимание, что языки программирования продолжают эволюционировать от процессоров, а не к ним.Даже в случае C, который, возможно, считается языком низкого уровня, он не соответствует набору команд.Всегда будут времена, когда вы можете создавать сегменты ассемблера, которые превосходят компилятор.Не обязательно сегмент, который является вашим узким местом, но во всей программе вы можете часто вносить улучшения здесь и там.Вы все еще должны проверить ценность этого.Во встроенной среде это может и делает разницу между успехом и провалом продукта.Если ваш продукт имеет 25 долларов США на единицу, инвестируемую в более энергоемкую, платную недвижимость, высокоскоростные процессоры, так что вам не нужно использовать ассемблер, но ваш конкурент тратит 10 долларов или меньше на единицу и готов смешать asm с C, чтобы использовать меньшую память,использовать меньше энергии, более дешевые детали и т. д. До тех пор, пока NRE будет восстановлено, в конечном итоге смешанный с ассемблерным раствором будет.

True встроенный - специализированный рынок со специализированными инженерамиДругой рынок встраиваемых систем, ваши встроенные linux roku, tivo и т. Д. Встраиваемые телефоны и т. Д. Для выживания нужны портативные операционные системы, поскольку вам нужны сторонние разработчики.Таким образом, платформа должна быть больше похожа на настольный компьютер, чем на встроенную систему.Как уже упоминалось, в библиотеке C или операционной системе могут быть некоторые оптимизации для ассемблера, но, как и в случае с рабочим столом, вы хотите попытаться использовать больше оборудования, чтобы программное обеспечение можно было переносить, а не оптимизировать вручную.И ваша линейка продуктов или встроенная операционная система потерпит неудачу, если для успеха третьей стороны потребуется ассемблер.

Самое большое беспокойство у меня заключается в том, что эти знания теряются с угрожающей скоростью. Потому что никто не проверяет ассемблер, потому что никто не пишет на ассемблере и т. Д. Никто не замечает, что компиляторы не улучшаются, когда дело доходит до создаваемого кода. Разработчики часто думают, что им нужно покупать больше оборудования вместо того, чтобы понимать, что, зная компилятор или как лучше программировать, они могут повысить свою производительность на 5 - несколько сотен процентов с помощью того же компилятора, иногда с тем же исходным кодом. 5-10% обычно с одинаковым исходным кодом и компилятором. gcc 4 не всегда производит лучший код, чем gcc 3, я держу оба, потому что иногда gcc3 работает лучше. Конкретные целевые компиляторы могут (не всегда) обходить gcc, вы можете увидеть улучшение на несколько сотен процентов, иногда с одним и тем же исходным кодом и другим компилятором. Откуда все это? Люди, которые все еще пытаются найти и / или использовать ассемблер. Некоторые из этих людей работают с бэкэндами компилятора. Интерфейс и середина - это, конечно, весело и познавательно, но во внутреннем интерфейсе вы создаете или нарушаете качество и производительность получаемой программы. Даже если вы никогда не пишете на ассемблере, а время от времени просматриваете только выходные данные компилятора (gcc -O2 -s myprog.c), это сделает вас лучшим программистом высокого уровня и сохранит некоторые из этих знаний. Если никто не хочет знать и писать на ассемблере, то по определению мы прекратили писать и поддерживать компиляторы для языков высокого уровня, и программное обеспечение в целом прекратит свое существование.

Поймите, что, например, для gcc вывод компилятора - это сборка, которая передается ассемблеру, который превращает его в объектный код. Компилятор C обычно не создает двоичные файлы. Объекты, когда они объединены в окончательный двоичный файл, выполняются компоновщиком, еще одной программой, которая вызывается компилятором, а не частью компилятора. Компилятор превращает C или C ++ или ADA или что-то еще в ассемблер, тогда инструменты ассемблера и компоновщика делают это до конца. Динамические перекомпиляторы, такие как, например, tcc, должны каким-то образом генерировать двоичные файлы, но я вижу, что это исключение, а не правило. LLVM имеет собственное решение времени выполнения, а также достаточно наглядно показывает высокий уровень внутреннего кода для целевого кода и двоичного пути, если вы используете его в качестве кросс-компилятора.

Итак, вернемся к делу, да, это делается чаще, чем вы думаете. В основном это связано с тем, что язык не сравнивается напрямую с набором команд, а затем компилятор не всегда генерирует достаточно быстрый код. Если вам удастся, скажем, в десятки раз улучшить интенсивно используемые функции, такие как malloc или memcpy. Или хотите иметь HD-видеоплеер на вашем телефоне без аппаратной поддержки, балансировать плюсы и минусы ассемблера. По-настоящему встраиваемые рынки все еще используют ассемблер довольно часто, иногда это все на C, но иногда программное обеспечение полностью кодируется на ассемблере. Для настольного компьютера x86 процессор не является узким местом. Процессоры имеют микрокодирование. Даже если вы сделаете красиво выглядящий ассемблер на поверхности, он не будет работать очень быстро на всех процессорах семейства x86, небрежный, достаточно хороший код, скорее всего, будет работать примерно одинаково по всем направлениям.

Я настоятельно рекомендую изучать ассемблер для ISA не x86, таких как arm, thumb / thumb2, mips, msp430, avr. Цели, в которых есть компиляторы, особенно с поддержкой компиляторов gcc или llvm. Изучите ассемблер, научитесь понимать выходные данные компилятора C и докажите, что вы можете добиться большего успеха, фактически изменив этот вывод и протестировав его. Эти знания помогут сделать код высокого уровня вашего рабочего стола намного лучше без ассемблера, быстрее и надежнее.

3 голосов
/ 17 ноября 2010

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

Предполагая, что приличный компилятор выпущен в течение последних двух лет, вы обычно можете настроить свой код C / C ++, чтобы получить то же преимущество в производительности, что и при использовании ассемблера.

Многие люди в комментариях и ответах здесь говорят о «ускорении N раз», которое они получили, переписывая что-то в сборке, но это само по себе не значит слишком много. Я получил ускорение в 13 раз от переписывания функции C, оценивающей уравнения гидродинамики в C , путем применения многих из тех же оптимизаций, которые вы сделали бы, если бы вы писали это в сборке, зная аппаратное обеспечение и профилирование. В конце концов, он достаточно приблизился к теоретической пиковой производительности ЦП, чтобы не было никакого смысла * переписывать его в сборке. Обычно ограничивающим фактором является не язык, а сам код, который вы написали. Пока вы не используете «специальные» инструкции, с которыми сталкивается компилятор, трудно превзойти хорошо написанный код C ++.

Сборка не волшебно быстрее. Это просто выводит компилятор из цикла. Это часто плохо, если вы действительно не знаете, что делаете, так как компилятор выполняет много оптимизаций, которые действительно очень больно делать вручную. Но в редких случаях компилятор просто не понимает ваш код и не может сгенерировать эффективную сборку для него, и , а затем , может быть полезно написать какую-то сборку самостоятельно. Кроме разработки драйверов и т. П. (Где вам нужно напрямую манипулировать оборудованием), единственное место, о котором я могу думать, где может стоить написание сборки, - это если вы застряли с компилятором, который не может генерировать эффективный код SSE из внутренние (такие как MSVC). Даже там, я все еще начинаю использовать встроенные функции в C ++, и профилирую его, и пытаюсь настроить его как можно больше, но поскольку компилятор просто не очень хорош в этом, возможно, в конечном итоге стоило бы переписать этот код в сборе.

2 голосов
/ 17 ноября 2010

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

1 голос
/ 19 мая 2019
  1. "Эта практика еще сделана?" -> Это делается в обработке изображений, обработке сигналов, искусственном интеллекте (например, эффективном умножении матриц) и других. Я бы поспорил, что обработка жеста прокрутки на моем трекпаде macbook также является частично ассемблерным кодом, потому что он немедленный. -> Это даже делается в приложениях C # (см. https://blogs.msdn.microsoft.com/winsdk/2015/02/09/c-and-fastcall-how-to-make-them-work-together-without-ccli-shellcode/)

  2. "Не слишком ли громоздко и архаично писать на ассемблере?" -> Это инструмент, похожий на молоток или отвертку, а для некоторых задач требуется часовая отвертка.

    1. "Когда мы компилируем код C (с флагом -O3 или без него), компилятор выполняет некоторую оптимизацию кода ... Так как же помогает создание языка ассемблера?" -> Мне нравится то, что сказал @jalf, то, что написание C-кода так, как вы бы писали на ассемблере, уже приведет к эффективному коду. Однако, чтобы сделать это, вы должны подумать, как написать код на ассемблере, например. понять все места, где данные копируются (и чувствовать боль каждый раз, когда это не нужно). С языком ассемблера вы можете быть уверены, какие инструкции генерируются. Даже если ваш код на C эффективен, нет гарантии, что результирующая сборка будет эффективной с каждым компилятором. (см. https://lucasmeijer.com/posts/cpp_unity/) -> На языке ассемблера, когда вы распространяете двоичный файл, вы можете тестировать процессор и создавать различные ветви в зависимости от функций процессора, оптимизированных для AVX или только для SSE, но вам нужно распространять только один двоичный файл. При использовании встроенных функций это также возможно в C ++ или .NET Core 3. (см. https://devblogs.microsoft.com/dotnet/using-net-hardware-intrinsics-api-to-accelerate-machine-learning-scenarios/)
1 голос
/ 17 ноября 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...