Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:asm-extoptimize.20kb [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== asm-extoptimize.20kb ======
 +<​sub>​{{asm-extoptimize.20kb.odt|Original file}}</​sub>​
 +
 +====== ассемблер — экстремальная оптимизация ======
 +
 +крис касперски ака мыщъх, no-email
 +
 +**ассемблер — это удивительный язык, открывающий дверь в мир больших возможностей и неограниченного самовыражения. состязания между программистами здесь — обычное дело. выигрывает тот, у кого нестандартный взгляд,​ необычный подход. зачастую,​ самое "​тупое"​ решение — самое быстрое и правильное.**
 +
 +===== введение =====
 +
 +Путь начинающего ассемблерщика не только долог, но еще и тернист. Повсюду торчат острые шипы, дорогу преграждают разломы,​ ловушки и капканы. В темной чаще горят злые глаза, доносятся какие-то ухающие звуки и прочие неблагоприятные факторы,​ нагнетающие мрачную атмосферу и серьезно затрудняющую продвижение вперед.
 +
 +Большинство учебников затрагивают только MS-DOS, крайне поверхностно описывая //​**практические**//​ проблемы программирования под Windows. Мыщъх делиться с читателями рецептами,​ которые известны любому профессионалу,​ но совершенно неочевидны новичку.
 +
 +{{asm-extoptimize.20kb_Image_0.jpg?​553}}
 +
 +Рисунок 1 программирование на ассемблере это путь в никуда,​ магистраль,​ ведущая в вечность!
 +
 +===== готовые функции на блюдечке =====
 +
 +Грань между плюсами "​мышиным"​ и "​рукописным"​ кода очень тонка. Отклонение в одну строну — снижает продуктивность программы,​ в другую — увеличивает (причем зря) время разработки. Короче,​ не будем разводить демагогию,​ а рассмотрим фрагмент кода, запускающий процесс на выполнение стандартным способом через win32 API-функцию **CreateProcess**:​
 +
 +{{asm-extoptimize.20kb_Image_1.png?​552}}
 +
 +Рисунок 2 некоторые программисты любят навороченные среды разработки типа WinAsmStudio (аналог MicrosoftVisualStudio) с окнами,​ мастерами и прочими "​перламутровыми пуговицами"​…
 +
 +xor eax, eax; eax := 0
 +
 +push offset pi; lpProcessInformation
 +
 +push offset sis; lpStartupInfo
 +
 +push eax; lpCurrentDirectory
 +
 +push eax; lpEnvironment
 +
 +push eax; dwCreationFlags
 +
 +push eax; bInheritHandles
 +
 +push eax; lpThreadAttributes
 +
 +push eax; lpProcessAttributes
 +
 +pushoffsetfile_name;​ имя исполняемого файла с аргументами
 +
 +push eax; lpApplicationName
 +
 +call ds:​[CreateProcess];​ косвенныйвызов API-функциичерез IAT
 +
 +Листинг 1 запуск процесса на выполнение через win32 API – 12 команд и 73h байта
 +
 +Ассемблированный код занимает 1Fh байт и еще 54h байта расходуются на структуры PROCESS_INFORMATION и STARTUPINFO плюс длина имени файла. А вот что получится,​ если воспользоваться морально "​устаревшей"​ функцией **WinExec**,​ доставшийся в наследство от 16-разрядной старушки Windows (вопреки распространенному заблуждению,​ она реализована одновременно как 16- и 32-разрядная функция,​ а потому перехода в 16-разрядный режим при вызове WinExec из 32-разрядного кода //не происходит//,​ а, значит,​ не происходит и падения производительности):​
 +
 +push 00h; uCmdShow (короче чем XOR EAX,​EAX/​PUSH EAX)
 +
 +pushoffsetfile_name;​ имя исполняемого файла с аргументами
 +
 +callds:​[WinExec];​ косвенный вызов API-функции через IAT
 +
 +Листинг 2 запуск процесса на выполнение через "​устаревшую"​ функцию WinExec - три команды и 1Eh байт машинного кода
 +
 +Всего три машинных команды,​ укладывающиеся в 1Eh байт (без учета имени файла) и никаких дополнительных структур! Расплатой за оптимизацию становится невозможность создания отладочных или "​замороженных"​ процессов,​ не говоря уже про атрибуты безопасности и прочую хрень, реально необходимую в одном случаев из десяти-двадцати случаев,​ а то и реже.
 +
 +{{asm-extoptimize.20kb_Image_2.png?​553}}
 +
 +Рисунок 3 …кто-то предпочитает простые,​ легковесные и аскетичные IDE по функциональности сравнимые с блокнотом (например,​ fasmw)
 +
 +Но это еще не предел оптимизации! Воспользовавшись функцией **system** из библиотеки MSVCRT.DLL (которая активно используется многими приложениями и практически всегда "​болтается"​ в памяти),​ мы сократим код до 1Dh байт или даже до 1Ah, если отсрочим восстановление стека, выполнив команду add esp,​ x в конце функции,​ выталкивая все аргументы одним махом:
 +
 +pushoffsetfile_name;​ имя исполняемого файла с аргументами
 +
 +callsystem; прямой вызов функции (почему так — см. врезку)
 +
 +addesp, 4; выталкиваем аргументы из стека (можно сделать позже)
 +
 +Листинг 3 запуск процесса на выполнение через функцию system библиотеки MSVCRT.DLL – три (две) команды и 1Dh (1Ah) байт кода
 +
 +Тоже самое относится и к функциям файлового ввода/​вывода,​ преобразованиям данных и т. д., и т. п. Никто же не будет спорить,​ что вызов **fopen** намного короче,​ чем **CreateFile**,​ а скорость исполнения у них практически та же самая, тем более что, библиотека MSVCRT.DLL//​всегда//​ присутствует памяти,​ поскольку используются системными процессами. Windows просто спроецирует ее на наше адресное пространство — вот и все! Никакого увеличения потребляемой памяти не произойдет!
 +
 +Наибольший выигрыш достигается на задачах,​ требующих перевода двоичных данных в ASCII-представление или наоборот. Собственно говоря,​ программирование на ассемблере и начинается с вывода на экран числа, заданного в двоичной форме. Конечно,​ "​вручную"​ разработанная и оптимизированная функция намного быстрее стандартного **sprintf**,​ однако,​ очень редко можно встретить программу,​ расходующую основное время на преобразование данных,​ поэтому,​ использование библиотечных функций сокращает размер и время разработки программы.
 +
 +{{asm-extoptimize.20kb_Image_3.png?​553}}
 +
 +Рисунок 4 настоящие программисты (особенно старого поколения!) используют только консольные редакторы типа Multi-Edit или TSE-Pro с кучей специализированных функций и мощным макро-движком!
 +
 +Приведенный ниже пример распечатывает число, содержащееся в регистре EAX в шестнадцатеричной,​ десятичной и восьмеричной форме, автоматически дописывая ведущие нули, растягивающие число до 4х разрядов. А теперь попробуйте осуществить тоже самое без использования библиотек и сравните размер полученного кода!
 +
 +moveax, 666h; число, которое необходимо вывести на экран
 +
 +
 +
 +**; // переводим число в ****hex****,​ ****dec**** и ****oct**** системы исчисления в ****ASCII****-представлении**
 +
 +subesp, 60h; резервируем память под буфер куда пойдет результат
 +
 +movebx, esp; сохраняем указатель на буфер в регистре EBX
 +
 +push eax; \
 +
 +pusheax; ​ + - передаем число для преобразования ф-ции sprintf
 +
 +push eax; /
 +
 +pushoffsets;​ передаем в стек указатель на строку спецификаторов
 +
 +pushebx; передаем указатель на буфер для получения результата
 +
 +callsprintf;​ прямой вызов функции sprintf
 +
 +
 +
 +******; // вывод преобразованных данных на экран через диалоговое окно**
 +
 +xor eax,eax; eax := 0
 +
 +push eax; uType
 +
 +push eax; lpCaption
 +
 +pushebx; lpText (наши преобразованные данные)
 +
 +push eax; hWnd
 +
 +call ds:​[MessageBoxA];​ косвенныйвызов API-функции MessageBox
 +
 +
 +
 +addesp, 60h + (5*4); выталкиваем аргументы из стека и уничтожаем буфер
 +
 +...
 +
 +...
 +
 +...
 +
 +sdb "%04X hex == %04d dec == %04o oct",0
 +
 +; строка спецификаторов
 +
 +Листинг 4 фрагмент программы,​ принимающий число в регистре EAX и выводящий его на экран в шестнадцатеричной,​ десятеричной и восьмеричной формах
 +
 +{{asm-extoptimize.20kb_Image_4.png?​552}}
 +
 +Рисунок 5 вывод на экран числа в разных системах исчисления
 +
 +===== >>>​ врезка вызов API-функций из ассемблерный вставок =====
 +
 +При вызове API и DLL-функций из ассемблерных вставок возникает множество проблем,​ довольно туманно описанных в документации,​ прилагаемой к компилятору. Возьмем,​ к примеру,​ MicrosoftVisualC++ и попробуем вызывать функцию **GetVersion** так, как мы бы сделали бы это на чистом ассемблере:​
 +
 +__asm{
 +
 +call GetVersion; прямойвызов API-функции
 +
 +}
 +
 +Листинг 5 "​логичный",​ но неправильный способ вызова API-функций
 +
 +Компилируем файл с настройками по умолчанию и запускам. Программа тут же рушится. Почему?​ Смотрим в дизассемблере:​
 +
 +.text:​00401000 E8 FF 2F 00 00callnear ptr GetVersion
 +
 +...
 +
 +.idata:​00404004 ?? ?? ?? ??extrn GetVersion:​dword;​ DWORD GetVersion(void)
 +
 +Листинг 6 дизассемблер показываем,​ что вместо запланированного вызова API-функции,​ управление получает двойное слово с указателем на нее
 +
 +Так вот где собака порылась! Компилятор сгенерировал переход по адресу,​ где расположено двойное слово, принадлежащее таблице импорта (секция .idata) и содержащее указатель на API-функцию GetVersion.
 +
 +{{asm-extoptimize.20kb_Image_5.png?​553}}
 +
 +Рисунок 6 дизассемблер IDA Pro – мощное средство выявления ошибок в программах
 +
 +Неудивительно,​ что попытка интерпретации таблицы импорта как исполняемого кода приводит к краху и чтобы программа заработала правильно,​ необходимо использовать косвенную адресацию,​ заключив имя функции в квадратные скобки и выставив перед ними знак префикса **cs****:** или **ds****:** (без разницы,​ но ds работает чуточку быстрее).
 +
 +Правильный код выглядит так:
 +
 +__asm{
 +
 +call ds:​[GetVersion];​ косвенныйвызов API-функции
 +
 +}
 +
 +Листинг 7 "​не логичный",​ но правильный способ вызова API-функций
 +
 +При вызове функций,​ представленных в двух вариантах — ASCII и UNICODE, мы можем указывать суффиксы **A** и **W** явно, а можем использовать "​каноническое"​ имя функции без суффиксов,​ и тогда компилятор самостоятельно выберет нужный вариант в зависимости от настроек по умолчанию или ключей компиляции.
 +
 +__asm{
 +
 +; тут мы передаем аргументы
 +
 +callds:​[CreateProcessW];​ косвенный вызов функции с суффиксом W
 +
 +}
 +
 +Листинг 8 косвенный вызов функции CreateProcess с явным заданием суффикса W
 +
 +.text:​0040101Edb3Eh;​ ds:
 +
 +.text:​0040101EcallCreateProcessW;​ вызывается UNICODE-версияфункции
 +
 +Листинг 9 а вот его дизассемблерный листинг — вызывается именно та функция,​ которая была указана
 +
 +__asm{
 +
 +; тут мы передаем аргументы
 +
 +callds:​[CreateProcess];​ косвенный вызов функции без суффиксов
 +
 +}
 +
 +Листинг 10 косвенный вызов функции CreateProcess без указания суффиксов,​ предоставляющий компилятору свободу выбора одного из двух вариантов
 +
 +.text:​0040101Edb3Eh;​ ds:
 +
 +.text:​0040101Ecall ​ CreateProcessA;​ вызывается ASCII-версияфункции
 +
 +Листинг 11 компилятор выбрал ASCII-вариант,​ что соответствует его настройкам по умолчанию
 +
 +А вот при вызове функций типа system квадратные скобки ставить уже не надо, точнее нельзя! Функция system является частью библиотеки времени исполнения (RTL — RunTimeLibrary),​ линкуемой статическим образом,​ поэтому call system сработает как и ожидалось,​ а вот call ds:​[system] передаст управление по адресу 83EC8B55h, попытавшись проинтерпретировать начало функции system как указатель:​
 +
 +.text:​0040100B 3E FF 15 1A 10 40 00calldword ptr system
 +
 +; косвенный вызов статически линкуемой функции
 +
 +; приводит к тому, что первые 4 байта функции
 +
 +; интерпретируются как указатель и управление
 +
 +; передается по адресу 83EC8B55h
 +
 +...
 +
 +.text:​00401018 systemprocnear;​ началофункции system
 +
 +.text:​00401018 **55**pushebp
 +
 +.text:​00401019 **8B EC**movebp, esp
 +
 +.text:​0040101B **83** EC 10subesp, 10h
 +
 +.text:​0040101E 56pushesi
 +
 +Листинг 12 косвенный вызов статически линкуемых функций приводит к краху
 +
 +Таким образом,​ //**при вызове функций из ассемблерных вставок всегда следует учитывать специфику конкретной вызываемой функции,​ не надеясь на то, что компилятор сделает это за нас.**//
 +
 +При программировании на чистом ассемблере подобная проблема не возникает,​ поскольку имена и типы вызовов функций всегда объявляются вручную (или через включаемые файлы) и мы заранее знаем как именно интерпретирует их транслятор. При работе с ассемблерными вставками подобной определенности у нас нет. В частности,​ если компилятор решил использовать инкрементную линковку,​ то имя функции интерпретируется уже не как указатель на двойное слово из таблицы импорта,​ а как указатель на "​переходник",​ представляющего собой jmp [pFunc],​ то есть нам квадратные скобки снова отпадают!
 +
 +Инкрементная линковка обычно включается в режиме оптимизации,​ а в отладочном варианте — отсутствует. Сюрприз,​ да? При изменении ключей компиляции ассемблерные вставки изменяют свое поведение,​ причем безо всякого предупреждения!
 +
 +Короче говоря,​ внешние функции из ассемблерных вставок лучше не вызывать,​ а если и вызывать,​ то очень осторожно.
 +
 +{{asm-extoptimize.20kb_Image_6.png?​553}}
 +
 +Рисунок 7 а вообще же, выбор конкретного инструментария — дело вкуса, о которых,​ как известно,​ не спорят,​ в частности,​ потребности мыщъх'​а вполне удовлетворяет редактор,​ встроенный в FAR плюс несколько плагинов
 +
 +===== выделение памяти на стеке =====
 +
 +На процессорах 8086/8088 существовала замечательная возможность — затолкать в стек аргумент-указатель с одновременным выделением памяти всего одной (!) однобайтовой (!) машинной командой PUSH ESP, которая сначала уменьшала значение ESP, а только потом заталкивала его в стек. То есть, в стек попадало уже уменьшенное значение ESP, что способствовало трюкачеству.
 +
 +Рассмотрим конкретный пример — функцию,​ одним из аргументов которой является указатель на переменную,​ принимающую возвращаемый результат:​ f(inta, word *x). Предельно компактный вызов (на 8086!) выглядел так:
 +
 +pushsp; передаем указатель на x с одновременным выделением памяти под сам x
 +
 +pushsi; передаем переменную a
 +
 +callf; зовем функцию
 +
 +Листинг 13 трюкаческий пример,​ передающий указатель на переменную с одновременным выделением под нее памяти (только для 8086/8088!)
 +
 +Подвох в том, что переменная x возвращается в ячейке памяти,​ выделенной PUSHSP! То есть указатель на x указывает сам на себя, что хорошо видно в отладчике:​
 +
 +{{asm-extoptimize.20kb_Image_7.png?​553}}
 +
 +Рисунок 8 содержимое стека на момент вызова функции f на древней XT снабженной 8086 процессором — в отладчике хорошо видно, что в стек попадает уже уменьшенное значение регистра SP, в результате чего указатель *x указывает сам на себя!
 +
 +Начиная с 80286 логика работы инструкции PUSH ESP предательским образом изменилась и теперь процессор помещает в стек такое значение регистра ESP, каким оно было до модификации (кстати,​ псевдокод команды PUSH, приведенный в руководстве Intel содержит ошибку,​ из которой следует,​ что в стек помещается уменьшенное значение ESP, хотя на практике это не так!).
 +
 +И пока программисты спорят какое из двух решений "​идеологически"​ более "​правильное",​ прежний код отказывается работать,​ потому что команда PUSH ESP вместо указателя,​ указывающего на себя, теперь заталкивает в стек указатель на следующее двойное слово!
 +
 +{{asm-extoptimize.20kb_Image_8.png?​552}}
 +
 +Рисунок 9 содержимое стека на момент вызова функции f на современных процессорах — в отладчике хорошо видно, что в стек попадает такое значение регистра ESP, каким оно было _до_ модификации,​ в результате чего указатель *x указывает на _следующее_ двойное слово!
 +
 +Поэтому,​ при переходе с 8086 на 286+ приходится добавлять "​лишнюю"​ команду PUSH EAX, резервирующую ячейку на стеке, на которую будет указывать значение ESP, засланное в стек инструкцией PUSH ESP
 +
 +pusheax; выделяем память под переменную x (регистр — может быть любым)
 +
 +pushesp; передаем указатель на x как аргумент функции f
 +
 +pushesi; передаем переменную a
 +
 +call f; зовем f
 +
 +Листинг 14 трюкаческий пример,​ портированный на 286+ процессоры
 +
 +Несмотря на то, что 8086/8088 процессоры уже давно не встречаются в дикой природе (ну разве что в виде эмуляторов,​ да и то…), многие программы,​ написанные под них, актуальны и сегодня. Это касается как уже откомпилированного машинного кода, так и различных ассемблерных библиотек,​ переносимых под современные процессоры. Одна из причин,​ по которой они могут не работать — это и есть различие в логике обработке команды PUSH ESP.
 +
 +Вообще же, динамическое выделение памяти посредством PUSH + фиктивный регистр — вполне законный примем,​ которым пользуются не только люди, но и компиляторы. Это намного компактнее,​ чем обращение к локальным/​глобальным переменным,​ выделяемым классическим способом.
 +
 +Естественно,​ большие объемы памяти лучше всего выделять с помощью **SUB ESP****,​**** XXh**,​ но при этом следует помнить как минимум о двух вещах. Первое и главное — Windows-системы выделяют стековую память динамически,​ используя для этого специальную "​сторожевую"​ страницу памяти (pageguard). Как только к ней происходит обращение — система выделяет еще одну или несколько страниц памяти,​ перемещая сторожевую страницу наверх (в сторону меньших адресов памяти). При последовательном "​росте"​ стека все работает нормально,​ но если попытаться прыгнуть за сторожевую страницу,​ сразу же возникнет непредвиденное исключение — ведь никакой памяти по данному адресу еще нет — и работа программы завершается в аварийном режиме. То есть, если у нас есть к примеру 1 Мбайт стекового пространства,​ это еще не значит,​ что код SUB ESP,​ 10000h/​MOV [ESP],​EAX будет работать. Тут уж как повезет (или не повезет). Если ранее вызываемые функции выделяли стековую память планомерно,​ задвинув сторожевую страницу куда-то вглубь стекового пространства,​ то какие-то шансы у нас есть, но полагаться на них — несерьезно. Поэтому,​ при выделении под локальные переменные более 4х Кбайт,​ необходимо выполнить цикл, последовательно обращающийся хотя бы к одной ячейке каждой из запрашиваемых страниц. Читать все ячейки — необязательно,​ да и непроизводительно.
 +
 +Компиляторы делают это автоматически,​ а вот многие ассеблерщики о таком коварстве Windows зачастую даже и не подозревают,​ а потом упорно ищут бага в своей программе,​ не понимая почему она не работает!
 +
 +
 +
 +main()
 +
 +{
 +
 +charx[1024*1024];//​ выделяем 1 Мбайт стековой памяти
 +
 +return *x;// обращаемся к наиболее "​дальней"​ стековой ячейке
 +
 +}
 +
 +Листинг 15 пример программы на Си, выделяющий 1 Мбайт памяти под локальные переменные и обращающийся к самой "​дальней"​ ячейке
 +
 +.text:​00401000 _mainproc near
 +
 +.text:​00401000moveax,​ 100000h
 +
 +.text:​00401005call__alloca_probe
 +
 +.text:​0040100Amovsxeax,​ byte ptr [esp]
 +
 +.text:​00401012addesp,​ 100000h
 +
 +.text:​00401018retn
 +
 +.text:​00401018 _mainendp
 +
 +...
 +
 +.text:​00401020 __alloca_probeproc near; CODE XREF: _main+5↑p
 +
 +.text:​00401020
 +
 +.text:​00401020 arg_0= dword ptr  8
 +
 +.text:​00401020
 +
 +.text:​00401020pushecx
 +
 +.text:​00401021cmpeax,​ 1000h
 +
 +.text:​00401026leaecx,​ [esp+arg_0]
 +
 +.text:​0040102Ajbshort loc_401040
 +
 +.text:​0040102C
 +
 +.text:​0040102C loc_40102C:;​ CODE XREF: __alloca_probe+1E↓j
 +
 +.text:​0040102Csubecx,​ 1000h
 +
 +.text:​00401032subeax,​ 1000h
 +
 +.text:​00401037test[ecx],​ eax
 +
 +.text:​00401039cmpeax,​ 1000h
 +
 +.text:​0040103Ejnbshort loc_40102C
 +
 +.text:​00401040
 +
 +.text:​00401040 loc_401040:;​ CODE XREF: __alloca_probe+A↑j
 +
 +.text:​00401040subecx,​ eax
 +
 +.text:​00401042moveax,​ esp
 +
 +.text:​00401044test[ecx],​ eax
 +
 +.text:​00401046movesp,​ ecx
 +
 +.text:​00401048movecx,​ [eax]
 +
 +.text:​0040104Amoveax,​ [eax+4]
 +
 +.text:​0040104Dpusheax
 +
 +.text:​0040104Eretn
 +
 +.text:​0040104E __alloca_probeendp
 +
 +Листинг 16 при выделении большого объема локальных переменных,​ компилятор вызывает недокументированную функцию __alloca_probe,​ совершающую "​пробежку"​ по стеку и при необходимости отодвигающую сторожевую страницу на требуемое расстояние,​ то же самое необходимо делать и в ассемблерных программах!
 +
 +Но коварство Windows на этом не заканчиваются. Многие API-функции неявно закладываются на выравнивание стека и если нам, к примеру,​ требуется ровно 69h байт стековой памяти,​ ни в коем случае нельзя писать SUB ESP,​69h,​ иначе все рухнет! Следует округлить 69h по границе двойного слова и запросить 6Ch байт или... между актами выделения/​освобождения памяти не вызывать никаких API-функций.
 +
 +Часто, в погоне за оптимизацией,​ программисты,​ борющиеся за каждый байт памяти,​ забывают о выравнивании и… часами ищут причину,​ по которой оптимизированный вариант программы отказывается работать.
 +
 +{{asm-extoptimize.20kb_Image_9.jpg?​516}}
 +
 +Рисунок 10 проблемы выравнивания в оптимизации
 +
 +===== заключение =====
 +
 +Системное программирование хранит множество секретов,​ загадок и тайн, постепенно становясь уделом небольшой горстки профессионалов,​ в то время как мир дружно сходит с ума, подсаживаясь на языки высокого уровня,​ которые чем дальше — тем все выше и выше. Об ассемблере вспоминают только тогда, когда требуется что-то очень сильно нестандартное,​ с чем компилятор уже не справляется или сгенерированный им код не отвечает требованиям производительности.
 +
 +Вот тут-то и выясняется,​ что специалистов,​ владеющих ассемблеров,​ практически нет, а те что есть, уже утратили свои навыки и оптимизируют намного хуже компиляторов,​ разработчики которых за последние несколько лет сделали качественный рывок вперед и теперь просто так их не обгонишь! Сам по себе ассемблер не обеспечивает ни компактности кода, ни высокой скорости. Все решают хитрые трюки и приемы программирования,​ находчивость и инженерная смекалка наконец!
 +
 +Главное — выбрать верную стратегию поведения. Не пытаться сократить программу на пару байт, которые все равно будут потеряны при выравнивании,​ а реально оценивать свой творческий потенциал,​ сопоставляя его с целями и задачами операциями. Алгоритмическая оптимизация зачастую ускоряет программу в десятки раз, в то время как перенос Сишного кода на ассемблер дает в среднем случае 10%-15% выигрыш. Но это еще не значит,​ что ассемблер бесполезен. Просто,​ как и любой другой инструмент,​ он имеет границы своей применимости,​ с которыми следует считаться,​ чтобы не попасть впросак!
 +
 +