On Wed, Jul 29, 2026 at 12:03:22AM -0400, Michael Meissner wrote:
> I need to look into this more, but given TImode integer support is more
> limited in the PowerPC, I wonder if maybe we should use DImode for
> limbs on 64-bit systems (and possibly SImode if we do BitInt for the
> older 32-bit systems).
No 64-bit target has a great support for TImode or 32-bit target for DImode,
in both cases those are emulated with some limited hardware support at
times, some operations are done through libgcc libcalls, others through
double-world operations, some emulated in vector instructions, ...
That is why GCC differentiates between what limb should be used for
ABI purposes (info->abi_limb_mode), so what scalar mode is used to determine
the size and alignment of the larger _BitInt(N) N > 128 (for small _BitInt(N),
the psABIs typically treat it if N <= 8 as QImode, if N <= 16 as HImode,
if N <= 32 as SImode and then vary for bits until N <= 128), and
another limb size used internally and in libgcc APIs (info->limb_mode).
Currently, the only 2 supported cases are that the modes are always the same
(what is used e.g. on x86_64/i686 or s390x) or the same for some of the
really small cases and abi_limb_mode being twice the size of limb_mode for
the larger ones.
So, the decision which mode to use in the psABI doesn't need to take into
account whether efficient instructions exist to implement all the needed
operations (at least as long as you don't choose something larger than
twice the size of what can be efficiently done).
The question is about size/alignment. E.g. on x86_64
for N in [1, 8]:
sizeof(char) == alignof(char) == sizeof(_BitInt(N)) == alignof(_BitInt(N)) == 1
for N in [9, 16]:
sizeof(short) == alignof(short) == sizeof(_BitInt(N)) == alignof(_BitInt(N)) ==
2
for N in [17, 32]:
sizeof(int) == alignof(int) == sizeof(_BitInt(N)) == alignof(_BitInt(N)) == 4
for N in [33, 64]:
sizeof(long) == alignof(long) == sizeof(_BitInt(N)) == alignof(_BitInt(N)) == 8
sizeof(__int128) == alignof(__int128) == 16
for N in [65, 65535]:
sizeof(_BitInt(N)) == (N + 63) / 64 * 8
alignof(_BitInt(N)) == 8
E.g. on aarch64 it is the same, except:
for N in [65, 65535]:
sizeof(_BitInt(N)) == (N + 127) / 128 * 16
alignof(_BitInt(N)) == 16
In all cases the larger _BitInt (for x86_64 for N >= 65, for aarch64 for
N >= 129) are passed/returned as if it is
struct { unsigned long bitint[(N + 63) / 64]; }
for x86-64 and
struct { unsigned __int128 bitint[(N + 127) / 128]; }
for aarch64.
Now, under the hood both of the targets handle the really large _BitInt
using loops or straight line code over 64-bit limbs because 64-bit
operations are efficient on the target.
The ugly thing about the x86_64 (or s390x) decision that some people
don't like is that we have the __int128 type and that passing of
_BitInt(N) for N in [65, 128] may differ from __int128 (on x86_64
actually it doesn't), and more importantly that
alignof (__int128) != alignof (_BitInt(128)).
The ugly thing about the aarch64 (and arm, loongarch, riscv) is
that it wastes for some values of N more storage, so e.g.
sizeof (_BitInt(513)) on x86_64 is 72, but on aarch64 is 80.
That is one choice the psABI needs to be made (and one problematic
point which is solvable by 1-2 days of work in the generic code hopefully
is that we don't support big-endian abi_limb_mode != limb_mode right now:
for little endian padding bits are at highest addresses, so if we
treat the _BitInt as a sequence of say 64-bit limbs even when the psABI
says 128-bit limbs, worst case we have one full 64-bit limb of padding bits
at the highest address and depending on the choice below either don't
touch that limb at all or do some extra extension into it; but on big-endian
the extra full padding limb comes first and all of sudden all accesses have
to be biased by extra 8 bytes).
Another important choice is what to do with padding bits which are
present in most of the _BitInts. One choice is say the padding bits
are undefined, this in theory requires less care during storing of values
and more care during reading of them (one has to sign or zero extend them
to full (internal) limb). Another choice is to say it must be always
sign-extended (for signed _BitInt) or zero-extended (for unsigned _BitInt),
extra effort during storing (needs to extend there), less effort during
reading (in some cases it can avoid the extension). There is a third
choice used for loongarch, don't repeat that, that is just weird.
Now, the GCC implementation of either of the strategy isn't perfect yet,
in some cases we extend in both cases, some of it could be improved
over time. One limitation is that RTL generally for the really small
sizes (single limb _BitInt) expects extension before operation rather
than after it, so for those with undefined padding bits we can emit
unnecessarily extension both before and after operations. And also
when emitting straight line code or loops (padding in that case is
typically used outside of those loops just for one limb) some extension
can happen unnecessarily too. But the extended case does have to
add extra extension not present on the undefined padding bits targets,
especially for the case of internal limbs full of padding bits (and that
again is right now supported only for little-endian but can in theory
be adjusted also for big endian).
And endianity, what ordering of limbs is used for _BitInt using more
than one psABI limb. Currently GCC doesn't really support endianity
of the limb ordering being different from endianity of the target
and some choices (different abi_limb_mode from mode and different endianity
from target endianity at the same time) are impossible to support.
So, the above are the choices a target needs to make (plus decide whether
passing as a struct mentioned above is best for _BitInt or something
different). And I'm arguing that the choice should be made primarily
based on what you think is best for the architecture in question and not
based on current GCC limitations, the psABI will apply to other compilers
too. But sure, some choices will be harder to implement than others.
I'd strongly prefer if you don't choose something really strange, e.g.
using little-endian ordering of limbs on big-endian target or big-endian
ordering of limbs on little-endian target, that both requires extra effort
to support and ressurrect PDP-endianish stuff. Similarly, going for
absurd sizes, say for N >= 2048 go with
sizeof(_BitInt(N)) == (N + 2047) / 2048 * 256
alignof(_BitInt(N)) == 256
etc.
Under the hood, most _BitInt operations aren't really vectorizable. There
is a small subset of them which are, moves, bitwise and/or/xor/not, and
that is about it, even basic ops like addition, subtraction, comparison
(especially non-equality one), etc. are hardly vectorizable. And
some operations are done through libgcc.a libcalls only (multiplication,
division/modulo, conversions to/from binary and decimal floating point),
because it would require too much code.
The primary question is, how much do I care about
alignof (_BitInt(128)) == alignof (__int128)
and is it so important that I want to pay extra 8 bytes for many of those?
Obviously for 32-bit targets it is more about
alignof (_BitInt(64)) == alignof (long long)
and extra 4 bytes for many.
Jakub