Содержание статьи
Турбопередача стековых аргументов
Передачу аргументов через стек можно существенно ускорить, в случае если аргументы представляют собой константу, известную еще на стадии трансляции. Классический способ передачи выглядит так:
Классический способ передачи стековых аргументов
00000000: 6869060000 push 00000066900000005: 6899090000 push 0000009990000000A: 6896060000 push 0000006960000000F: E852060000 call 000000666
Довольно расточительное (в плане процессорных тактов) решение, особенно если функция вызывается многократно. При этом операнды команды PUSH
перегоняются из секции .
(находящейся в кодовой кэш‑памяти первого уровня) в область стека, находящуюся в кэш‑памяти данных. Ну и зачем гонять их туда и обратно, когда аргументы можно использовать непосредственно по месту хранения?
Усовершенствованный пример выглядит так:
.codeMOV EBP, ESPMOV ESP, offset func_arg + 4CALL my_funcMOV ESP, EBP.datafunc_arg DD 00h, 696h, 999h, 669h
И хотя размер кода после оптимизации не только не сократился, но даже увеличился (14h байт до оптимизации и 1Eh - после), мы сохранили немного стековой памяти и сократили время выполнения. Причем чем больше аргументов передается функции, тем в более выигрышном положении оказывается оптимизированный вариант, поскольку неоптимизированный вынужден тратить на каждый аргумент один дополнительный байт!
00000000: 8BEC mov ebp, esp00000002: BC66000000 mov esp, 00000001300000007: E80E000000 call 0000006660000000C: 8BE5 mov esp, ebp…0000000E: 00 00 00 00 96 06 00 00 ? 99 09 00 00 69 06 00 000000001E:
Несколько замечаний по поводу. Первое. Операционные системы семейства Windows NT (к которым принадлежат Windows 2000, Windows XP, Windows Vista, Windows Server 2003 и Windows Server Longhorn) гарантируют целостность содержимого стека выше его вершины (для адресов меньших, чем ESP), поэтому переносят такие извращения безо всякого ущерба для работоспособности программы. Операционные системы семейства Windows 9x ведут себя иначе, бесцеремонно используя все, что находится выше ESP в целях «производственной необходимости», что ведет к искажению секции данных и последующему краху программы. Поэтому все, что было сказано здесь, распространяется только на NT.
Замечание номер два. Перед аргументами необходимо оставить двойное слово (а в 64-битном режиме — четвертное) для сохранения адреса возврата. При этом секция данных, где находится это слово, должна быть доступна на запись. Если же функция вызывается из одного единственного места и адрес возврата известен заранее, ничего не мешает положить его рядом с аргументами. Но тогда функцию придется пускать командой jump, а не call, что еще больше увеличивает производительность:
Вызов функции с предопределенным адресом возврата командой JMP
.codeMOV EBP, ESPMOV ESP, offset func_arg + 4JMP my_funchere:MOV ESP, EBP.datafunc_arg DD offset here, 696h, 999h, 669h
Кстати говоря, ни адрес возврата, ни аргументы функции вовсе не обязаны быть константами, известными на стадии компиляции, и они могут свободно модифицироваться в любой момент командами MOV
и STOS
. Также если аргументы хранятся в локальных переменных, то засылать их в стек не обязательно! Достаточно лишь скорректировать регистр ESP таким образом, чтобы переменные‑аргументы оказались на вершине. Естественно, порядок размещения аргументов в памяти должен совпадать с порядком передачи аргументов, но на ассемблере, в отличие от языков высокого уровня, мы можем самостоятельно выбирать нужную схему размещения переменных, так что это не проблема.
Еще одна тонкость: «оптимизированный» вариант обладает всеми формальными атрибутами «передачи по значению», но де‑факто аргументы передаются по ссылке. То есть совсем наоборот! Аргументы передаются по значению, но это значение после выхода из функции сохраняет свое состояние, ведет себя так, как будто бы оно было передано по ссылке. Иногда это экономит такты процессора и сокращает потребности в памяти, но иногда ведет к трудноуловимым ошибкам, лишний раз подтверждая тезис, что нет в мире совершенства.
И последнее: при всех этих играх со стеком следует помнить, что целый ряд API-функций требует, чтобы указатель стека был выровнен на границу четырех байтов. Нарушение этого правила ведет к непредсказуемым последствиям.
Повторное использование кадра стека
При входе внутрь функции большое количество локальных переменных инициализируется константами или значениями, инвариантными по отношению к самой функции (то есть другими переменными, чаще всего глобальными). Причем инициализация обычно осуществляется командой MOV
, а для обслуживания строковых переменных приходится прибегать к REP
. Все это медленно, громоздко и непроизводительно.
А почему бы не подготовить кадр стека еще на стадии трансляции?! В грубом приближении это будет выглядеть так:
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»