mov
+ adc $-1, %eax
более эффективен, чем xor
-нулевой + setc
+ 3-компонентный lea
как для задержки, так и для счетчика uop на большинстве процессоров, и не хуже для любых соответствующие процессоры. 1
Это похоже на gcc пропущенную оптимизацию : он, вероятно, видит особый случай и фиксируется на нем, стреляя себе в ногу и предотвращая распознавание паттерна adc
.
Я не знаю, что именно он увидел / искал, так что да, вы должны сообщить об этом как об ошибке с пропущенной оптимизацией. Или, если вы хотите копать глубже, вы можете посмотреть результаты GIMPLE или RTL после прохождения оптимизации и посмотреть, что произойдет. Если вы знаете что-нибудь о внутренних представительствах GCC. У Godbolt есть окно дампа дерева GIMPLE, которое вы можете добавить из того же выпадающего меню, что и "клон компилятора".
Тот факт, что clang компилирует его с adc
, доказывает, что это законно, т. Е. Требуемый асм соответствует исходному коду C ++, и вы не пропустили какой-то особый случай, мешающий компилятору выполнить эту оптимизацию. (Предполагая, что clang не содержит ошибок, как здесь).
Эта проблема, безусловно, может возникнуть, если вы не будете осторожны, например, Попытка написать функцию общего вида adc
, которая принимает перенос и обеспечивает перенос из сложения с 3 входами, трудна в C, потому что любое из двух дополнений может выполнять, поэтому вы не можете просто использовать sum < a+b
идиома после добавления переноса к одному из входов. Я не уверен, что можно заставить gcc или clang испускать add/adc/adc
, где середина adc
должна взять перенос и произвести вынос.
например. 0xff...ff + 1
оборачивается до 0, поэтому sum = a+b+carry_in
/ carry_out = sum < a
не может оптимизироваться до adc
, поскольку ему необходимо игнорировать перенос в специальном случае, где a = -1
и carry_in = 1
.
Итак, другое предположение, что, возможно, gcc подумал о том, чтобы сделать + X
ранее, и выстрелил себе в ногу из-за этого особого случая. Это не имеет большого смысла, хотя.
Какой смысл его использовать, поскольку я должен предоставить флаг переноса?
Вы используете _addcarry_u32
правильно.
Смысл его существования в том, чтобы позволить вам выразить добавление с помощью переноса в , а также переноса out , что сложно в чистом C. GCC и clang не оптимизируют это хорошо, часто не просто сохраняя результат переноса в CF.
Если вы хотите только вынос, вы можете предоставить 0
в качестве переноса, и он будет оптимизирован до add
вместо adc
, но все равно даст вам выноску как переменную C.
например. чтобы добавить два 128-битных целых числа в 32-битные порции, вы можете сделать это
// bad on x86-64 because it doesn't optimize the same as 2x _addcary_u64
// even though __restrict guarantees non-overlap.
void adc_128bit(unsigned *__restrict dst, const unsigned *__restrict src)
{
unsigned char carry;
carry = _addcarry_u32(0, dst[0], src[0], &dst[0]);
carry = _addcarry_u32(carry, dst[1], src[1], &dst[1]);
carry = _addcarry_u32(carry, dst[2], src[2], &dst[2]);
carry = _addcarry_u32(carry, dst[3], src[3], &dst[3]);
}
( На Годболте с GCC / clang / ICC )
Это очень неэффективно по сравнению с unsigned __int128
, где компиляторы просто использовали бы 64-битный add / adc, но заставляют clang и ICC выдавать цепочку add
/ adc
/ adc
/ adc
. GCC делает беспорядок, используя setcc
для хранения CF в целое число для некоторых шагов, затем add dl, -1
, чтобы вернуть его в CF для adc
.
GCC, к сожалению, отстой в расширенной точности / большойинтеграции, написанной на чистом C. Clang иногда работает немного лучше, но большинство компиляторов в этом плохи Вот почему функции самого низкого уровня gmplib написаны от руки в asm для большинства архитектур.
Сноска 1 : или для количества мопов: равно Intel Haswell и более ранним, где adc
равно 2 моп, за исключением немедленного нуля, когда особый случай декодеров семейства Sandybridge равен 1 моп.
Но 3-компонентный LEA с base + index + disp
делает его инструкцией с 3-тактным временем ожидания для процессоров Intel, так что это определенно хуже.
В Intel Broadwell и более поздних версиях adc
- это команда с 1 моп, даже с ненулевым моментом, использующая поддержку мопов с 3 входами, представленную в Haswell для FMA.
Таким образом, равное общее количество мопов, но худшая задержка означает, что adc
все равно будет лучшим выбором.
https://agner.org/optimize/