Одна из заявленных причин для знания ассемблера заключается в том, что иногда его можно использовать для написания кода, который будет более производительным, чем написание этого кода на языке более высокого уровня, в частности C. Тем не менее, я также слышал, как много раз говорилось, что, хотя это не совсем ложно, случаи, когда ассемблер действительно может быть использован для генерации более производительного кода, крайне редки и требуют экспертных знаний и опыта сборки.
Этот вопрос даже не касается того факта, что инструкции на ассемблере будут специфичными для машины и непереносимыми, или каких-либо других аспектов ассемблера. Конечно, есть много веских причин для знания ассемблера, но это должен быть конкретный вопрос, требующий примеров и данных, а не расширенный дискурс по ассемблеру и языкам более высокого уровня.
Может ли кто-нибудь привести конкретные примеры случаев, когда сборка будет быстрее, чем хорошо написанный код C с использованием современного компилятора, и можете ли вы подтвердить это утверждение профилирующим доказательством? Я вполне уверен, что эти случаи существуют, но я действительно хочу точно знать, насколько эзотеричны эти случаи, так как это, кажется, является предметом некоторого спора.
источник
-O3
флага, вам, вероятно, лучше оставить оптимизацию для компилятора C :-)Ответы:
Вот реальный пример: умножение с фиксированной запятой на старых компиляторах.
Они не только пригодятся на устройствах без плавающей запятой, они сияют, когда дело доходит до точности, поскольку они дают 32 бита точности с предсказуемой ошибкой (у плавающего есть только 23 бита, и труднее предсказать потерю точности). т. е. равномерная абсолютная точность по всему диапазону вместо почти одинаковой относительной точности (
float
).Современные компиляторы прекрасно оптимизируют этот пример с фиксированной запятой, поэтому для более современных примеров, которые все еще нуждаются в специфичном для компилятора коде, см.
uint64_t
32x32 => 64-битное умножение, не может быть оптимизирована на 64-битном процессоре, поэтому вам нужны встроенные или__int128
эффективный код на 64-битных системах.C не имеет оператора полного умножения (2N-битный результат из N-битных входов). Обычный способ выразить это в C - привести входные данные к более широкому типу и надеяться, что компилятор распознает, что старшие биты входных данных не интересны:
Проблема с этим кодом заключается в том, что мы делаем то, что не может быть прямо выражено на языке Си. Мы хотим умножить два 32-битных числа и получить 64-битный результат, из которого мы возвращаем средний 32-битный. Однако в C это умножение не существует. Все, что вы можете сделать, это повысить целые числа до 64 бит и сделать умножение 64 * 64 = 64.
Однако x86 (и ARM, MIPS и другие) могут выполнять умножение в одной инструкции. Некоторые компиляторы игнорировали этот факт и генерировали код, который вызывает функцию библиотеки времени выполнения для выполнения умножения. Сдвиг на 16 также часто выполняется библиотечной подпрограммой (такой же сдвиг может выполнять и x86).
Таким образом, у нас остается один или два библиотечных вызова только для умножения. Это имеет серьезные последствия. Сдвиг не только медленнее, регистры должны сохраняться в вызовах функций, а также не помогают вставка и развертывание кода.
Если вы переписываете тот же код на (встроенном) ассемблере, вы можете значительно увеличить скорость.
В дополнение к этому: использование ASM - не лучший способ решения проблемы. Большинство компиляторов позволяют вам использовать некоторые инструкции на ассемблере во внутренней форме, если вы не можете выразить их в C. Например, компилятор VS.NET2008 выставляет 32 * 32 = 64-битное значение mul как __emul, а 64-битное смещение как __ll_rshift.
Используя встроенные функции, вы можете переписать функцию таким образом, чтобы у C-компилятора была возможность понять, что происходит. Это позволяет встроить код, распределить регистр, исключить общее подвыражение и постоянное распространение. Вы получите огромныйТаким образом, улучшение производительности по сравнению с рукописным ассемблерным кодом.
Для справки: конечный результат для mul с фиксированной точкой для компилятора VS.NET:
Разница в производительности делителей с фиксированной точкой еще больше. У меня были улучшения до 10 раз для тяжелого кода с фиксированной запятой, написав пару asm-строк.
Использование Visual C ++ 2013 дает одинаковый код сборки для обоих способов.
gcc4.1 из 2007 также хорошо оптимизирует версию на чистом C. (В проводнике компилятора Godbolt не было установлено более ранних версий gcc, но, вероятно, даже более старые версии GCC могли бы делать это без встроенных функций.)
См. Source + asm для x86 (32-бит) и ARM в проводнике компилятора Godbolt . (К сожалению, у него нет достаточно старых компиляторов для создания плохого кода из простой версии на чистом C).
Современные процессоры могут делать вещи C не имеют операторов для вообще , как
popcnt
и битые-сканирование , чтобы найти первый или последний набор бит . (POSIX имеетffs()
функцию, но ее семантика не соответствует x86bsf
/bsr
. См. Https://en.wikipedia.org/wiki/Find_first_set ).Некоторые компиляторы могут иногда распознавать цикл, который подсчитывает количество установленных битов в целом числе и компилировать его в
popcnt
инструкцию (если она включена во время компиляции), но гораздо надежнее использовать__builtin_popcnt
в GNU C или в x86, если вы только нацеливание оборудования с SSE4.2:_mm_popcnt_u32
от<immintrin.h>
.Или в C ++, присвойте
std::bitset<32>
и используйте.count()
. (Это тот случай, когда язык нашел способ портативного представления оптимизированной реализации popcount через стандартную библиотеку, таким образом, который всегда будет компилироваться во что-то правильное и может использовать все, что поддерживает цель.) См. Также https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .Аналогично,
ntohl
может компилироваться вbswap
(32-битный байт подкачки x86 для преобразования в порядковый номер) в некоторых реализациях C, которые его имеют.Другая важная область для встроенных функций или рукописного асма - это ручная векторизация с инструкциями SIMD. Компиляторы не плохи с простыми циклами, как
dst[i] += src[i] * 10.0;
, но часто плохо или вообще не векторизации, когда все становится сложнее. Например, вы вряд ли получите что-то вроде Как реализовать Atoi с помощью SIMD? автоматически генерируется компилятором из скалярного кода.источник
Много лет назад я учил кого-то программировать на C. Упражнение состояло в том, чтобы повернуть изображение на 90 градусов. Он вернулся с решением, которое заняло несколько минут, в основном потому, что он использовал умножения и деления и т. Д.
Я показал ему, как исправить проблему, используя сдвиги битов, и время для обработки на неоптимизирующем компиляторе, которое он имел, сократилось примерно до 30 секунд.
Я только что получил оптимизирующий компилятор, и тот же код поворачивал графику за <5 секунд. Я посмотрел на ассемблерный код, который генерировал компилятор, и из того, что я увидел, решил тут же, что мои дни написания ассемблера закончились.
источник
add di,di / adc al,al / add di,di / adc ah,ah
и т. Д. Для всех восьми 8-битных регистров, затем снова делать все 8 регистров и затем повторять всю процедуру три еще раз, и, наконец, сохранить четыре слова в Ax / BX / CX / DX. Никоим образом ассемблер не приблизится к этому.Практически всегда, когда компилятор видит код с плавающей запятой, рукописная версия будет быстрее, если вы используете старый плохой компилятор. ( Обновление 2019 года: в целом это не так для современных компиляторов. Особенно при компиляции для чего-либо, кроме x87; компиляторам проще работать с SSE2 или AVX для скалярной математики или с любыми не x86 с плоским регистром FP, в отличие от х87 стек регистров.)
Основная причина заключается в том, что компилятор не может выполнять какие-либо робастные оптимизации. Смотрите эту статью из MSDN для обсуждения на эту тему. Вот пример, где версия сборки в два раза быстрее, чем версия C (скомпилирована с VS2K5):
И некоторые цифры с моего ПК, на котором запущена версия по умолчанию * :
Из интереса я поменял цикл с помощью dec / jnz, и это не имело никакого значения для времени - иногда быстрее, иногда медленнее. Я предполагаю, что ограниченный объем памяти затмевает другие оптимизации. (Примечание редактора: более вероятно, что узкое место задержки FP достаточно, чтобы скрыть дополнительные затраты
loop
. Выполнение двух сумм Кахана параллельно для нечетных / четных элементов и добавление их в конце может ускорить это в 2 раза. )Ой, я запустил немного другую версию кода, и он вывел числа неправильно (т.е. C был быстрее!). Исправлены и обновлены результаты.
источник
-ffast-math
. У них есть уровень оптимизации,-Ofast
который в настоящее время эквивалентен-O3 -ffast-math
, но в будущем может включать в себя больше оптимизаций, которые могут привести к неправильной генерации кода в угловых случаях (таких как код, основанный на NaN IEEE).a+b == b+a
), но не ассоциативным (переупорядочение операций, поэтому округление промежуточных соединений отличается). Re: этот код: я не думаю, что без комментариев x87 иloop
инструкция очень удивительная демонстрация fast asm.loop
очевидно, на самом деле не является узким местом из-за задержки FP. Я не уверен, конвейеризует ли он операции FP или нет; x87 трудно читать людям. Дваfstp results
insns в конце явно не оптимальны. Вытащить дополнительный результат из стека было бы лучше сделать с не магазином. Как иfstp st(0)
IIRC.Не предоставляя какого-либо конкретного примера или свидетельства профилировщика, вы можете написать лучший ассемблер, чем компилятор, если знаете больше, чем компилятор.
В общем случае современный компилятор C знает гораздо больше о том, как оптимизировать рассматриваемый код: он знает, как работает конвейер процессора, он может попытаться переупорядочить инструкции быстрее, чем это может сделать человек, и так далее - это в основном так же, как компьютер так же хорош или лучше, чем лучший игрок в настольные игры и т. д. просто потому, что он может выполнять поиск в проблемном пространстве быстрее, чем большинство людей. Хотя вы теоретически можете работать так же хорошо, как и компьютер в конкретном случае, вы, конечно, не можете делать это с той же скоростью, что делает его невозможным для более чем нескольких случаев (т. Е. Компилятор наверняка превзойдет вас, если вы попытаетесь написать несколько процедур в ассемблере).
С другой стороны, есть случаи, когда компилятор не имеет такого большого количества информации - я бы сказал, прежде всего, при работе с различными формами внешнего оборудования, о которых компилятору ничего не известно. Основным примером, вероятно, являются драйверы устройств, где ассемблер в сочетании с глубоким знанием рассматриваемого оборудования человеком может дать лучшие результаты, чем компилятор Си.
Другие упоминали специальные инструкции, о чем я говорю в параграфе выше - инструкции, о которых компилятор может иметь ограниченные знания или вообще не знать их, что позволяет человеку быстрее писать код.
источник
ocamlopt
пропускает планирование команд на x86 и вместо этого оставляет его на ЦП, потому что он может более эффективно переупорядочивать во время выполнения.В моей работе есть три причины, по которым я должен знать и использовать сборку. В порядке важности:
Отладка - я часто получаю библиотечный код с ошибками или неполной документацией. Я выясняю, что он делает, вступая на уровне сборки. Я должен делать это примерно раз в неделю. Я также использую его как инструмент для отладки проблем, в которых мои глаза не замечают идиоматическую ошибку в C / C ++ / C #. Глядя на сборку, это проходит.
Оптимизация - компилятор неплохо справляется с оптимизацией, но я играю не так, как большинство. Я пишу код обработки изображений, который обычно начинается с кода, который выглядит следующим образом:
«сделать что-то часть» обычно происходит порядка нескольких миллионов раз (т. е. от 3 до 30). Соскребая циклы в этой фазе «сделать что-то», выигрыш в производительности значительно увеличивается. Я обычно не начинаю там - я обычно начинаю с того, что сначала пишу код для работы, а затем делаю все возможное, чтобы реорганизовать C, чтобы он был естественно лучше (лучший алгоритм, меньшая нагрузка в цикле и т. Д.). Мне обычно нужно читать ассемблер, чтобы увидеть, что происходит, и редко нужно его писать. Я делаю это возможно каждые два или три месяца.
делать то, что язык не позволит мне. К ним относятся - получение архитектуры процессора и конкретных функций процессора, доступ к флагам, не входящим в процессор (мужик, я действительно хотел бы, чтобы C предоставил вам доступ к флагу переноса) и т. Д. Я делаю это, возможно, раз в год или два года.
источник
Только при использовании некоторых специальных наборов инструкций компилятор не поддерживает.
Чтобы максимизировать вычислительную мощность современного ЦП с несколькими конвейерами и прогнозирующим ветвлением, вам необходимо структурировать программу сборки таким образом, чтобы сделать а) практически невозможным для человека писать б) еще более невозможно поддерживать.
Кроме того, улучшенные алгоритмы, структуры данных и управление памятью обеспечат вам как минимум на порядок большую производительность, чем микрооптимизации, которые вы можете выполнять при сборке.
источник
Хотя C "близок" к низкоуровневой обработке 8-битных, 16-битных, 32-битных, 64-битных данных, есть несколько математических операций, не поддерживаемых C, которые часто могут выполняться элегантно в определенных инструкциях сборки наборы:
Умножение с фиксированной запятой: произведение двух 16-битных чисел представляет собой 32-битное число. Но правила в Си говорят, что произведение двух 16-битных чисел - это 16-битное число, а произведение двух 32-битных чисел - это 32-битное число - нижняя половина в обоих случаях. Если вы хотите, чтобы верхняя половина умножалась на 16х16 или умножалась на 32х32, вы должны играть в игры с компилятором. Общий метод заключается в приведении к битовой ширине, которая больше необходимой, умножении, сдвиге вниз и приведении назад:
В этом случае компилятор может быть достаточно умен, чтобы знать, что вы на самом деле просто пытаетесь получить верхнюю половину умножения 16x16 и делать правильные вещи с собственным умножением 16x16m. Или это может быть глупо и требовать, чтобы библиотечный вызов умножил 32x32, что слишком много, потому что вам нужно всего лишь 16 бит продукта - но стандарт C не дает вам никакого способа выразить себя.
Определенные операции сдвига битов (ротация / переносы):
Это не слишком не элегантно в C, но, опять же, если компилятор не достаточно умен, чтобы понимать, что вы делаете, он будет выполнять много «ненужной» работы. Многие наборы инструкций по сборке позволяют вращать или сдвигать влево / вправо с результатом в регистре переноса, поэтому вы можете выполнить вышеизложенное в 34 инструкциях: загрузить указатель на начало массива, очистить перенос и выполнить 32. сдвиг вправо, используя автоинкремент по указателю.
В другом примере есть регистры сдвига с линейной обратной связью (LFSR), которые элегантно выполняются в сборке: возьмите блок из N битов (8, 16, 32, 64, 128 и т. Д.), Сдвиньте все это на 1 (см. Выше). алгоритма), тогда, если результирующий перенос равен 1, тогда вы XOR в битовой комбинации, которая представляет полином.
Сказав это, я бы не прибегал к этим методам, если у меня не было серьезных ограничений производительности. Как уже говорили другие, сборка намного сложнее документировать / отлаживать / тестировать / поддерживать, чем код C: выигрыш в производительности сопряжен с серьезными затратами.
редактировать: 3. Обнаружение переполнения возможно в сборке (на самом деле не может сделать это в C), это делает некоторые алгоритмы намного проще.
источник
Короткий ответ? Иногда.
Технически каждая абстракция имеет свою стоимость, а язык программирования - это абстракция работы процессора. С однако очень близко. Несколько лет назад я помню, как громко смеялся, когда я вошел в свою учетную запись UNIX и получил следующее сообщение об удаче (когда такие вещи были популярны):
Забавно, потому что это правда: C похож на портативный ассемблер.
Стоит отметить, что ассемблер просто работает, как вы пишете. Однако между C и языком ассемблера существует компилятор, и это чрезвычайно важно, потому что насколько быстро ваш код на C имеет для того, насколько хорош ваш компилятор.
Когда появился gcc, одна из вещей, которые сделали его настолько популярным, это то, что он часто был намного лучше, чем компиляторы C, которые поставлялись со многими коммерческими разновидностями UNIX. Мало того, что это был ANSI C (ни один из этих мусоров K & R C), он был более надежным и, как правило, создавал лучший (более быстрый) код. Не всегда, но часто.
Я говорю вам все это, потому что нет общего правила о скорости C и ассемблере, потому что нет никакого объективного стандарта для C.
Точно так же, ассемблер сильно различается в зависимости от того, какой процессор вы используете, какие у вас системные характеристики, какой набор инструкций вы используете и так далее. Исторически существовало два семейства процессорных архитектур: CISC и RISC. Самым крупным игроком в CISC была и остается архитектура Intel x86 (и набор инструкций). RISC доминировал в мире UNIX (MIPS6000, Alpha, Sparc и т. Д.). CISC выиграл битву за сердца и умы.
Во всяком случае, когда я был более молодым разработчиком, распространенной мудростью было то, что рукописный x86 часто мог быть намного быстрее, чем C, потому что, как работает архитектура, он имел сложность, которая приносила пользу человеку. RISC, с другой стороны, казался разработанным для компиляторов, поэтому никто (я знал) не писал, скажем, Sparc на ассемблере. Я уверен, что такие люди существовали, но, без сомнения, они оба сошли с ума и к настоящему моменту были институционализированы.
Наборы инструкций являются важным моментом даже в одном семействе процессоров. Некоторые процессоры Intel имеют такие расширения, как SSE - SSE4. У AMD были свои собственные инструкции SIMD. Преимущество такого языка программирования, как C, заключается в том, что кто-то может написать свою библиотеку, чтобы она была оптимизирована для любого процессора, на котором вы работали. Это была тяжелая работа на ассемблере.
В ассемблере по-прежнему есть оптимизации, которые не может сделать ни один компилятор, и хорошо написанный алгоритм ассемблера будет таким же быстрым или быстрым, как его эквивалент в Си. Большой вопрос: стоит ли это того?
В конечном счете, хотя ассемблер был продуктом своего времени и был более популярен в то время, когда циклы ЦП были дорогими. В настоящее время процессор, стоимость которого составляет 5-10 долларов (Intel Atom), может делать практически все, что угодно. Единственная реальная причина для написания ассемблера в наши дни - это низкоуровневые вещи, такие как некоторые части операционной системы (несмотря на то, что подавляющее большинство ядра Linux написано на C), драйверы устройств, возможно встроенные устройства (хотя C имеет тенденцию доминировать там). тоже) и тд. Или просто для ударов (что несколько мазохистски).
источник
Вариант использования, который может больше не применяться, но для вашего удовольствия: на Amiga ЦП и графические / аудио чипы будут бороться за доступ к определенной области ОЗУ (первые 2 МБ ОЗУ будут специфическими). Поэтому, когда у вас было только 2 МБ ОЗУ (или меньше), отображение сложной графики и воспроизведение звука снизили бы производительность процессора.
В ассемблере вы можете чередовать свой код таким умным способом, что ЦП будет пытаться получить доступ к ОЗУ только тогда, когда графические / аудиочипы заняты внутри (т.е. когда шина была свободна). Таким образом, переупорядочивая ваши инструкции, умело используя кэш ЦП, синхронизацию шины, вы могли достичь некоторых эффектов, которые были просто невозможны при использовании любого языка более высокого уровня, потому что вам приходилось синхронизировать каждую команду, даже вставлять NOP здесь и там, чтобы сохранить различные чипы друг от друга радар.
Это еще одна причина, по которой инструкция ЦПУ NOP (Без операции - ничего не делать) может на самом деле ускорить работу всего приложения.
[РЕДАКТИРОВАТЬ] Конечно, техника зависит от конкретной настройки оборудования. Это было основной причиной, по которой многие игры Amiga не могли справиться с более быстрыми процессорами: время выполнения инструкций было неверным.
источник
Укажите один, который не является ответом.
Даже если вы никогда не программируете это, я считаю полезным знать хотя бы один набор инструкций на ассемблере. Это часть бесконечного стремления программистов знать больше и, следовательно, быть лучше. Также полезно, когда вы заходите в фреймворки, у вас нет исходного кода и, по крайней мере, неточно понимаете, что происходит. Это также поможет вам понять JavaByteCode и .Net IL, так как они похожи на ассемблер.
Чтобы ответить на вопрос, когда у вас мало кода или много времени. Наиболее полезно для использования во встроенных чипах, где низкая сложность чипов и слабая конкуренция в компиляторах, ориентированных на эти чипы, могут перевесить баланс в пользу людей. Также для устройств с ограниченным доступом вы часто обмениваете размер кода / объем памяти / производительность таким образом, чтобы компилятору было сложно это сделать. например, я знаю, что это пользовательское действие не вызывается часто, поэтому у меня будет небольшой размер кода и низкая производительность, но эта другая функция, которая выглядит похожей, используется каждую секунду, поэтому у меня будет больший размер кода и более высокая производительность. Это тот тип компромисса, который может использовать опытный программист на ассемблере.
Я также хотел бы добавить, что есть много промежуточных положений, где вы можете кодировать в C компиляцию и исследовать созданную сборку, а затем либо изменить свой код C, либо настроить и поддерживать как сборку.
Мой друг работает над микроконтроллерами, в настоящее время чипами для управления маленькими электродвигателями. Он работает в комбинации низкого уровня c и Assembly. Однажды он рассказал мне о хорошем дне на работе, когда он сократил основной цикл с 48 инструкций до 43. Он также столкнулся с выбором, например, с расширением кода для заполнения чипа 256 Кбайт, и бизнес хочет новую функцию, не так ли?
Я хотел бы добавить, как коммерческий разработчик с большим количеством портфолио или языков, платформ, типов приложений, которые я никогда не испытывал необходимости погружаться в написание ассемблера. Я всегда ценил знания, которые я получил об этом. И иногда отлаживается в этом.
Я знаю, что гораздо больше ответил на вопрос «почему я должен изучать ассемблер», но я чувствую, что это более важный вопрос, чем когда он быстрее.
так что давайте попробуем еще раз. Вы должны думать о сборке
Не забудьте сравнить свою сборку с сгенерированным компилятором, чтобы увидеть, что быстрее / меньше / лучше.
Дэвид.
источник
sbi
иcbi
), которые компиляторы привыкли (а иногда и делают) не в полной мере воспользоваться из-за ограниченного знания аппаратного обеспечения.Я удивлен, что никто не сказал это.
strlen()
Функция гораздо быстрее , если написано в сборе! В C лучшее, что вы можете сделать, этово время сборки вы можете значительно ускорить его:
длина в ecx. Это сравнивает 4 символа за раз, так что это в 4 раза быстрее. И подумайте, используя старшее слово eax и ebx, оно станет в 8 раз быстрее , чем предыдущая процедура C!
источник
(word & 0xFEFEFEFF) & (~word + 0x80808080)
нулевом свойстве, если все байты в слове не равны нулю.Матричные операции с использованием инструкций SIMD, вероятно, быстрее, чем код, сгенерированный компилятором.
источник
Я не могу привести конкретные примеры, потому что это было слишком много лет назад, но было много случаев, когда рукописный ассемблер мог превзойти любой компилятор. Причины, почему:
Вы можете отклониться от соглашений о вызовах, передавая аргументы в регистрах.
Вы можете тщательно продумать, как использовать регистры, и избежать хранения переменных в памяти.
Для таких вещей, как таблицы переходов, вы можете избежать проверки границ индекса.
В основном, компиляторы выполняют довольно хорошую работу по оптимизации, и это почти всегда «достаточно хорошо», но в некоторых ситуациях (например, рендеринг графики), где вы платите дорого за каждый отдельный цикл, вы можете использовать ярлыки, потому что вы знаете код где компилятор не мог, потому что он должен быть на безопасной стороне.
Фактически, я слышал о некотором коде рендеринга графики, где подпрограмма, такая как процедура рисования линии или заполнения полигона, фактически генерировала небольшой стек машинного кода в стеке и выполняла его там, чтобы избежать постоянного принятия решения. о стиле линии, ширине, шаблоне и т. д.
Тем не менее, я хочу, чтобы компилятор генерировал хороший ассемблерный код для меня, но не был слишком умным, и они в основном это делают. Фактически, одна из вещей, которые я ненавижу в Фортране, - это шифрование кода в попытке «оптимизировать» его, как правило, без существенной цели.
Обычно, когда приложения имеют проблемы с производительностью, это связано с расточительным дизайном. В наши дни я бы никогда не порекомендовал ассемблер для производительности, если бы приложение не было настроено в течение всего дюйма, все еще не было достаточно быстрым и проводило все свое время в тесных внутренних циклах.
Добавлено: я видел множество приложений, написанных на ассемблере, и главное преимущество в скорости по сравнению с такими языками, как C, Pascal, Fortran и т. Д., Было в том, что программист был намного осторожнее при кодировании на ассемблере. Он или она собирается писать примерно 100 строк кода в день, независимо от языка, и на языке компилятора, который будет равен 3 или 400 инструкциям.
источник
Несколько примеров из моего опыта:
Доступ к инструкциям, которые недоступны из C. Например, многие архитектуры (такие как x86-64, IA-64, DEC Alpha и 64-битный MIPS или PowerPC) поддерживают 64-битное 64-битное умножение, дающее 128-битный результат. GCC недавно добавила расширение, обеспечивающее доступ к таким инструкциям, но до этого требовалась сборка. И доступ к этой инструкции может иметь огромное значение для 64-битных процессоров при реализации чего-то вроде RSA - иногда даже в 4 раза улучшая производительность.
Доступ к специфичным для CPU флагам. Тот, кто меня сильно укусил, это флаг для переноски; при выполнении сложения с множественной точностью, если у вас нет доступа к биту переноса ЦП, нужно вместо этого сравнить результат, чтобы увидеть, не переполнился ли он, что требует 3-5 дополнительных инструкций для каждой ветви; и еще хуже, которые являются довольно последовательными с точки зрения доступа к данным, что убивает производительность на современных суперскалярных процессорах. При обработке тысяч таких целых чисел подряд возможность использовать addc является огромным преимуществом (есть и суперскалярные проблемы с конкуренцией за бит переноса, но современные процессоры справляются с этим довольно хорошо).
SIMD. Даже автовекторизация компиляторов может выполнять только относительно простые случаи, поэтому, если вам нужна хорошая производительность SIMD, к сожалению, часто необходимо писать код напрямую. Конечно, вы можете использовать встроенные функции вместо ассемблера, но как только вы достигнете уровня встроенных функций, вы все равно в основном пишете сборку, просто используя компилятор в качестве распределителя регистров и (номинально) планировщик команд. (Я склонен использовать встроенные функции для SIMD просто потому, что компилятор может генерировать прологи функций и все такое для меня, поэтому я могу использовать один и тот же код в Linux, OS X и Windows, не имея дело с проблемами ABI, такими как соглашения о вызовах функций, но другие чем то, что встроенные SSE на самом деле не очень хорошие - Altivec кажутся лучше, хотя у меня нет большого опыта с ними).AES или SIMD исправление ошибок: можно представить себе компилятор, который может анализировать алгоритмы и генерировать такой код, но мне кажется, что такой умный компилятор по крайней мере 30 лет от существующего (в лучшем случае).
С другой стороны, многоядерные машины и распределенные системы сместили многие из самых больших выигрышей в производительности в другом направлении - получите дополнительное ускорение на 20% для записи ваших внутренних циклов в сборке, или на 300%, запустив их на нескольких ядрах, или на 10000% на запустить их через кластер машин. И, конечно же, оптимизацию высокого уровня (такие как фьючерсы, запоминание и т. Д.) Часто гораздо проще выполнить на языке более высокого уровня, таком как ML или Scala, чем на C или asm, и зачастую они могут обеспечить гораздо больший выигрыш в производительности. Так что, как всегда, есть компромиссы, которые нужно сделать.
источник
Плотные петли, как при игре с изображениями, поскольку изображение может занимать миллионы пикселей. Заседание и выяснение того, как наилучшим образом использовать ограниченное количество регистров процессора, может иметь значение. Вот пример из реальной жизни:
http://danbystrom.se/2008/12/22/optimizing-away-ii/
Тогда часто у процессоров есть некоторые эзотерические инструкции, которые слишком специализированы, чтобы компилятор мог их использовать, но иногда программист на ассемблере может их использовать. Взять, к примеру, инструкцию XLAT. Действительно здорово, если вам нужно выполнять поиск в таблице в цикле, а таблица ограничена 256 байтами!
Обновлено: О, просто подумайте о том, что является наиболее важным, когда мы говорим о циклах в целом: компилятор часто не имеет ни малейшего представления о том, сколько итераций будет обычным делом! Только программист знает, что цикл будет повторяться много раз и что поэтому будет полезно подготовить цикл с некоторой дополнительной работой, или если он будет повторяться так мало раз, что на самом деле установка займет больше итераций ожидается.
источник
Чаще, чем вы думаете, C должен делать вещи, которые кажутся ненужными с точки зрения программиста сборки, только потому, что так говорят стандарты C.
Целочисленное продвижение, например. Если вы хотите сдвинуть переменную char в C, обычно можно ожидать, что код на самом деле сделает только одно - сдвиг в один бит.
Стандарты, однако, предписывают компилятору делать расширение знака до int перед сдвигом и впоследствии обрезать результат до char, что может усложнить код в зависимости от архитектуры целевого процессора.
источник
Вы на самом деле не знаете, действительно ли ваш хорошо написанный C-код действительно быстр, если вы не смотрели на разборку того, что производит компилятор. Много раз вы смотрите на это и видите, что «хорошо написанное» было субъективным.
Поэтому нет необходимости писать на ассемблере, чтобы получить самый быстрый код, но, безусловно, стоит знать ассемблер по той же причине.
источник
Я прочитал все ответы (более 30) и не нашел простой причины: ассемблер работает быстрее, чем C, если вы прочитали и применили Справочное руководство по оптимизации архитектур Intel® 64 и IA-32 , поэтому причина, по которой сборка может медленнее то, что люди, которые пишут такие медленные сборки, не читали Руководство по оптимизации .
В старые добрые времена Intel 80286 каждая инструкция выполнялась с фиксированным числом циклов ЦП, но после выпуска Pentium Pro, выпущенного в 1995 году, процессоры Intel стали суперскалярными, используя сложную конвейеризацию: выполнение по порядку и переименование регистров. До этого на Pentium, выпущенном в 1993 году, существовали конвейеры U и V: линии с двумя конвейерами, которые могли выполнять две простые инструкции за один такт, если они не зависели друг от друга; но это было не то, что можно сравнить с тем, что «Выполнение вне очереди» и «Переименование регистров» появилось в Pentium Pro и почти не изменилось.
Чтобы объяснить в двух словах, самый быстрый код - это когда инструкции не зависят от предыдущих результатов, например, вы всегда должны очищать целые регистры (с помощью movzx) или использовать
add rax, 1
вместо них илиinc rax
удалять зависимость от предыдущего состояния флагов и т. Д.Вы можете прочитать больше об Оформлении заказа и Переименовании Регистрации, если позволяет время, в Интернете есть много информации.
Есть и другие важные вопросы, такие как прогнозирование ветвлений, количество единиц загрузки и хранения, количество шлюзов, которые выполняют микрооперации, и т. Д., Но наиболее важной вещью, которую следует учитывать, является выполнение вне очереди.
Большинство людей просто не знают о выполнении вне очереди, поэтому они пишут свои программы сборки, например, для 80286, ожидая, что выполнение их инструкции займет фиксированное время независимо от контекста; в то время как компиляторы C знают о выполнении вне очереди и правильно генерируют код. Вот почему код таких незнакомых людей медленнее, но если вы узнаете, ваш код будет быстрее.
источник
Я думаю, что общий случай, когда ассемблер работает быстрее, это когда умный программист на ассемблере смотрит на вывод компилятора и говорит: «Это критический путь для производительности, и я могу написать его, чтобы повысить его эффективность», а затем этот человек настраивает ассемблер или переписывает его. с нуля.
источник
Все зависит от вашей рабочей нагрузки.
Для повседневных операций C и C ++ просто хороши, но есть определенные рабочие нагрузки (любые преобразования, включающие видео (сжатие, распаковка, эффекты изображения и т. Д.)), Которые в значительной степени требуют сборки для обеспечения производительности.
Они также обычно включают использование специфичных для CPU расширений чипсета (MME / MMX / SSE / что угодно), которые настроены для таких операций.
источник
У меня есть операция транспонирования битов, которая должна быть сделана, на 192 или 256 битах на каждое прерывание, что происходит каждые 50 микросекунд.
Это происходит по фиксированной карте (аппаратные ограничения). Используя C, это заняло около 10 микросекунд. Когда я перевел это на Ассемблер, учитывая особенности этой карты, специфическое кэширование регистров и использование бит-ориентированных операций; выполнение заняло менее 3,5 мкс.
источник
Возможно, стоит взглянуть на « Оптимизацию неизменяемости и чистоты» Уолтера Брайта. Это не профилированный тест, но он показывает вам один хороший пример различия между рукописным и сгенерированным компилятором ASM. Уолтер Брайт пишет оптимизирующие компиляторы, поэтому, возможно, стоит взглянуть на его другие сообщения в блоге.
источник
LInux сборка , задает этот вопрос и дает плюсы и минусы использования сборки.
источник
Простой ответ ... Тот, кто хорошо знает ассемблер (у него также есть ссылка, и он использует все функции кеша, конвейера и т. Д.), Гарантированно способен создавать гораздо более быстрый код, чем любой компилятор.
Однако разница в эти дни просто не имеет значения в типичном приложении.
источник
Одной из возможностей версии PolyPascal для CP / M-86 (брат Turbo Pascal) было заменить функцию «использовать биос для вывода символов на экран» процедурой машинного языка, которая по существу были даны x, y и строка для размещения там.
Это позволило обновлять экран намного быстрее, чем раньше!
В двоичном файле было место для встраивания машинного кода (несколько сотен байтов), и там были и другие вещи, поэтому было необходимо сжать как можно больше.
Оказывается, что поскольку экран был размером 80x25, обе координаты могли помещаться в байтах, поэтому обе они могли помещаться в двухбайтовом слове. Это позволило выполнить вычисления, необходимые в меньшем количестве байтов, так как одно добавление может манипулировать обоими значениями одновременно.
Насколько мне известно, нет компиляторов C, которые могут объединять несколько значений в регистр, выполнять SIMD-инструкции для них и разбивать их позже (и я не думаю, что машинные инструкции будут в любом случае короче).
источник
Один из наиболее известных фрагментов сборки взят из цикла отображения текстур Майкла Абраша ( подробно описанного здесь ):
В настоящее время большинство компиляторов выражают продвинутые специфичные для процессора инструкции в виде встроенных функций, то есть функций, которые компилируются вплоть до самой инструкции. MS Visual C ++ поддерживает встроенные функции для MMX, SSE, SSE2, SSE3 и SSE4, поэтому вам не нужно беспокоиться о переходе к сборке, чтобы воспользоваться преимуществами инструкций для конкретной платформы. Visual C ++ также может использовать фактическую архитектуру, на которую вы ориентируетесь, с соответствующей настройкой / ARCH.
источник
При правильном программировании программы на ассемблере всегда могут быть выполнены быстрее, чем их аналоги на С (по крайней мере, незначительно). Было бы трудно создать C-программу, в которой вы не могли бы вынести хотя бы одну инструкцию Ассемблера.
источник
http://cr.yp.to/qhasm.html имеет много примеров.
источник
GCC стал широко используемым компилятором. Его оптимизации в целом не так хороши. Гораздо лучше, чем средний программист, пишущий на ассемблере, но для реальной производительности это не так хорошо. Есть компиляторы, которые просто невероятны в коде, который они производят. Так что в качестве общего ответа будет много мест, где вы можете перейти к выводу компилятора и настроить ассемблер для повышения производительности и / или просто переписать подпрограмму с нуля.
источник
Долгое время, есть только одно ограничение: время. Когда у вас нет ресурсов для оптимизации каждого отдельного изменения в коде и вы тратите свое время на выделение регистров, оптимизацию нескольких разливов, а что нет, компилятор будет побеждать каждый раз. Вы вносите свои изменения в код, перекомпилируете и измеряете. Повторите при необходимости.
Кроме того, вы можете многое сделать на стороне высокого уровня. Кроме того, проверка полученной сборки может дать IMPRESSION то, что код является дерьмом, но на практике он будет работать быстрее, чем, как вы думаете, будет быстрее. Пример:
int y = data [i]; // делать что-то здесь .. call_function (y, ...);
Компилятор будет читать данные, помещать их в стек (разливать), а затем читать из стека и передавать в качестве аргумента. Звучит дерьмо? Это может быть очень эффективная компенсация задержки и привести к более быстрому времени выполнения.
// оптимизированная версия call_function (data [i], ...); // не так оптимизировано в конце концов ..
Идея с оптимизированной версией состояла в том, что мы уменьшили давление в регистре и избежали пролива. Но на самом деле «дерьмовая» версия оказалась быстрее!
Глядя на ассемблерный код, просто глядя на инструкции и делая вывод: больше инструкций, медленнее, было бы ошибочным суждением.
Здесь нужно обратить внимание: многие эксперты по сборке думают, что знают много, но знают очень мало. Правила меняются от архитектуры к следующей тоже. Например, не существует x86-кода «серебряная пуля», который всегда самый быстрый. В эти дни лучше идти по эмпирическим правилам:
Кроме того, чрезмерное доверие к компилятору, волшебным образом превращающее плохо продуманный код C / C ++ в «теоретически оптимальный» код, является желанным мышлением. Вы должны знать компилятор и цепочку инструментов, которые вы используете, если вы заботитесь о «производительности» на этом низком уровне.
Компиляторы в C / C ++, как правило, не очень хороши в переупорядочении подвыражений, потому что функции имеют побочные эффекты, для начала. Функциональные языки не страдают от этого предостережения, но не соответствуют нынешней экосистеме. Существуют опции компилятора, которые позволяют смягчить правила точности, которые позволяют компилятору / компоновщику / генератору кода изменять порядок операций.
Эта тема немного тупиковая; для большинства это не актуально, а в остальном они все равно знают, что делают.
Все сводится к следующему: «понимать, что вы делаете», это немного отличается от того, что вы делаете.
источник