Когда сборка происходит быстрее, чем C? - PullRequest
440 голосов
/ 23 февраля 2009

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

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

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

Ответы [ 39 ]

14 голосов
/ 06 апреля 2011

Я удивлен, что никто не сказал этого. Функция strlen() намного быстрее, если написана на ассемблере! В C лучшее, что вы можете сделать, это

int c;
for(c = 0; str[c] != '\0'; c++) {}

во время сборки вы можете значительно ускорить его:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

длина указана в ecx. Это сравнивает 4 символа за раз, так что это в 4 раза быстрее. И подумайте, используя старшее слово eax и ebx, оно будет в 8 раз быстрее , чем в предыдущей программе C!

13 голосов
/ 23 февраля 2009

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

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

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

  • Для таких вещей, как таблицы переходов, вы можете избежать проверки границ индекса.

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

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

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

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

Добавлено: я видел множество приложений, написанных на ассемблере, и главное преимущество в скорости по сравнению с такими языками, как C, Pascal, Fortran и т. Д. Было в том, что программист был намного осторожнее при кодировании на ассемблере. Он или она собирается писать примерно 100 строк кода в день, независимо от языка, и на языке компилятора, который будет равен 3 или 400 инструкциям.

12 голосов
/ 23 февраля 2009

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

11 голосов
/ 15 октября 2009

Несколько примеров из моего опыта:

  • Доступ к инструкциям, которые недоступны из 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, и зачастую они могут обеспечить гораздо больший выигрыш в производительности. Так что, как всегда, есть компромиссы.

10 голосов
/ 23 февраля 2009

Плотные петли, как при игре с изображениями, поскольку изображение может занимать миллионы пикселей. Сесть и понять, как наилучшим образом использовать ограниченное количество регистров процессора, может иметь значение. Вот пример из реальной жизни:

http://danbystrom.se/2008/12/22/optimizing-away-ii/

Тогда часто у процессоров есть некоторые эзотерические инструкции, которые слишком специализированы, чтобы компилятор не мог их использовать, но иногда программист на ассемблере может их использовать. Взять, к примеру, инструкцию XLAT. Действительно здорово, если вам нужно выполнять поиск таблиц в цикле и , таблица ограничена 256 байтами!

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

10 голосов
/ 15 марта 2014

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

Целочисленное продвижение, например. Если вы хотите сдвинуть переменную char в C, обычно можно ожидать, что код на самом деле сделает это, сдвиг в один бит.

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

9 голосов
/ 23 февраля 2009

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

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

8 голосов
/ 23 февраля 2009

Я думаю, что общий случай, когда ассемблер работает быстрее, это когда умный программист на ассемблере смотрит на вывод компилятора и говорит: «Это критический путь для производительности, и я могу написать его, чтобы он был более эффективным», а затем этот человек настраивает ассемблер или переписывает с нуля.

7 голосов
/ 24 февраля 2009

Все зависит от вашей рабочей нагрузки.

Для повседневных операций C и C ++ хороши, но есть определенные рабочие нагрузки (любые преобразования, включающие видео (сжатие, распаковка, эффекты изображения и т. Д.)), Которые в значительной степени требуют сборки для обеспечения производительности. *

Они также обычно включают в себя использование специфичных для CPU расширений чипсета (MME / MMX / SSE / что угодно), которые настроены для таких операций.

6 голосов
/ 24 мая 2009

У меня есть операция транспонирования битов, которая должна быть сделана, на 192 или 256 битах на каждое прерывание, которое происходит каждые 50 микросекунд.

Это происходит по фиксированной карте (аппаратные ограничения). Используя C, это заняло около 10 микросекунд. Когда я перевел это на Ассемблер, учитывая особенности этой карты, специфическое кэширование регистров и использование бит-ориентированных операций; выполнение заняло менее 3,5 мкс.

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