GCC имеет 128-битные целые числа. Используя их, я могу заставить компилятор использовать инструкции mul
(или imul
только с одним операндом). Например
uint64_t x,y;
unsigned __in128 z = (unsigned __int128)x*y;
создает mul
. Я использовал это для создания функции 128x128 до 256 (см. Конец этого вопроса перед обновлением для кода, если вам это интересно).
Теперь я хочу сделать 256-битное дополнение, и я не нашел способ заставить компилятор использовать ADC
, кроме как с помощью сборки. Я мог бы использовать ассемблер, но я хочу встроенные функции для повышения эффективности. Компилятор уже создает эффективную функцию от 128x128 до 256 (по причине, которую я объяснял в начале этого вопроса), поэтому я не понимаю, почему я должен переписать это в сборке (или любые другие функции, которые компилятор уже реализует эффективно).
Вот встроенная функция сборки, с которой я пришел:
#define ADD256(X1, X2, X3, X4, Y1, Y2, Y3, Y4) \
__asm__ __volatile__ ( \
"addq %[v1], %[u1] \n" \
"adcq %[v2], %[u2] \n" \
"adcq %[v3], %[u3] \n" \
"adcq %[v4], %[u4] \n" \
: [u1] "+&r" (X1), [u2] "+&r" (X2), [u3] "+&r" (X3), [u4] "+&r" (X4) \
: [v1] "r" (Y1), [v2] "r" (Y2), [v3] "r" (Y3), [v4] "r" (Y4))
(вероятно, не каждый выход нуждается в модификаторе раннего clobber но я получаю неправильный результат, по крайней мере, за последние два)
И вот функция, которая делает то же самое в C
void add256(int256 *x, int256 *y) {
uint64_t t1, t2;
t1 = x->x1; x->x1 += y->x1;
t2 = x->x2; x->x2 += y->x2 + ((x->x1) < t1);
t1 = x->x3; x->x3 += y->x3 + ((x->x2) < t2);
x->x4 += y->x4 + ((x->x3) < t1);
}
Зачем нужна сборка? Почему компилятор не компилирует функцию add256
для использования флагов переноса? Есть ли способ заставить компилятор сделать это (например, могу ли я изменить add256
, чтобы он это сделал)? Что делать для компилятора, который не поддерживает встроенную сборку (написать все функции в сборке?) Почему для этого не существует?
Вот функция 128x128 до 256
void muldwu128(int256 *w, uint128 u, uint128 v) {
uint128 t;
uint64_t u0, u1, v0, v1, k, w1, w2, w3;
u0 = u >> 64L;
u1 = u;
v0 = v >> 64L;
v1 = v;
t = (uint128)u1*v1;
w3 = t;
k = t >> 64L;
t = (uint128)u0*v1 + k;
w2 = t;
w1 = t >> 64L;
t = (uint128)u1*v0 + w2;
k = t >> 64L;
w->hi = (uint128)u0*v0 + w1 + k;
w->lo = (t << 64L) + w3;
}
Определяет тип:
typedef __int128 int128;
typedef unsigned __int128 uint128;
typedef union {
struct {
uint64_t x1;
uint64_t x2;
int64_t x3;
int64_t x4;
};
struct {
uint128 lo;
int128 hi;
};
} int256;
Update:
Мой вопрос в основном дублирует эти вопросы:
- get-gcc-to-use-carry-logic-for-произвольная-точность-арифметика-без-встроенная-сборка
- efficient-128-bit-addition-using-carry-flag
- multiword-addition-in-c.
У Intel есть хорошая статья (Новая инструкция по поддержке большой целочисленной арифметики), в которой обсуждается большая целочисленная арифметика и три новые инструкции MULX, ADCX, ADOX, Они пишут:
внутренние определения мульки, adcx и adox также будут интегрированы в компиляторы. Это первый пример инструкции типа "добавить с переносом", реализуемой с помощью. встроенные функции Внутренняя поддержка позволит пользователям реализовать большие целочисленная арифметика с использованием языков программирования более высокого уровня, таких как C/С++.
Внутренние функции
unsigned __int64 umul128(unsigned __int64 a, unsigned __int64 b, unsigned __int64 * hi);
unsigned char _addcarry_u64(unsigned char c_in, unsigned __int64 a, unsigned __int64 b, unsigned __int64 *out);
unsigned char _addcarryx_u64(unsigned char c_in, unsigned __int64 a, unsigned __int64 b, unsigned __int64 *out);
Кстати, MSVC уже имеет _umul128
собственный. Таким образом, хотя MSVC не имеет __int128
, встроенный _umul128
может быть использован для генерации mul
и, следовательно, умножения на 128 бит.
Инструктаж MULX
доступен с BMI2 в Хасуэлле. Для процессоров Broadwell доступны инструкции ADCX
и ADOX
. Слишком плохо, что для ADC
, который был доступен с 8086 в 1979 году, не существует. Это решит проблему встроенной сборки.
Изменить: на самом деле __int128
будет использовать MULX
, если BMI2 определен (например, с помощью -mbmi2
или - march=haswell
).
Edit:
Я попробовал добавить Clang с несущими носителями, как предложено Lưu Vĩnh Phúc
void add256(int256 *x, int256 *y) {
unsigned long long carryin=0, carryout;
x->x1 = __builtin_addcll(x->x1, y->x1, carryin, &carryout); carryin = carryout;
x->x2 = __builtin_addcll(x->x2, y->x2, carryin, &carryout); carryin = carryout;
x->x3 = __builtin_addcll(x->x3, y->x3, carryin, &carryout); carryin = carryout;
x->x4 = __builtin_addcll(x->x4, y->x4, carryin, &carryout);
}
но это не сгенерировано ADC
, и это сложнее, чем я ожидаю.