Ответ 1
mov
+ adc $-1, %eax
более эффективен, чем xor
-zero + setc
+ 3-компонентный lea
для обоих значений задержки и числа UOP, и не хуже для любых по-прежнему актуальных CPU. 1
Это похоже на пропущенную оптимизацию gcc: он, вероятно, видит особый случай и фиксируется на нем, стреляя себе в ногу и предотвращая распознавание паттерна adc
.
Я не знаю, что именно он увидел/искал, так что да, вы должны сообщить об этом как об ошибке при пропущенной оптимизации. Или, если вы хотите копать глубже, вы можете посмотреть результаты GIMPLE или RTL после прохождения оптимизации и посмотреть, что произойдет. Если вы знаете что-нибудь о внутренних представительствах GCC. У Godbolt есть окно дампа дерева GIMPLE, которое вы можете добавить из того же выпадающего меню, что и "клон компилятора".
Тот факт, что clang компилирует его с помощью adc
доказывает, что он допустим, т.е. Требуемый asm соответствует источнику 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
правильно.
Смысл его существования заключается в том, чтобы позволить вам выражать добавление с переносом, а также выполнением, что сложно в чистом 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, к сожалению, отстой в расширенной точности /biginteger, написанном на чистом C. Clang иногда работает немного лучше, но большинство компиляторов плохо в этом разбираются. Вот почему функции самого низкого уровня gmplib написаны от руки в asm для большинства архитектур.
Сноска 1: или для счетчика мопов: равняется Intel Haswell и более ранним, где adc
равен 2 моп, за исключением немедленного нуля, когда особый случай декодеров семейства Sandybridge равен 1 моп.
Но трехкомпонентный LEA с base + index + disp
делает его инструкцией с 3 циклами задержки на процессорах Intel, так что это определенно хуже.
В Intel Broadwell и более поздних версиях adc
- это команда с 1 мопом, даже с немедленным -zero, использующим поддержку мопов с 3 входами, введенных в Haswell для FMA.
Таким образом, равное общее количество мопов, но худшая задержка означает, что adc
все равно будет лучшим выбором.