On Mon, Jul 06, 2026 at 07:23:00PM +0200, Jakub Jelinek wrote:
> Hardcoding some limb type to some _BitInt limb type (but which one,
> on many architectures we have two, the ABI one which influences
> size/alignment decision and another one, often smaller width
> but sometimes the same, which is used in most of the operations)
> would simplify it a little bit, but would be a nightmare for C library
> writers or users which want to write similar APIs in their libraries.
> As I wrote, at least for the libc built with older compiler which doesn't
> support this (which is something that needs to be supported for at least
> a few years), my plan was that one would compile these intrinsics/macros
> into assembly and use in the library. But the library better should be
> able to choose what limb it wants to use and use it on all architectures,
> rather than dealing with on aarch64 I need 128-bit limb type, on x86_64
> 64-bit limb type, on i686 32-bit limb type, ...
>
> Now, one could hardcode some limb type for all users, say
> unsigned long, but I wasn't sure that is the right choice for all the
> C libraries and all users. I'd say the most usual choices will be likely
> unsinged long, unsigned int and unsigned long long, but already that that
> point the intrinsic needs to support all the cases the posted patch handles,
> namely when the user chosen limb has smaller width than the _BitInt internal
> limb type (but right now the _BitInt internal limb type width has to be
> divisible by the user chosen limb width in that case), or same width, or
> larger width (the posted patch requires double the width in that case).
> I intentionally chose not to support bit-precise integers in the
> type-generic macro, don't think splitting some _BitInt(n) for runtime n
> into an array of _BitInt(17) or array of _BitInt(925) is useful or
> worth the effort.
>
> Regarding endianity, sure, we could hardcode say little-endian ordering,
> or always match the architecture endianity, but that again makes the
> C library maintainance harder for little gain on the compiler side.
For endianity, not sure if we want _le and _be suffixed macros
or put the signed/unsigned into the names as well, or even the width
of the user chosen limb type.
The stdbit.h stdc_{load,store}8_{le,be}{u,s}N functions would suggest
everything should go into the names, but I'm not sure if we want to
export from the C library all those without them actually being needed.
They are not huge, but not small either. E.g. on x86_64
#include <stdarg.h>
void
foo (va_list *ap, int n, unsigned long *p)
{
__builtin_va_arg_bitint (*ap, n, p, false, false);
}
void
bar (va_list *ap, int n, unsigned int *p)
{
__builtin_va_arg_bitint (*ap, n, p, false, false);
}
void
baz (va_list *ap, int n, unsigned __int128 *p)
{
__builtin_va_arg_bitint (*ap, n, p, false, false);
}
void
qux (va_list *ap, int n, unsigned long *p)
{
__builtin_va_arg_bitint (*ap, n, p, true, true);
}
void
corge (va_list *ap, int n, unsigned int *p)
{
__builtin_va_arg_bitint (*ap, n, p, true, true);
}
void
garply (va_list *ap, int n, unsigned __int128 *p)
{
__builtin_va_arg_bitint (*ap, n, p, true, true);
}
is at -O2 with the patch
3: 0000000000000000 668 FUNC GLOBAL DEFAULT 1 foo
4: 000000000000029c 738 FUNC GLOBAL DEFAULT 1 bar
5: 000000000000057e 888 FUNC GLOBAL DEFAULT 1 baz
6: 00000000000008f6 667 FUNC GLOBAL DEFAULT 1 qux
7: 0000000000000b91 767 FUNC GLOBAL DEFAULT 1 corge
8: 0000000000000e90 863 FUNC GLOBAL DEFAULT 1 garply
Note, the builtin functions actually can handle both the
endian and uns arguments as constants or runtime arguments,
in which case they emit slightly larger code but far smaller than
twice the size.
So, we could also go with just 3 type-generic macros where
both then endianity and signedness would be runtime arguments too.
I've also tried to compile what the __builtin_va_arg_bitint does
using just C2Y without extensions (sure, it could be optimized
to some extent, using multiple smaller temporaries for clusters
of n values, etc.) but I gave up trying to compile this after
15 minutes at -O2 (and consuming many gigabytes of compile
time memory).
#include <stdarg.h>
#include <stddef.h>
#include <limits.h>
void
foo (va_list *ap, size_t n, unsigned long *p, bool end, bool uns)
{
unsigned _BitInt(BITINT_MAXWIDTH) r = 0;
if (uns)
switch (n)
{
#define A(n) case n: r = va_arg (*ap, unsigned _BitInt(n)); break;
#define B(n) A(n##0) A(n##1) A(n##2) A(n##3) A(n##4) \
A(n##5) A(n##6) A(n##7) A(n##8) A(n##9)
#define C(n) B(n##0) B(n##1) B(n##2) B(n##3) B(n##4) \
B(n##5) B(n##6) B(n##7) B(n##8) B(n##9)
#define D(n) C(n##0) C(n##1) C(n##2) C(n##3) C(n##4) \
C(n##5) C(n##6) C(n##7) C(n##8) C(n##9)
#define E(n) D(n##0) D(n##1) D(n##2) D(n##3) D(n##4) \
D(n##5) D(n##6) D(n##7) D(n##8) D(n##9)
A(1) A(2) A(3) A(4) A(5) A(6) A(7) A(8) A(9)
B(1) B(2) B(3) B(4) B(5) B(6) B(7) B(8) B(9)
C(1) C(2) C(3) C(4) C(5) C(6) C(7) C(8) C(9)
D(1) D(2) D(3) D(4) D(5) D(6) D(7) D(8) D(9)
E(1) E(2) E(3) E(4) E(5)
D(60) D(61) D(62) D(63) D(64)
C(650) C(651) C(652) C(653) C(654)
B(6550) B(6551) B(6552)
A(65530) A(65531) A(65532) A(65533) A(65534) A(65535)
}
else
switch (n)
{
#undef A
#define A(n) case n: r = va_arg (*ap, _BitInt(n)); break;
A(1) A(2) A(3) A(4) A(5) A(6) A(7) A(8) A(9)
B(1) B(2) B(3) B(4) B(5) B(6) B(7) B(8) B(9)
C(1) C(2) C(3) C(4) C(5) C(6) C(7) C(8) C(9)
D(1) D(2) D(3) D(4) D(5) D(6) D(7) D(8) D(9)
E(1) E(2) E(3) E(4) E(5)
D(60) D(61) D(62) D(63) D(64)
C(650) C(651) C(652) C(653) C(654)
B(6550) B(6551) B(6552)
A(65530) A(65531) A(65532) A(65533) A(65534) A(65535)
}
r <<= (BITINT_MAXWIDTH - n);
if (uns)
r >>= (BITINT_MAXWIDTH - n);
else
r = ((_BitInt(BITINT_MAXWIDTH)) r) >> (BITINT_MAXWIDTH - n);
size_t w = (n + LONG_WIDTH - 1) / LONG_WIDTH;
if (end)
for (size_t i = 0; i < w; ++i)
p[w - 1 - i] = r >> (i * LONG_WIDTH);
else
for (size_t i = 0; i < w; ++i)
p[i] = r >> (i * LONG_WIDTH);
}
Jakub