Подтвердить что ты не робот

Использование языка ассемблера в C/С++

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

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

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

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

4b9b3361

Ответ 1

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

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

    ИЛИ ЖЕ

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

...А ТАКЖЕ...

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

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

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

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

Ответ 2

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

Ответ 3

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

__asm
{
    mov eax, ebx
}

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

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

Ответ 4

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

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

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

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

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

Ответ 5

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

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

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

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

Ответ 6

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

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

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

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

Ответ 7

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

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

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

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

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

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

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

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

Ответ 8

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

Ответ 9

В моей работе я использовал сборку встроенной цели (микроконтроллер) для доступа на низком уровне.

Но для программного обеспечения для ПК я не думаю, что это очень полезно.

Ответ 10

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

Мой пример был, когда я писал контроллер принтера, и была функция, которая должна была вызываться каждые 50 микросекунд. Он должен делать перестановку бит, более или менее. Используя C, я смог сделать это примерно через 35 минут, а с помощью сборки я сделал это примерно через 8 микросекунд. Это очень специфическая процедура, но все же, что-то реальное и необходимое.

Ответ 11

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

Ответ 12

  1. "Эта практика все еще сделана?" → Это делается в обработке изображений, обработке сигналов, искусственном интеллекте (например, эффективном умножении матриц) и других. Я бы поспорил, что обработка жеста прокрутки на моем трекпаде macbook также является частично ассемблерным кодом, потому что он немедленный. → Это даже делается в приложениях С# (см. Https://blogs.msdn.microsoft.com/winsdk/2015/02/09/c-and-fastcall-how-to-make-them-work-together- без-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/)

Ответ 13

  • Да. Используйте встроенные модули сборки или сборки сборки. Какой метод вы должны использовать, зависит от того, сколько кода сборки вам нужно написать. Обычно это нормально, чтобы использовать встроенную сборку для нескольких строк и один раз переключать на отдельные объектные модули, если это более чем одна функция.
  • Определенно, но иногда это необходимо. Важным примером здесь будет программирование операционной системы.
  • Большинство компиляторов сегодня оптимизируют код, который вы пишете на высокоуровневом языке, намного лучше, чем кто-либо мог написать код сборки. Люди в основном используют его для написания кода, который в противном случае невозможно было бы писать на языке высокого уровня, таком как C. Если кто-то использует его для чего-либо еще, значит, он лучше оптимизирован, чем современный компилятор (я сомневаюсь) или просто глупо, например он не знает, какие флагов компилятора или функциональные атрибуты использовать.