Audytor.ru

Теплоснабжение "Аудитор"
4 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Как писать на ассемблере в 2021 году

Как писать на ассемблере в 2021 году

Несмотря на наличие множества языков различной степени высокоуровневости, сегодня ассемблер не потерял своей актуальности и в индексе TIOBE находится на почётном 10-ом месте (на февраль 2021), обогнав такие модные языки как Go и Rust. Одна из причин его привлекательности – в простоте и максимальной близости к железу; с другой стороны, программирование на ассемблере всё ещё может рассматриваться как искусство и даёт совершенно особые эмоции.

Однако писать программы целиком и полностью на ассемблере — не просто долго, муторно и сложно — но ещё и несколько глупо — ведь высокоуровневые абстракции для того и были придуманы, чтобы сократить время разработки и упростить процесс программирования. Поэтому чаще всего на ассемблере пишут отдельно взятые хорошо оптимизированные функции, которые затем вызываются из языков более высокого уровня, таких как с++ и c#.

Исходя из этого, наиболее удобной средой для программирования будет Visual Studio, в состав которой уже входит MASM. Подключить его к проекту на с/c++ можно через контекстное меню проекта Build Dependencies – Build Customizations…, поставив галочку напротив masm, а сами программы на ассемблере будут располагаться в файлах с расширением .asm (в свойствах которого Item Type должно иметь значение Microsoft Macro Assembler). Это позволит не просто компилировать и вызывать программы на ассемблере без лишних телодвижений – но и осуществлять сквозную отладку, «проваливаясь» в ассемблерный исходник непосредственно из c++ или c# (в том числе и по точке останова внутри ассемблерного листинга), а также отслеживать состояния регистров наряду с обычными переменными в окне Watch.

Подсветка синтаксиса

B Visual Studio нет встроенной подсветки синтаксиса для ассемблера и прочих достижений современного IDE-строения; но её можно обеспечить с помощью сторонних расширений.

Читайте так же:
Проверить счетчик кадров никон

AsmHighlighter — исторически первый с минимальным функционалом и неполным набором команд — отсутсвуют не только AVX, но и некоторые из стандартных, в частности fsqrt. Именно этот факт побудил к написанию собственного расширения —

