Использование LEA для значений, которые не являются адресами / указателями?
Я пытался понять, как работает инструкция вычисления адреса, особенно с leaq
команда. Тогда я запутываюсь, когда вижу примеры использования leaq
делать арифметические вычисления. Например, следующий код C,
long m12(long x) {
return x*12;
}
В сборе,
leaq (%rdi, %rdi, 2), %rax
salq $2, $rax
Если мое понимание верно, Лейк должен переместить любой адрес (%rdi, %rdi, 2)
, который должен быть 2*%rdi+%rdi
, оценить, чтобы в %rax
, Что я запутался, так как значение х хранится в %rdi
, который является просто адресом памяти, почему времена%rdi на 3, а затем сдвиг влево этого адреса памяти на 2 равны х раз 12? Разве это не когда мы %rdi
на 3 мы переходим к другому адресу памяти, который не содержит значения x?
4 ответа
leaq
не должен работать с адресами памяти, и он вычисляет адрес, он фактически не читает из результата, так что до mov
или подобное пытается использовать это, это просто эзотерический способ добавить одно число, плюс 1, 2, 4 или 8 раз другое число (или то же самое число в этом случае). Как вы видите, этим часто злоупотребляют в математических целях. 2*%rdi+%rdi
просто 3 * %rdi
так что это вычисления x * 3
без использования умножителя на процессоре.
Аналогично, сдвиг влево для целых чисел удваивает значение для каждого сдвинутого бита (каждый ноль, добавленный справа), благодаря тому, как работают двоичные числа (то же самое в десятичных числах, добавление нулей справа умножается на 10).
Так что это злоупотребляет leaq
инструкция для выполнения умножения на 3, затем смещение результата для достижения дальнейшего умножения на 4 для окончательного результата умножения на 12 без фактического использования команды умножения (которая, по-видимому, полагает, будет выполняться медленнее, и, насколько я знаю, может быть прав: второе предположение - компилятор, как правило, проигрышная игра).
lea
(см. руководство по набору инструкций Intel) - это инструкция сдвига и добавления, которая использует синтаксис операндов памяти и машинное кодирование. Это объясняет название, но это не единственное, для чего это хорошо. На самом деле он никогда не обращается к памяти, так что это похоже на использование &
в С.
См., Например, Как умножить регистр на 37, используя только 2 последовательные инструкции в файле x86?
В С это как uintptr_t foo = &arr[idx]
, Обратите внимание &
чтобы дать вам результат arr + idx
в том числе масштабирование для размера объекта arr
, В C это было бы злоупотреблением синтаксисом и типами языка, но в x86 указатели на ассемблеры и целые числа - это одно и то же. Все только байты, и программа должна поместить инструкции в правильном порядке, чтобы получить полезные результаты.
Первоначальный разработчик / архитектор набора инструкций 8086 ( Стивен Морс) мог или не мог иметь в виду математику указателя как основной вариант использования, но современные компиляторы считают его просто еще одним вариантом выполнения арифметики с указателями / целыми числами, и это как вы должны думать об этом тоже.
(Обратите внимание, что 16-битные режимы адресации не включают смены, просто [BP|BX] + [SI|DI] + disp8/disp16
Таким образом, LEA не был столь полезен для математики без указателей до 386. См. этот ответ для более подробной информации о 32/64-битных режимах адресации, хотя в этом ответе используется синтаксис Intel, такой как [rax + rdi*4]
вместо синтаксиса AT&T, используемого в этом вопросе. Машинный код x86 остается одинаковым независимо от того, какой синтаксис вы используете для его создания.)
Может быть, архитекторы 8086 просто хотели показать оборудование для вычисления адресов для произвольного использования, потому что они могли сделать это без использования большого количества дополнительных транзисторов. Декодер уже должен уметь декодировать режимы адресации, а другие части ЦП должны уметь вычислять адреса. Для помещения результата в регистр вместо использования его со значением сегмента-регистра для доступа к памяти не требуется много дополнительных транзисторов. Росс Ридж подтверждает, что LEA на оригинальном 8086 повторно использует аппаратные средства декодирования и вычисления эффективного адреса ЦП.
Обратите внимание, что большинство современных процессоров выполняют LEA на тех же ALU, что и обычные инструкции добавления и переключения. Они имеют выделенные AGU (блоки генерации адресов), но используют их только для реальных операндов памяти. Атом порядка - одно исключение; LEA работает в конвейере раньше, чем ALU: входы должны быть готовы быстрее, но выходы также готовы раньше. Исполнительные процессоры с неупорядоченным исполнением (подавляющее большинство для современных x86) не хотят, чтобы LEA мешал с реальной загрузкой / хранением, поэтому они запускают его на ALU.
lea
имеет хорошую задержку и пропускную способность, но не такую хорошую, как add
или же mov r32, imm32
на большинстве процессоров, так что используйте только lea
когда вы можете сохранить инструкции с ним вместо add
, (См. Руководство по микроарху x86 Agner Fog и руководство по оптимизации asm.)
Внутренняя реализация не имеет значения, но можно с уверенностью сказать, что декодирование операндов в LEA делит транзисторы с режимами декодирования адресации для любой другой инструкции. (Таким образом, есть аппаратное повторное использование / совместное использование даже на современных процессорах, которые не работают lea
на AGU.) Любой другой способ раскрытия мульти-входной инструкции сдвига и сложения потребовал бы специального кодирования для операндов.
Таким образом, 386 получил инструкцию ALU "сдвиг и добавление" для "свободной", когда он расширил режимы адресации для включения масштабируемого индекса, а возможность использовать любой регистр в режиме адресации сделала LEA намного проще для использования и для не указателей.,
x86-64 получил дешевый доступ к счетчику программ ( вместо необходимости читать что call
нажал) "бесплатно" через LEA, потому что он добавил режим относительной RIP, делая доступ к статическим данным в позиционно-независимом коде x86-64 значительно дешевле, чем в 32-битном PIC. (Относительный RIP действительно нуждается в специальной поддержке в ALU, которые обрабатывают LEA, а также в отдельных AGU, которые обрабатывают фактические адреса загрузки / сохранения. Но никаких новых инструкций не требовалось.)
Это так же хорошо для произвольной арифметики, как и для указателей, поэтому ошибочно думать о ней как о предназначенной для указателей в наши дни. Это не "злоупотребление" или "уловка", чтобы использовать его для не указателей, потому что все в языке ассемблера является целым числом. Имеет более низкую пропускную способность, чем add
, но он достаточно дешев, чтобы использовать его почти все время, когда он сохраняет хотя бы одну инструкцию. Но он может сохранить до трех инструкций:
;; Intel syntax.
lea eax, [rdi + rsi*4 - 8] ; 3 cycle latency on Intel SnB-family
; 2-component LEA is only 1c latency
;;; without LEA:
mov eax, esi ; maybe 0 cycle latency, otherwise 1
shl eax, 2 ; 1 cycle latency
add eax, edi ; 1 cycle latency
sub eax, 8 ; 1 cycle latency
На некоторых процессорах AMD даже комплексный LEA имеет задержку только в 2 цикла, но последовательность из 4 команд будет иметь задержку в 4 цикла от esi
быть готовым к финалу eax
быть готовым В любом случае, это экономит 3 мопа для внешнего интерфейса для декодирования и выдачи, и это занимает место в буфере переупорядочения вплоть до выхода на пенсию.
lea
имеет несколько основных преимуществ, особенно в 32/64-битном коде, где режимы адресации могут использовать любой регистр и могут сдвигаться:
- неразрушающий: вывод в регистр, который не является одним из входов. Иногда это полезно, как просто скопировать и добавить как
lea 1(%rdi), %eax
или жеlea (%rdx, %rbp), %ecx
, - может выполнять 3 или 4 операции в одной инструкции (см. выше).
- Математика без изменения EFLAGS, может быть полезна после теста перед
cmovcc
, Или, может быть, в цикле add-with-carry на процессорах с частичным остановом флагов. x86-64: позиционно-независимый код может использовать REA-относительный LEA для получения указателя на статические данные.
7-байтовое
lea foo(%rip), %rdi
немного больше и медленнее, чемmov $foo, %edi
(5 байт), поэтому предпочитаюmov r32, imm32
в позиционно-зависимом коде в ОС, где символы находятся в младших 32 битах виртуального адресного пространства, как в Linux. Возможно, вам придется отключить настройку PIE по умолчанию в gcc, чтобы использовать это.В 32-битном коде
mov edi, OFFSET symbol
короче и быстрее чемlea edi, [symbol]
, (ОставьтеOFFSET
в синтаксисе NASM.) RIP-относительный недоступен, а адреса вписываются в 32-разрядные, поэтому нет причин для рассмотренияlea
вместоmov r32, imm32
если вам нужно получить статические адреса символов в регистрах.
За исключением REA-относительного LEA в режиме x86-64, все они в равной степени применимы к вычислению указателей по сравнению с вычислением целочисленных добавлений / сдвигов без указателей.
См. Также вики-теги x86 для руководств / руководств по сборке и информации о производительности.
Размер операнда и размер адреса для x86-64 lea
См. Также Какие целые операции дополнения 2 можно использовать без обнуления старших битов на входах, если требуется только младшая часть результата?, 64-разрядный размер адреса и 32-разрядный размер операнда являются наиболее компактной кодировкой (без дополнительных префиксов), поэтому предпочитайте lea (%rdx, %rbp), %ecx
когда это возможно вместо 64-битной lea (%rdx, %rbp), %rcx
или 32-разрядный lea (%edx, %ebp), %ecx
,
x86-64 lea (%edx, %ebp), %ecx
всегда трата префикса размера адреса против lea (%rdx, %rbp), %ecx
, но 64-битный адрес / размер операнда явно необходим для выполнения 64-битной математики. (Дизассемблер Agner Fog objconv даже предупреждает о бесполезных префиксах размера адреса в LEA с 32-битным размером операнда.)
За исключением, может быть, Райзена, где Агнер Фог сообщает, что размер 32-битного операнда lea
в 64-битном режиме имеет дополнительный цикл задержки. Я не знаю, может ли переопределение размера адреса на 32-битный ускорить LEA в 64-битном режиме, если вам нужно усечь его до 32-битного.
Этот вопрос является почти дубликатом очень высоко проголосовавших. Какова цель инструкции LEA?, но большинство ответов объясняют это с точки зрения вычисления адреса на фактических данных указателя. Это только одно использование.
LEA для расчета адреса. Это не разыменовывает адрес памяти
Это должно быть намного более читабельным в синтаксисе Intel
m12(long):
lea rax, [rdi+rdi*2]
sal rax, 2
ret
Таким образом, первая строка эквивалентна rax = rdi*3
Затем сдвиг влево умножить Rax на 4, что приводит к rdi*3*4 = rdi*12
Я думаю, путаница возникает из-за того, что первый операнд,(%rdi, %rdi, 2)
выглядит как ссылка на память.
Из книги «Компьютерные системы: взгляд программиста» Рэндала Брайанта и Дэвида О'Халларона о:
Ее первый операнд выглядит как ссылка на память, но вместо чтения из назначенного места инструкция копирует эффективный адрес в место назначения.
И вот соответствующая часть:
Эту инструкцию можно использовать для создания указателей для последующих обращений к памяти. Кроме того, его можно использовать для компактного описания распространенных арифметических операций. Например, если зарегистрироваться
rdx
содержит значениеx
, то инструкцияleaq 7(%rdx,%rdx, 4) , %rax
установим регистр%rax
до 5х+7. Составители часто находят умное применениеleaq
это не имеет ничего общего с эффективными вычислениями адресов.