GCC has 128-bit integers. Using these I can get the compiler to use the mul
(or imul
with only one operand) instructions. For example
uint64_t x,y;
unsigned __in128 z = (unsigned __int128)x*y;
produces mul
. I have used this to create a 128x128 to 256 function (see the end of this question, before the update, for code for that if you're interested).
Now I want to do 256-bit addition and I have not found a way to get the compiler to use ADC
except by using assembly. I could use an assembler but I want inline functions for efficiency. The compiler already produces an efficient 128x128 to 256 function (for the reason I explained at the start of this question) so I don't see why I should rewrite this in assembly as well (or any other functions which the compiler already implements efficiently).
Here is the inline assembly function I have come up with:
#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))
(probably not every output needs a early clobber modifier but I get the wrong result without at least the last two)
And here is a function which does the same thing in 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);
}
Why is assembly necessary for this? Why can't the compiler compile the add256
function to use the carry flags? Is there a way to coerce the compiler to do this (e.g. can I change add256
so that it does this)? What is someone suppose to do for a compiler which does not support inline assembly (write all the functions in assembly?) Why are there no intrinsic for this?
Here is the 128x128 to 256 function
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;
}
Some type defines:
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:
My question is largely a duplicate of these questions:
- get-gcc-to-use-carry-logic-for-arbitrary-precision-arithmetic-without-inline-assembly
- efficient-128-bit-addition-using-carry-flag
- multiword-addition-in-c.
Intel has a good article (New Instructions Support Large Integer Arithmetic) which discusses large integer arithmetic and the three new instructions MULX, ADCX, ADOX. They write:
intrinsic definitions of mulx, adcx and adox will also be integrated into compilers. This is the first example of an “add with carry” type instruction being implemented with intrinsics. The intrinsic support will enable users to implement large integer arithmetic using higher level programming languages such as C/C++.
The intrinsics are
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);
Incidentally, MSVC already has a _umul128
intrinsic. So even though MSVC does not have __int128
the _umul128
intrinsic can be used to generate mul
and therefore 128 bit multiplication.
The MULX
instruciton is available since BMI2 in Haswell. The ADCX
and ADOX
instructions are available for Broadwell processors. It's too bad there is no intrinsic for ADC
which has been available since the 8086 in 1979. That would solve the inline assembly problem.
Edit: actually __int128
will use mulx
if BMI2 is defined (e.g. using -mbmi2
or -march=haswell
).
Edit:
I tried the Clang's add with carry builtins as suggested by 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);
}
but this does not generated ADC
and it's more complicated than I expect.