Комментарии 167
А ещё в природе есть такая вещь, как i386 в корпусе PQFP. У нас под него писали на ассемблере, потому что nasm может сразу выдать Intel hex, содержащий только нашу программу, а возиться для получения аналогичного результата на C никому не хотелось.
На работе у меня Windows (7, WSL нету), там не ELF’ы и objcopy есть разве что в cygwin (у меня тоже установлен). И вряд ли всё так просто: нужно разместить безусловный переход по строго определённому адресу (вектора прерываний), а сама программа должна быть по нулевому адресу. И ещё к аргументам нужно добавить, как минимум, -march=i386 -static -Os
. И нужно понять как записывать и считывать регистры специального назначения. Разобраться можно, но зачем?
Помимо этого написание программы на ассемблере позволило использовать «память» в режиме «только чтение», обошедшись без стека и без собственно реализации записи в память — самой памяти в системе никогда не было, была имитация на FPGA, которую тоже нужно написать.
Вспомнил молодость, ассемблер, Clipper, с чего начинается Родина… В смысле, первые символы dos-овского .exe-файла и первые символы .exe-файла под Windows, как ставили Windows 95 поверх MS-DOS и Windows 3.1, а потом пытались откатиться при помощи установки Windows 3.1… Эх…
Впрочем соглашусь, что за исключением очень редких случаев, писать прямо на асме нет смысла.
Я на ассемблере не пишу, пишу на C++, но знать и понимать его стоит. Как минимум для целей отладки или понимания происходящего под капотом: иногда после этого сильно по другому смотришь на более высокий уровень. Обязательно нужно знакомится с соглашениями по вызовам для вашей платформы и так далее. Особенно это актуально для всякого embedded.
Знание ассемблера безусловно полезное, например в случае отладки, когда есть только бинарный код, но насколько целесообразно в прикладном ПО делать вставки на ассемблере?
Да, и выяснилось, что он не самый быстрый. Например, фаза рендеринга markdown в html там не кешируется.
Кстати, кешируется:
create table Posts (
id integer primary key autoincrement,
threadID integer references Threads(id) on delete cascade,
userID integer references Users(id) on delete cascade,
postTime integer,
ReadCount integer,
Content text,
Rendered text -- здесь
);
Есть еще противоположная сторона оптимизации: когда нужно уместить код в минимальное количество байт, тогда приходится использовать ассемблер и всякие трюки. Например, декомпрессор в паковщике исполняемых файлов upx написан на ассемблере.
https://github.com/cisco/openh264 — полистайте здесь
Ну, не буду сильно спорить..
Возьмем достаточно большой файл.
time cat 700mb.iso > /dev/null
real 0m13.280s
user 0m0.024s
sys 0m0.632s
time cat 700mb.iso > /dev/null
real 0m0.275s
user 0m0.004s
sys 0m0.220s
Из диска он читается 13с, из кеша — меньше секунды.
time cat 700mb.iso | md5sum -
73fe07300ad456cff2f7b9524c295fd1 -
real 0m1.518s
user 0m1.336s
sys 0m0.544s
Хеш-сумма этих 700 мб считается около 1 с.
Нехитрый эксперимент показывает, что самое медленное при работе с файлами — дисковый ввод-вывод; его в первую очередь нужно оптимизировать.
Вот сгенерированный С++ компилятором код умножения на 5 и суммирования 1024 байт из буффера, и я с трудом представляю, каких сил будет стоить написание и сопровождение подобного кода вручную:
godbolt.org/g/2QW1Qx
Так на ассемблере писать никто не будет. С этим замечательно справится компилятор.
Цикл из 1024 сложений и одно умножение на 5 (2 сдвига и сложение) — это как-то более логично.
А это смотря что нужно оптимизировать. В смысле написания и поддержки — 2 строки на C++ конечно же, оптимальное решение.
В смысле быстродействия… Напрашивающийся цикл из 1024 сложений выглядит лучше… На 8086 это бы прокатило ;) Но компилятор лучше знает архитектуру процессора, и, подозреваю, что эти странные 128-битные операции неспроста..
Напрашивающийся цикл из 1024 сложений выглядит лучше…Вряд ли он лучше. Работать будет в лучшем случае так же, но скорее всего — медленнее. И дело не только в странных и не очень операциях, а еще и в ветвлениях и затратах на сам цикл.
Да. Если написать коротко и понятно — это будет, скорее всего, неоптимально. А если написать оптимально — будет сложно (а то и невозможно) поддерживать.
Так, в вашем примере, если заменить int на short (или long) — компилятор использует совершенно другие инструкции. Получается, для разных типов данных написанный на ассемблере алгоритм нужно переписывать.
… а С++ реализация cosine similarity между одним float-вектором размерностью 192 и 1.7 гигабайтами других таких же векторов с использованием SIMD-intrinsics вычисляется за 0.3 секунды на процессоре Celeron J1900 @ 1.99GHz, на NAS-е, внутри виртуалки.
Не то, чтобы сильно быстро, но это даже не ноутбучный процессор, не самая быстрая память, еще и виртуалка.
Это не ассемблер в чистом виде, а вот как-то вот так:
xlo = _mm256_unpacklo_epi16(row0, zero);
xhi = _mm256_unpackhi_epi16(row0, zero);
pix0_0_7 = _mm256_cvtepi32_ps(xlo);
pix0_8_f = _mm256_cvtepi32_ps(xhi);
row1 = _mm256_permutevar8x32_epi32(_mm256_loadu_si256((const _m256i*)&srcrow2[scol]), b256m);
Относительно очень близкой по логике обработки dcraw/LibRaw, выигрыш на SSSE3 — примерно порядок, от AVX2 — еще раза в полтора (плюс dcraw/LibRaw — целочисленные, а FRV — внутри в плавучке, что несколько улучшает качество теней)
Это даже не сколько Си, это интринсики :)
Возможно, бывают какие-то хитрые алгоритмы, которые очень сложно выразить в C и проще — на языке ассемблера, но я пока не смог представить ничего подобного.
Конечно, бывают задачи, которые проще решаются на ассемблере: первое, что приходит в голову из того, что писал сам — загрузчик в MBR. Но когда речь идет об оптимизации, я подозреваю баг компилятора или ошибку в высокоуровневом коде.
Так в том-то и посыл...
Полностью согласен.
что приходит в голову из того, что писал сам — загрузчик в MBR
Что удивительно, беглый поиск не показал мне ни одного pureC бутлодера в MBR на Си… Хотя вру: https://www.codeproject.com/Articles/664165/Writing-a-boot-loader-in-Assembly-and-C-Part хотя и тут без вставок никак.
Говорить «для загрузчика необходим ассемблер», глядя на x86 — всё равно, что говорить «для завода двигателя необходим кривой стартёр», глядя на 2101.
Кстати, вспомнилось: мне однажды нужно было сделать инъекцию: был закрытый код, который вызывался в контексте ISR и ошибочно вызывал функцию логирования (косвенно, через общий код), которая аллоцировала большой буффер на стеке, чем срывала его, будучи запущенной из ISR.
Инъекция заключалась в том, что имя функции логирования подменялось (средствами GCC и линкера) и вместо неё вызывался враппер, который дополнительно делал проверку. Вот врапер оказалось проще написать на ассемблере, используя знание о соглашениях на вызовы, не портя стек (функция была с переменным числом аргументов).
Ну и да, я понимаю, что это костыль. Но решилось автоматом ещё несколько проблем и стало обоснованием более предметно предъявить разработчикам закрытой части SDK.
Плюс, некоторые компиляторы позволяют себе переупорядочить порядок действий.
Но смысл то один — пишу ровно ассемблерные куски правда на немного птичьем языке
Но это именно отдельные функции, а не вставки asm-а в С-код.
С другой стороны хорошие компиляторы могут и самостоятельно использовать эффективные simd-инструкции при правильно написанном С-коде и высоком уровне оптимизации. И временами это бывает не менее эффективно, чем использование интринсиков.
It is now possible to call x86 intrinsics from select functions in a file that are tagged with the corresponding target attribute without having to compile the entire file with the -mxxx option. This improves the usability of x86 intrinsics and is particularly useful when doing Function Multiversioning.
Может быть у вас есть пример из реального проекта, когда вставки на ассемблере существенно дали выигрыш в скорости для десктоп приложения?
- Битовый массив. Ассемблерные инструкции позволили напрямую ставить/снимать/проверять бит. То же самое чрез маску — работало прилично медленней.
- Критическая секция для однопроцессорных машин. Работала без переключения в режим ядра для случая, когда вход в критсекцию свободен. Применялась в серверном приложении при одновременной работе 20 тредов. Проблема была в том, что нужно было обращение к регистру FS, которое компилятор не поддерживал
но насколько целесообразно в прикладном ПО делать вставки на ассемблере?В прикладном не знаю, а в библиотеках такого очень много. Можете полюбоваться на ассемблерную версию memchr из glibc.
Битовый массив. Ассемблерные инструкции позволили напрямую ставить/снимать/проверять бит. То же самое чрез маску — работало прилично медленней.Хм, мне казалось, что компиляторы умеют генерировать битовые инструкции из обращений к битовым полям.
На моем опыте под битовыми операциями понимаются следующие конструкции: https://godbolt.org/g/1Ayj7e
В то время как это вполне себе описывается:
mov eax, dword ptr [x]
sub eax, 5
BTR argc, eax
add eax, 7
BTS argc, eax
Лично мне больше нравится стабильный проверенный компилятор + ассемблерные вставки, чем экспериментальные версии.
Есть пример проекта, когда использование ассемблера дало прирост в энергоэффективности устройств.
В студенческие годы разрабатывал с товарищем АСКУЭ (автоматическая система контроля и учёта электроэнергии). Использование ассемблера и знание режимов работы микроконтроллера позволило запитать 7 кучек (по кучке на подъезд) из 30+ устройств (по устройству на квартиру) очень небольшим блоком питания. Не помню точно потребления, но та что-то в районе микроампер в режиме ожидания и раз в 15 минут при опросе — милиамперы для каждого устройства) Что дало преимущество, когда понадобилось поставить ИБП для всех устройств.
Я не знаю откуда это пошло, что у FASM документация слабая???
Это не так! FASM ассемблер простой и та документация которая идет в стандартной поставке и вправду исчерпывающая.
Статья посвящена языку ассемблер с учетом актуальных реалий.
К сожалению про язык ассемблера в статье ничего. Да и озаглавить бы её стоило, наверное, «Сравнение ассемблеров», чтобы не вводить людей в заблуждение. По моему мнению, (не могу судить о большинстве) заголовок «Как писать на ассемблере в 2018 году» предполагает, что в статье пойдёт речь о там как писать код на языке ассемблера с учётом современных наборов команд или технологий.
А кто-то реально сейчас пишет на TASM да ещё и для DOS? Устаревшие вузовские курсы не в счёт.
На flatassembler.net/download.php 4-я ссылка, в описании упоминается OpenBSD
Кстати, для MacOS X тоже:
https://board.flatassembler.net/topic.php?t=13413
И конечно для Menuet/KolibriOS и всяких экспериментальных ОС. Вообще, FASM, несмотря на то, что написан на ассемблере, переносится очень легко на всяких ОС. Намного легче чем тот же NASM.
Очевидно, что имеется ввиду The Assembler x86. Не какой-то там ARM9 с его баррел-шифтами (правда, кому они нужны?), не какая-то там SH, и не о разнице между MIPS и Loongson, не об Эльбрусовском чего-то там они придумали, не о сетевых всяких там — об их величестве x86.
Ну, ладно, допустим… Об x86-64… Допустим… Так опять-же не об жонглированием ядрами и атомных инструкциях…
«Я писал на АСМЕ» — ага, да… Вотиманно на «АСМЕ».
правда, кому они нужны?
Гггг. Сегодня сравнивал производительность на Cortex-M3 двух реализаций AES-128 — на C и на асме, в последней как раз barrel shift используется.
Ну, асм быстрее. Меньше, чем в два раза, но быстрее. Ещё б на фоне недостатков кода на нём это кого-нибудь бы в контексте данного проекта волновало бы.
«Компактный код» сейчас совершенно не значит «быстрый код».
Писать UI, или IO на ассемблере это практически убивать свое время на скучнейшую работу. Если так уж горит — можно сделать всю обвязку на плюсах или еще чем то, а потом сделать ассемблерную вставку.
Моих — да, любых — нет)
К слову, разработчики видеокодеков по-прежнему активно используют асм. Посмотрите исходные коды libvpx, libx265, libx265, да вообще ffmpeg директории asm.
Использование ассемблера в вычислительном коде скорее всего приведет к замедлению ваших программ.
А вы пробовали? Я да. Несколько раз. И дело в том, что нет, не получается. На ассемблере всегда получается намного быстрее.
На C/C++ можно написать быстрые программы, но дело в том, что надо ясно представлять как они должны выглядеть на ассемблере. А потом обмануть компилятора, чтобы сгенерировать нужный ассемблерский код. Для меня проще написать сразу на ассемблере. :P
Думаю, все хоть раз игрались с компиляцией программы, а потом с декомпиляцией, чтоб посмотреть, как компилятор выкрутился. Так вот, в последние годы он настолько всё оптимизирует, что для понимания получившегося ассемблерного кода нужно знать назубок чуть ли не все команды процессора (а для x86-64 это здоровенная такая куча), да ещё и огромную кучу трюков. Может, конечно, кто-то на такое и способен, я же просто смирился с тем, что компиляторы оптимизируют лучше человека (да и я не видел, чтобы кто-то этот тезис оспаривал).
То о чём вы говорите верно разве что для программирования AVR или MSP430, где горсть команд, и человек может ещё конкурировать с компилятором, и где без ассемблера порой никак.
Может, конечно, кто-то на такое и способен, я же просто смирился с тем, что компиляторы оптимизируют лучше человека (да и я не видел, чтобы кто-то этот тезис оспаривал).
Я не только оспариваю, но и несколько раз проверял экспериментально. Получается парадоксальный результат — все верно – компилятор компилирует отлично, но все равно, программы на ассемблере получаются быстрее. Дело в том, что человек на ассемблере пишет не так как на ЯВУ (и в этом ошибаются все, которые сравнивают программирование на ассемблер и ЯВУ). Когда пишет на ассемблере, программист выбирает другие алгоритмы, другую архитектуру программы.
Потому что ленивый и выбирает то, что легче написать именно на ассемблере и то что легче будет поддерживаться. Та же самая программа, можно написать и на C/C++, только код будет смотреться совершенно дико и неестественно.
Я проверял все это несколько раз, и всегда результат одинаков. Можете попробовать сами. Надо только писать более менее реальную программу, а не "алгоритм XYZ".
Но у человека есть преимущество — знание данных и видение картины в целом, что позволяет иногда отсекать, скажем так, «боковые ветки» в решениях, что-то использовать повторно и так далее.
Я не думаю, что это настолько однозначно. Статистики, разумеется, ни у меня ни у вас нет, но я бы скорее согласился когда ассемблеристов было больше. Сейчас их мало (рынок) и те, кто остались, как мне кажется должны быть достаточно высокой квалификации.
Ну и главное. У компилятора есть только один шанс сгенерировать код (если это не JIT). А у програмиста есть возможность написать, потестировать и переписать.
Хотя вот задумался и стало интересно есть ли сейчас компиляторы на нейросетях или ген. алгоритмах, которые могут медленно и упорото упорно перегенерировать код, пока не достигнут максимальной производительности.
На мой взгляд учить asm сейчас стоит для всяких ARM Cortex M3 на крайний случай Broadcomm/Raspberry Pi. На PC потребности в нем минимальны. А в embedded — это требование жизни.
x86 особенно, MIPS частично, во встроееной технике проиграли маркетинговую войну ARM. Его и стоит учить.
Keil, gnu asm.
bob.cs.sonoma.edu/IntroCompOrg-RPi/intro-co-rpi.html — интересная книга.
На мой взгляд учить asm сейчас стоит для всяких ARM Cortex M3 на крайний случай Broadcomm/Raspberry Pi. На PC потребности в нем минимальны. А в embedded — это требование жизни.
Три года уже работаю в embedded'е ни разу за всё время ни мне ни кому из коллег не довелось использовать ассемблер, дизассемблер приходилось а вот ассемблер нет. Даже тем кто пишет под AVR. Так что очень преувеличено его необходимость в современном embedded'е.
Что надо это понимание как будет выполнятся программа на том чипе под который пишешь и каким ботом это может выйти.
Неудивительно, что ни один из «байткод-процессоров» — ни для p-кода, ни для Java — не стал коммерчески успешным. (Сюда же можно отнести и намного более ранний процессор Intel iAPX 432 (1981) — аппаратную реализацию байткода для Ады.) <...> Для того, чтобы «в железе» отслеживать зависимости между данными, распределять их по «железным» регистрам, и переставлять обращения к ним так, чтобы уложиться в «железные» ограничения — требуются очень сложные механизмы. Получается, что при одном и том же уровне полупроводниковых технологий эффективнее создать процессор для простой ISA, и на нём реализовать трансляцию байткода, — чем выполнять этот же байткод «в железе». Вновь и вновь мечтательные айти-предприниматели убеждались, что превратить «универсальный машинный язык» в настоящий — хоть и возможно технически, но коммерчески бесперспективно.
Автор книги «Processor Design» считает так же: «These Java-specific processors prompted some interest, but not a lot of enthusiasm.»
Так что запилили-выпилили, опять запилили — опять выпилили, запасаемся покорном и ждем третьей реинкарнации.
Если обучать, тогда надо брать какой нибудь восьмибитный процессор с регистровой адресной парой, чтобы студентам жизнь мёдом не казалась — КР580, Z80, MOS6510. И писать на этом многозадачную операционку с TCP/IP. Без ограничений языка. Хочешь — пиши на С, только компилятор пиши сам…
Только, боюсь, 70% студентов вылетят нафиг.
Да и 70% преподавателей тоже…
Я у тамошних преподов спрашивал, не собираются ли они переводить курс на ARM или на ещё что-нибудь актуальное для практических (embedded) целей. Нет: говорят, что если и будут переводить, то на x86 или x64, просто по причине доступности инструментов разработки и сред выполнения.
:-(
Не надо готовить ARM (Java/Pascal) программистов в ВУЗах, все будут в пролёте! Уже, в общем-то, кадровый голод. Не языкам студентов учат, и не конкретным ассемблерам, а общим принципам.
2) Как раз для обучения современным архитектурам — PDP-11, с его сверх-высокоуровневыми инструкциями, довольно нерепрезентативен.
У PDP-11 отличнейший высокоуровневый ассемблер, который даст нормальное понимание как оно вообще всё было 30 лет назад ;)
Без такого экспоната процессор так и останется чёрным ящиком, в котором волшебные гномики понимают и выполняют инструкции.
В скором времени мы запустим портал с тестами (вопрос – варианты ответа) на знание ассемблера и архитектуры компьютера.
Ох всегда меня это раздражало. Знания ассемблера, блин ну вы же сами пишите:
Язык ассемблера — символьная форма записи машинного кода, использование которого упрощает написание машинных программ.
Ну что значит в таком случае знание ассемблера? Если вы имеете ввиду что это x86 так так и напишите. Но есть ведь ещё ARM, MISP, x51, AVR, Z80, Microbalze, NIOSII, PIC и прочее прочее. Так знания какого ассемблера вы хотите проверять?
Конечно, в основном это мелкие вещи, которые один раз пишутся как шаблон, а потом нужные переменные подставляются (в памяти что-нибудь подправить, запускалку с закрытием ненужных окон сделать и т.п.). Но для этого уже давно инструменты есть и тоже вроде как смысл теряется.
Мне больше интересно, как сейчас дела обстоят (и обстоят ли) с программированием под FreeDOS или что-то подобное. Там ведь ассемблер вполне себе удобен (особенно для резидентных программ).
Мне больше интересно, как сейчас дела обстоят (и обстоят ли) с программированием под FreeDOS или что-то подобное. Там ведь ассемблер вполне себе удобен (особенно для резидентных программ).
Работать под (Free)DOS не имеет никакого смысла, потому что просто нельзя использовать возможности компьютера. А вот в Linux программировать на ассемблере очень просто. Почти как в DOS.
Я читаю вот это: https://fresh.flatassembler.net/lscr/
Сравнение ассемблеров/архитектур — когда-то ассемблер DEC мне казался самым лучшим (там архитектура позволяла циклы чтение-модификация-запись при работе с памятью), потом было разочарование от ассемблера для Intel x86 (тут надо делать операции через регистр-аккумулятор AX/EAX и, если уж мы читаем слева-направо, то почему надо писать MOV DX,200, а не MOV 200,DX?). Позже, на Palm III познакомился с ассемблером Pila для Motorola 68000 — вот где красивая архитектура!
Еще интересно было бы узнать про самомодифицирующийся код (и про то, что его нельзя уже изменить, когда попал в конвейер команд/данных, что может быть использовано для определения, что ваша программа прогоняется под отладчиком.
Также можно было бы включить в будущие статьи, например, проект из блога компании Intel, в котором в приложение для Android вставлен и работает программа на Ассемблере. То есть, получается, мы можем писать на Ассемблере для Android! Ну, хотя бы чуть-чуть, для native библиотек.
Еще вопрос: есть книга П.Брамм, Д.Брамм «Микропроцессор 80386 и его программирование». В ней описаны, в частности, дескрипторы страниц и перевод процессора в защищенный режим. Не понятно (простите за невежество), если ОС переводит процессор в защищенный режим и участки памяти вообще не видны для приложений пользователя, то как тогда вирусы могут туда проникнуть? Возможно, меняя код в BIOS или на загрузочном диске.
Для ARM Ассемблера хотелось бы узнать, как включать/останавливать ядра.
Но если человек пишет на ассемблере, он уже не имеет готовых функций типа printf(), в которых за него продумано, как «общаться» с системой, и должен делать это сам.
Писал вот такой вот код на третьем курсе (MASM):
includelib D:\masm32\lib\kernel32.lib
ExitProcess PROTO, :DWORD
GetStdHandle Proto, :DWORD
Sleep PROTO, :DWORD
WriteConsoleA PROTO, handle: DWORD, lpBuffer:PTR BYTE, nNumberOfBytesToWrite:DWORD, lpNumberOfBytesWritten:PTR DWORD, lpReserved:DWORD
ReadConsoleA PROTO :DWORD,:DWORD,:DWORD,:DWORD,:DWORD
.data
STD_OUTPUT_HANDLE EQU -11
STD_INPUT_HANDLE EQU -10
coh dd ? ;store console output handle here
cih dd ? ;store console input handle here
.code
ReadingCA proc msg:DWORD, col:DWORD
sub esp,8
push STD_INPUT_HANDLE
call GetStdHandle
mov [ebp-4],eax ;Local Variable for this subroutine
push 0
lea eax,[ebp-8]
push eax
push [ebp+12]
push [ebp+8]
push [ebp-4]
call ReadConsoleA
ret 8
ReadingCA endp
Так что WinAPI использовать возможно.
А книжка эта вроде была у меня, и еще Питера Нортона (который коммандер) с недокументированными возможностями MS-DOS, там тоже асм.
Как писать на ассемблере в 2018 году