Эти инструкции не эквивалентны. Для LEA rax
- это чистый вывод. Для вашего ожидаемого add
это rax += rsi
, поэтому компилятору сначала нужно будет mov %rdi, %rax
. Это менее эффективно, поэтому оно этого не делает.
lea
- это совершенно нормальный способ для компиляторов реализовать dst = src1 + src2
, сохраняя инструкцию mov
. В общем, не надоНе ожидайте, что операторы C будут компилироваться в инструкции, названные в их честь. Особенно маленькие сдвиги влево и добавление или умножение на 3, 5 или 9, потому что это главные цели для оптимизации с LEA. например, lea (%rsi, %rsi, 2), %rax
реализует result = y*3
. См. Использование LEA для значений, которые не являются адресами / указателями? для получения дополнительной информации. LEA также полезен, чтобы избежать разрушения любого из входов, если они оба необходимы позже.
Предполагая, что вы имели в виду t3
, чтобы быть такой же переменной, как temp3
, clang делает компилируйте, как вы ожидали, выполняя лучшую работу по распределению регистров, чтобы она могла использовать более короткую и более эффективную инструкцию add
без каких-либо дополнительных mov
инструкций вместо необходимости lea
.
Clang выбирает лучшее распределение регистров, чем GCC , поэтому он может просто использовать add
вместо необходимости lea
для последней инструкции. ( Godbolt ). Это сохраняет размер кода (из-за режима индексированной адресации), и add
имеет чуть лучшую пропускную способность, чем LEA, на большинстве процессоров, например 4 / тактовый вместо 2 / тактовый.
Clang также оптимизировал сдвиги вandl $1, %eax
/ negq %rax
для создания результата 0 или -1
этого арифметического сдвига вправо = передача в битах. Он также оптимизирован до 32-битного размера операнда для первых нескольких шагов, потому что сдвиги отбрасывают все, кроме младшего бита temp1
.
# side by side comparison, like the Godbolt diff pane
clang: | gcc:
movl %edi, %eax movq %rdi, %rax
subl %edx, %eax subq %rdx, %rdi
imull %edi, %eax imulq %rax, %rdi # temp1
andl $1, %eax salq $63, %rdi
negq %rax sarq $63, %rdi # temp2
xorq %rdi, %rax xorq %rax, %rdi # temp3
addq %rsi, %rax leaq (%rdi,%rsi), %rax # answer
retq ret
Обратите внимание, что clang выбрал imul %edi, %eax
(в RAX)но GCC решил размножиться в RDI. Это различие в распределении регистров, которое приводит к тому, что GCC требуется lea
в конце вместо add
.
Компиляторы иногда даже застревают с дополнительной инструкцией mov
в конце небольшой функциикогда они делают неправильный выбор, как это, если последняя операция не была чем-то вроде сложения, которое можно сделать с lea
в качестве неразрушающего операционного копирования. Это ошибки пропущенной оптимизации;вы можете сообщить о них в bugzilla GCC.
Другие пропущенные оптимизации
GCC и clang могли бы быть оптимизированы с помощью and
вместо imul
для установкимладший бит, только если оба входа нечетные.
Кроме того, поскольку имеет значение только младший бит вывода sub
, сработал бы XOR (добавление без переноса) или даже добавление! (Нечетный + -even = нечетный. Четный + -even = четный. Нечетный + -odd = нечетный.) Это позволило бы lea
вместо mov/sub
в качестве первой инструкции.
lea (%rdi,%rsi), %eax
and %edi, %eax # low bit matches (x-z)*x
andl $1, %eax # keep only the low bit
negq %rax # temp2
Позволяет сделатьТаблица истинности для младших битов x
и z
, чтобы увидеть, как это встряхивает, если мы хотим оптимизировать больше / иначе:
# truth table for low bit: input to shifts that broadcasts this to all bits
x&1 | z&1 | x-z = x^z | x*(x-z) = x & (x-z)
0 0 0 0
0 1 1 0
1 0 1 1
1 1 0 0
x & (~z) = BMI1 andn
Итак temp2 = (x^z) & x & 1 ? -1 : 0
. Но также temp2 = -((x & ~z) & 1)
. Мы можем изменить это на -((x&1) & ~z)
, что позволяет нам начинать с not z
и and $1, x
параллельно, для лучшего ILP. Или, если z
может быть готов в первую очередь, мы можем выполнить над ним операции и сократить критический путь с x
-> answer
, за счет z
.
или с * 1086. * BMI1 andn
инструкция, которая делает (~z) & x
, мы можем сделать это в одной инструкции. (Плюс еще один для выделения младшего бита)
Я думаю, что эта функция имеет одинаковое поведение для каждого возможного ввода, поэтому компиляторы могли бы выдавать ее из вашего исходного кода. Это одна возможность, которую вы должны пожелать, чтобы ваш компилятор выдал:
# hand-optimized
# long someFunc(long x, long y, long z)
someFunc:
not %edx # ~z
and $1, %edx
and %edi, %edx # x&1 & ~z = low bit of temp1
neg %rdx # temp2 = 0 or -1
xor %rdi, %rdx # temp3 = x or ~x
lea (%rsi, %rdx), %rax # answer = y + temp3
ret
Так что ILP все еще не существует, если z
не будет готов до x
и /или y
. Используя дополнительную инструкцию mov
, мы могли бы сделать x&1
параллельно с not z
Возможно, вы могли бы сделать что-то с test
/ setz
или cmov
, но IDK, если это побьетlea / and (temp1) + и / neg (temp2) + xor + add.
Я не смотрел на оптимизацию финального xor и add, но учтите, что temp3
в основном является условным НЕ x
. Вы можете улучшить задержку за счет пропускной способности, рассчитав оба способа одновременно и выбрав их с помощью cmov. Возможно, с помощью идентичности дополнения 2, что -x - 1 = ~x
. Может быть, улучшить ILP / латентность, выполнив x+y
, а затем исправив это с помощью чего-то, что зависит от условий x и z? Поскольку мы не можем вычесть с помощью LEA, кажется, что лучше просто НЕ и ДОБАВИТЬ.
# return y + x or y + (~x) according to the condition on x and z
someFunc:
lea (%rsi, %rdi), %rax # y + x
andn %edi, %edx, %ecx # ecx = x & (~z)
not %rdi # ~x
add %rsi, %rdi # y + (~x)
test $1, %cl
cmovnz %rdi, %rax # select between y+x and y+~x
retq
Это имеет больше ILP, но требует BMI1 andn
, чтобы все еще быть только 6 (однократными) командами. Бродвелл и позже имеют однопользовательский CMOV;в более ранних версиях Intel это 2 мопа.
Другая функция может быть 5 моп с использованием BMI andn
.
В этой версии первые 3 инструкции могут выполняться в первом цикле, предполагая, что x, y и z все готовы. Затем во 2-м цикле можно запустить ADD и TEST. В 3-м цикле CMOV может работать, принимая целочисленные входные значения от LEA, ADD и входные данные флага от TEST. Таким образом, общая задержка от x-> answer, y-> answer или z-> answer в этой версии составляет 3 цикла. (Предполагая однократный / однократный цикл). Прекрасно, если он находится на критическом пути, и не очень актуален, если он является частью независимой цепочки развертывания, и пропускная способность - все, что имеет значение.
против5 (andn) или 6 циклов (без) для предыдущей попытки. Или еще хуже для вывода компилятором, использующим imul
вместо and
(задержка 3 цикла только для этой инструкции).