ASM Advanced Editor. В нём, помимо подсветки и сворачивания участков кода (с использованием комментариев «;[«, «;[+» и «;]») реализована привязка подсказок к регистрам, всплывающих по наведению курсора ниже по коду (также через комментарии). Выглядит это так:

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

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

Asm Dude — обнаружился чуть позже. В нём автор пошёл другим путём и сфокусировал силы на встроенном справочнике команд и автодополнении, в том числе и с отслеживанием меток. Сворачивание кода там также присутствует (по «#region / #end region»), но привязки комментариев к регистрам вроде ещё нет.

32 vs. 64

С тех пор, как появилась 64-битная платформа, стало нормой писать по 2 варианта приложений. Пора завязывать с этим! Сколько можно тянуть легаси. Это же касается и расширений — найти процессор без SSE2 можно разве что в музее – к тому же, без SSE2 64-битные приложения и не заработают. Никакого удовольствия от программирования не будет, если писать по 4 варианта оптимизированных функций для каждой платформы. Только 64 бит/AVX, только хардкор! Хотя может быть и прямо противоположный взгляд — новые процессоры и так работают быстро, и оптимизацию стоит делать под старые. В общем, всё зависит от конкретной задачи.

Читайте так же:
Строй групп установка счетчиков

Преимущество 64-битной платформе вовсе не в «широких» регистрах – а в том, что этих самых регистров стало в 2 раза больше – по 16 штук как общего назначения, так и XMM/YMM. Это не только упрощает программирование, но и позволяет значительно сократить обращения к памяти.

Если ранее без FPU было никуда, т.к. функции с вещественными числами оставляли результат на вершине стека, то на 64-битной платформе обмен проходит уже без его участия с использованием регистров xmm расширения SSE2. Intel в своих руководствах также активно рекомендует отказаться от FPU в пользу SSE2. Однако есть нюанс: FPU позволяет производить вычисления с 80-битной точностью — что в некоторых случаях может оказаться критически важным. Поэтому поддержка FPU никуда не делаcь, и рассматривать её как устаревшую технологию совершенно точно не стоит. Например, вычисление гипотенузы можно делать «в лоб» без опасения переполнения,

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

Пример оптимизации: преобразование Хартли

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

Читайте так же:
Перерасчет некачественной гвс при наличии счетчика

Итак, в качестве задачи возьмём алгоритм (медленного) преобразования Хартли:

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

Обратите внимание: тут нет ни разворачивания цикла, ни SSE/AVX, ни косинусных таблиц, ни снижения сложности за счёт «быстрого» алгоритма преобразования. Единственная явная оптимизация — это итеративное вычисление синуса/косинуса во внутреннем цикле алгоритма непосредственно в регистрах FPU.

Поскольку речь идёт об интегральном преобразовании, то помимо скорости, нас ещё интересует точность вычисления и уровень накопленных погрешностей. В данном случае посчитать её очень просто — делая два преобразования подряд, мы должны получить (в теории) исходные данные. На практике они будут слегка отличаться, и можно будет посчитать ошибку через среднеквадратическое отклонения полученного результата от аналитического.

Результаты авто-оптимизации программы на c++ также могут сильно зависеть от настроек параметров компилятора и выбора допустимого расширенного набора инструкций (SSE/AVX/etc). При этом есть два нюанса:

  1. Современные компиляторы склонны всё возможное вычислять на этапе компиляции – поэтому вполне возможно в откомпилированном коде вместо алгоритма увидеть заранее посчитанное значение, что при замере производительности даст преимущество компилятору в 100500 раз. Во избежание этого в моих замерах используется внешняя функция zero(), добавляющая неопределённости входным параметрам.
  2. Если компилятору указать «не использовать AVX» — это ещё не значит, что в полученном коде будет отсутствовать AVX. Внешние библиотеки сами проверяют доступный набор команд на текущей платформе и выбирают соответствующую реализацию. Поэтому единственно надёжный способ сравнения производительности в таком случае – испытывать код на платформе, где AVX отсутствует в принципе.

Итак, компилятор Visual Studio 2019, целевая платформа AVX2, Floating Point Model=Precise. Чтобы было ещё интереснее — будет измерять из проекта на c# на массиве из 10000 элементов:

Читайте так же:
Установка счетчиков со своими счетчиками

C# ожидаемо оказался медленнее с++, а функция на ассемблере оказалась быстрее в 9 раз! Однако ещё рано радоваться — установим Floating Point Model=Fast:

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

В данном случае наш вариант оказался и быстрее, и точнее. Но так бывает не всегда – и выбирая FPU для хранения результатов мы неизбежно будем терять в возможности оптимизации векторизацией. Также никто не запрещает комбинировать FPU и SSE2 в тех случаях, когда это имеет смысл (в частности, такой подход я использовал в реализации double-double арифметики, получив 10-кратное ускорение при умножении).

Дальнейшая оптимизация преобразования Хартли лежит уже в другой плоскости и (для произвольного размера) требует алгоритма Блюстейна, который также критичен к точности промежуточных вычислений. Ну а этот проект можно скачать на GitHub, и в качестве бонуса там также можно найти реализацию функций для суммирования/масштабирования массивов на FPU/SSE2/AVX.

Что почитать

Литературы по ассемблеру навалом. Но можно выделить несколько ключевых источников:
1. Официальная документация от Intel. Ничего лишнего, вероятность опечаток минимальна (кои в печатной литературе встречаются повсеместно).
2. Официальная документация от Microsoft.
3. Онлайн справочник, спарсенный из официальной документации.
4. Сайт Агнера Фога, признанного эксперта по оптимизации. Также содержит образцы оптимизированного кода на C++ с использованием интринсиков.
5. SIMPLY FPU.
6. 40 Basic Practices in Assembly Language Programming.
7. Все, что нужно знать, чтобы начать программировать для 64-разрядных версий Windows.

Читайте так же:
Монтаж счетчиков по воде

Appendix: Почему бы просто не использовать интринсики (Intrinsics)?

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

Цикл с предусловием while

Общая форма записи

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

Пример на Си : Посчитать сумму чисел от 1 до введенного k

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

Пример бесконечного цикла

while — цикл с предусловием, поэтому вполне возможно, что тело цикла не будет выполнено ни разу если в момент первой проверки проверяемое условие окажется ложным.

Например, если в приведенном выше коде программы ввести k=-1, то получим результат
Цикл while может не выполниться ни разу

голоса
Рейтинг статьи
Ссылка на основную публикацию
Adblock
detector