
The new Clang _ExtInt feature provides exact bitwidth integer types - daurnimator
http://blog.llvm.org/2020/04/the-new-clang-extint-feature-provides.html
======
segfaultbuserr
I think it's funny. C was originally invented in an era when machines didn't
have a standard integer size, 36-bit architectures were at their heydays, so C
integers - char, short, int, and long - only have a guaranteed minimum size
that could be taken for granted, but nothing else, to achieve portability. But
after the computers of world have converged to multiple-of-8-bit integers, the
inability to specify a particular size of an integer become an issue. As a
result, in modern C programming, the standard is to use uint8_t, uint16_t,
uint32_t, etc., defined in <stdint.h>, C's inherent support of different
integer sizes are basically abandoned - no one needs it anymore, and it only
creates confusions in practice, especially in the bitmasking and bitshifting
world of low-level programming. Now, if N-bit integers are introduced to C,
it's kind of a negation-of-the-negation, and we complete a full cycle - the
ability to work on non-multiple-of-8-bit integers will come back (although the
original integer-size independence and portability will not come back).

~~~
m463
Why not just go to values?

Some languages like Ada allow a type that say goes from -273 to 600.

~~~
weinzierl
Pascal has it too. I always found it quite natural to specify the range I want
with the data type and not as precondition in the function. Another big
advantage is that you can avoid a good deal of potential off-by-one errors, if
you define your data types appropriately. For example the following definition
in Pascal would be much less error prone than the corresponding definition in
C[1]:

    
    
        var
        weekday:  0 ... 6;
        monthday: 1 ... 31;
    
    

For a quarter of a century I wonder why no one seems to miss that feature. I
really hope we will get them in C one day. Even more so I hope that the
proposals for refinement types in Rust[2] will one day be resolved and become
implemented.

[1]
[https://www.cplusplus.com/reference/ctime/tm/](https://www.cplusplus.com/reference/ctime/tm/)

[2] [https://github.com/rust-lang/rfcs/issues/671](https://github.com/rust-
lang/rfcs/issues/671)

------
Traster
I'd love to know if there's any use to this beyond FPGAs. This just seems to
be another case of porting the complexity of RTL design into C syntax so that
they can claim they have an HLS product that compiles C to gates. It's not C
to gates if you had to rewrite all your C to manually specific the bit widths
of every single signal. I really wonder how far we can keep going before the
naming police break into Intel Headquarters and rename all their marketing
material with "Low Level Synthesis".

~~~
zamadatix
Network protocols immediately came to mind for me. They love packed structs of
weird bit sizes because anything that's not payload is overhead on every
message for the rest of time. Fields can also be large, e.g. VLANs are 12
bits, VXLAN ids are 24 bits, MAC addresses are 48 bits, IPv6 addresses are 128
bits so it's not just limited to a couple of small sized bitflag style things.

~~~
Someone
For packed structures, C already has bit fields. Example (from
[https://en.cppreference.com/w/cpp/language/bit_field](https://en.cppreference.com/w/cpp/language/bit_field)):

    
    
      struct S {
        // will usually occupy 2 bytes:
        // 3 bits: value of b1
        // 2 bits: unused
        // 6 bits: value of b2
        // 2 bits: value of b3
        // 3 bits: unused
        unsigned char b1 : 3, : 2, b2 : 6, b3 : 2;
      };
    

This is more aimed at large integers.

~~~
gray_-_wolf
Kinda important part mentioned in the link is

> Adjacent bit field members may be packed to share and straddle the
> individual bytes.

So it might not be packed. Ofc it usually is, at least on gcc and such.

~~~
hermitdev
I think the bigger issue in regards to networking is ordering, not the (lack
of) packing. I don't think the ordering is guaranteed and probably depends on
CPU architecture (e.g. big vs little endian), not necessarily compiler.

~~~
gdwatson
In practice, little-endian machines have the first bit field in the least-
significant bits of the data type they fit into (usually but not always an
int), and big-endian machines have the first bit field in the most-significant
bits.

------
beefhash
Note that the spec[1] requires that this tops at an implementation-defined
size of integers, so you're likely not getting out of writing bignum code
yourself (and even fifimplemented, the bignum operations may likely be
variable-time and thus unsuitable for any kind of cryptography). Making the
size completely implementation-defined also sounds like it'll be unreliable in
practice, I feel like making it at least match the bit size of int would be a
worthwhile trade-off between predictability for the programmer aiming for
portability and simplicity of implementation.

[1] [http://www.open-
std.org/jtc1/sc22/wg14/www/docs/n2472.pdf](http://www.open-
std.org/jtc1/sc22/wg14/www/docs/n2472.pdf)

~~~
wongarsu
Both Clang and gcc already support a 128bit integer type, so it's certainly
possible that "implementation-defined" will end up being 128-bit or 256-bit
for x64 targets on common compilers (provided MSVC plays along).

~~~
TazeTSchnitzel
LLVM already supports completely arbitrary integer sizes up to 2^23-1 in its
IR ([https://llvm.org/docs/LangRef.html#integer-
type](https://llvm.org/docs/LangRef.html#integer-type)), and I think it can
“lower” any integer size to fit what the hardware actually supports. So if
Clang doesn't add an artificial constraint on top, in theory you could use a
one million-bit integer size if you wanted to?

------
0xTJ
I'm very much in support of this. One thing I like about Zig[1] s that
integers are explicitly given sizes. I've been playing recently with it, but
I'm waiting for a specific "TODO" in the compiler to be fixed.

[1] [https://ziglang.org/](https://ziglang.org/)

------
SlowRobotAhead
> These tools take C or C++ code and produce a transistor layout to be used by
> the FPGA.

Hmm, I haven’t been following that but it seems that...

> The result is massively larger FPGA/HLS programs than the programmer needed

And there it is.

Really seems odd to me to try and force procedural C into non-linear execution
of FPGA. Like it seems super odd, and when talking about changes to C to help
that... I really don’t get it.

This isn’t what C is for. What is the performance advantage over Verilog? How
many people want n-bit into in C when automatically handled structures work
well for most people.

Maybe I’m just not seeing the bigger picture here and that example was just
poor?

~~~
marcan_42
There is no performance advantage over Verilog. The reason why (some) people
want C is because Verilog and VHDL are unquestionably _terrible_ languages
(they weren't even intended to be HDLs, that use case came later) which are
damn near impossible to write complex systems in without spending most of your
time writing bug-prone boilerplate. Every big name IC designer shop ends up
wrapping them in layers of metaprogramming to make them palatable.

So, since those languages suck, people familiar with the procedural side of
things end up asking for C. Which is an even bigger impedance mismatch than
Verilog, but since you need a smarter backend to even begin to implement it,
it can make life easier by that alone.

Personally, I prefer stuff like nMigen, which is basically Python
metaprogramming a synthesis-oriented subset of Verilog constructs. Compiles
down to Verilog behind the scenes.

~~~
krupan
"Verilog and VHDL are unquestionably terrible languages (they weren't even
intended to be HDLs, that use case came later)"

They were intended to be HDLs (for simulation of hardware), but they were
never intended to be automatically translated into gates/schematics (i.e.,
synthesized)

------
waltpad
First of all, I suppose that it will be possible to make them unsigned (just
like for standard types). Is this correct?

Also, what's the relationship between standard types and the new _ExtInts? Are
_ExtInt(16) equivalent to shorts, or are they considered distinct and require
explicit cast?

> In order to be consistent with the C Language, expressions that include a
> standard type will still follow integral promotion and conversion rules. All
> types smaller than int will be promoted, and the operation will then happen
> at the largest type. This can be surprising in the case where you add a
> short and an _ExtInt(15), where the result will be int. However, this ends
> up being the most consistent with the C language specification.

For instance, what if I choose to replace short by _ExtInt(16) in the above?
What would be the promotion rule then?

Note that it was already possible to implement arbitrary sized ints for a size
<= 64, by using bitfields (although it's possible that you could fall into UB
territory in some situations, I've never used that to do modular arithmetic).

Edit: Ah, there's this notion of underlying type: one may use the nearest
upper type to implement a given size, but nothing prevents to use a larger
type, for instance:

struct short3_s { short value:3; };

struct longlong3_s { long long value:3; };

I don't know what the C standard says about that, but clearly these two types
are not identical (sizeof will probably gives different results). What's will
it be for _ExtInt? How these types will be converted?

Another idea:

what about

struct extint13_3_s {

    
    
      _ExtInt(13) value:3;
    

};

Will the above be possible? In other words, will it be possible to combine
bitfields with this new feature?

I guess it's a much more complicated problem that it appears to be at first.

------
jhj
Much of my time is spent writing Mentor Catapult HLS for ASIC designs these
days.

Every HLS vendor or language has their own, incompatible arbitrary bitwidth
integer type at present. SystemC sc_int is different from Xilinx Vivado ap_int
is different from Mentor Catapult ac_int is different from whatever Intel had
for their Altera FPGAs. It's a real mess.

I'm hoping this is another small step to slowly move the industry into a more
unified representation, or at least if LLVM support for this at the type level
could enable faster simulation of designs on CPU by improving the CPU code
that is emitted. What probably matters most for HLS though are the operations
which are performed on the types (static or dynamic bit slicing, etc).

~~~
aDfbrtVt
I'm in the same boat. After having played with all the other vendor libraries,
I think I like ac_datatypes the most. It's been really fast and the Catapult
is a pretty good engine. Can I ask what industry you're in? I'm in telecom.

~~~
jhj
I work for Facebook.

------
derefr
> While the spelling is undecided, we intend something like: 1234X would
> result in an integer literal with the value 1234 represented in an
> _ExtInt(11), which is the smallest type capable of storing this value.

That “smallest type capable of storing this value” is a disappointing
approach, IMHO. It’d be a lot more powerful to just be able to pass in bit
patterns (base-2 literals) and have the resulting type match the lexical width
of the literal. 0b0010X should have a bit-width of 4, not 2.

~~~
saagarjha
I wonder if the suffix could be Xn where n is an integer specifying the width.

~~~
nybble41
Would 0X12 then be a 12-bit integer with the value zero or a hexadecimal `int`
literal with a base-10 value of 18? Does this work for other bases (0X12X12)?

I'm not sure why they picked a letter which can already occur in integer
literals rather than one of the many unused letters. Given the focus on FPGAs
and HDL it's also worth noting that X is commonly used in binary or
hexadecimal constants in HDLs to denote undefined or "don't care" values,
which could lead to confusion. Rust integer literal syntax would be perfect
here (1234u11 or 1234i11) since it already includes the bit width and is
compatible with any base prefix.

------
dang
Speaking of C, if you missed last week's thread with C Committee members, it
was rather amazing:
[https://news.ycombinator.com/item?id=22865357](https://news.ycombinator.com/item?id=22865357).

Click 'More' at the bottom to page through it; it just keeps going.

------
waynecochran
At some point they need to branch off and not call it C anymore. C should stay
relatively small -- small enough that a competent programmer could write a
compiler and RTS for it.

~~~
weinzierl
Yes, keep C clean and add all the cruft to another language derived from C. We
could call the new language C++.

~~~
Gibbon1
My opinion is C and C++ need a divorce. So that C can be modernized with
features that make sense in the context of the language. And not constrained
as a broken subset of C++.

~~~
detaro
That has already happened though?

~~~
Gibbon1
I think it's starting to happen because C++ has become so grossly Byzantine. C
refuses to relinquish a bunch of niche applications. The heyday of OOP is
past. 10 years ago the attitude was C is going to die any day now. Now it more
like since C isn't dying it needs improvements. And none of them are backports
from C++ nor make sense in C++.

~~~
detaro
C99 already isn't a subset of C++ (although I think it took until C++14 to
finally give up on that), so any alignment is courtesy now, and it's
recognized they'll deviate where necessary.

------
nabla9
I think most commonly used languages with and without standards, C,C++,
JavaScript/Wasm, Python, Java, etc. should standardize new primitive type
represetations together (with hardware people included).

If you have different representations in different languages it just creates
unnecessary impedance mismatch. It would be better for everyone if you could
just pass these types from language to language.

~~~
einpoklum
C++ can have arbitrary-width integers as library types; it would not be that
big of a deal IMHO. If `optional`, `variant` and `any` (and maybe soon, `bit`)
are not in the language itself, no reason why n-bit-integer should be.

(Of course, this is written from the "we can jerry-rig the existing language
to do what you want" perspective with which so much is achievable efficiently
in C++.)

~~~
hermitdev
Boost Multiprecision [0] is an example of such a library type. It offers a
compile-time arbitrarily wide integers (with predefined types up to 1024 bits)
and a C++ wrapper around the GMP or MPIR libraries, which supports arbitrary
sizes at runtime (not sure how it's implemented, but probably on top of an
array of ints or BCD (binary-coded decimals)).

C++ has had `optional` and `variant` since (I think) C++11, maybe 14. I don't
think `any` made the cut. All of these types originated (for C++
standardization) in Boost, as well. I'd caution against using `any`, though.
From personal experience, the runtime overhead is quite high, and holding any
non-none type is a dynamic allocation. Performance is far better with
`variant` at the development cost of needing to know all the types you're
going to support at compile-time.

[0]
[https://www.boost.org/doc/libs/1_72_0/libs/multiprecision/do...](https://www.boost.org/doc/libs/1_72_0/libs/multiprecision/doc/html/index.html)

~~~
einpoklum
optional and variant are new in C++17, actually. They were experimental in 14.

But - in a sense, C++ still doesn't "have" them, since basically nothing in
the standard library relies on the availability of any of these.

(And - I also try to avoid std::any)

------
rurban
That's what I've wrote to their reddit post:

The feature is of course fantastic. But the syntax still looks bit overblown.

Type system-wise this seems to be more correct:

    
    
      _ExtInt(a) + _ExtInt(b) => _ExtInt(MAX(a, b) +1)
    

And int + _ExtInt(15) might need a pragma or warning flag to warn about that
promotion. One little int, or automatic int pollutes all.

~~~
Traster
Problem is:

_ExtInt(16) + _ExtInt(15) => ExtInt(17)

_ExtInt(17) + _ExtInt(15) => ExtInt(18)

So let's say we have a,b and c. a is 16 bits, b and c are 14bits.

a + (b + c) => ExtInt(17) (a + b) + c => ExtInt(18)

Now obviously this a trivial example, but it highlights the fact that unless
you're actually willing to carry the true ranges around in your type system,
your calculation of bit widths are going to vary due to the details of which
operations are done in which order with which intermediary variables.

~~~
rurban
I see, it's now the same problem as with floats. addition is not commutative
anymore. But isn't that the same problem in HW also? With the right order you
are saving transistors.

~~~
Traster
Well, it's not quite a problem with whether the operations are commutative -
both of those phrasings of the problem will give the correct answer in
hardware 100% of the time. The only difference is one made an efficient
decision about the order of operations with knowledge of how bit growth rules
work and the ranges of the inputs.

You do have the same problem in hardware, which is hardware designers job. The
difference RTLs don't claim to be high level languages. This is an instance
where there's a high level intention and a low level implementation, and the
high level synthesis tool has just ported new language constructs into the
high level language to do low level optimisation, rather than actually doing
the synthesis optimisations that are expected.

------
fortran77
I love Erlang for the ability to deal with _bits_. To see this in a compiled
language would be wonderful. Of course, you can get down to the bit level with
bitwise logical operations, but to be able to express it more naturally would
be a great boon to people writing low-level network stuff, and will probably
reduce programming errors.

------
ndesaulniers
Congrats Erich! One thing I'd be curious about is the ergonomics (or lack of)
of _explicit_ integer promotions and conversions for these types, as I find
the current rules for _implicit_ integer promotions a little confusing and
hard to remember.

For a fun compiler bug in LLVM due to representation of arbitrary width
integers, see: [https://nickdesaulniers.github.io/blog/2020/04/06/off-by-
two...](https://nickdesaulniers.github.io/blog/2020/04/06/off-by-two/)

------
Someone
_”Likewise, if a Binary expression involves operands which are both _ExtInt,
rather than promoting both operands to int the narrower operand will be
promoted to match the size of the wider operand, and the result of the binary
operation is the wider type.“_

I don’t understand that choice. The result should be of the wider type, yes,
but, for example, multiplying a __ExtInt(1)_ by a __ExtInt(1000)_ should take
less hardware than multiplying two _ExtInt(1000)_ s. So, why promote the
narrower one to the wider type?

------
saagarjha
I wonder if this could help standardize some vectorized code as well.

------
ralusek
A lot of people don't know this, but `BigInt`s are supported in modern
JavaScript; integers of arbitrarily large precision.

Try in your browser console:

    
    
        2n ** 4096n
    
        // output (might have to scroll right)
        1044388881413152506691752710716624382579964249047383780384233483283953907971557456848826811934997558340890106714439262837987573438185793607263236087851365277945956976543709998340361590134383718314428070011855946226376318839397712745672334684344586617496807908705803704071284048740118609114467977783598029006686938976881787785946905630190260940599579453432823469303026696443059025015972399867714215541693835559885291486318237914434496734087811872639496475100189041349008417061675093668333850551032972088269550769983616369411933015213796825837188091833656751221318492846368125550225998300412344784862595674492194617023806505913245610825731835380087608622102834270197698202313169017678006675195485079921636419370285375124784014907159135459982790513399611551794271106831134090584272884279791554849782954323534517065223269061394905987693002122963395687782878948440616007412945674919823050571642377154816321380631045902916136926708342856440730447899971901781465763473223850267253059899795996090799469201774624817718449867455659250178329070473119433165550807568221846571746373296884912819520317457002440926616910874148385078411929804522981857338977648103126085903001302413467189726673216491511131602920781738033436090243804708340403154190336n
    

To use, just add `n` after the number as literal notation, or can cast any
Number x with BigInt(x). BigInts may only do operations with other BigInts, so
make sure to cast any Numbers where applicable.

I know this is about C, I thought I'd just mention it, since many people seem
to be unaware of this.

~~~
justicz
Hm, does this work in Safari?
[https://caniuse.com/#feat=bigint](https://caniuse.com/#feat=bigint)

~~~
ralusek
Not yet, but I believe babel and others just transpile/polyfill it by having
it fall back on a string arithmetic library for working with integers of
arbitrary precision.

~~~
recursive
That will never be totally reliable as 1) javascript is dynamically typed 2)
javascript doesn't support operator overloading. Nonetheless, there are
attempts.

[https://www.npmjs.com/package/babel-plugin-transform-
bigint](https://www.npmjs.com/package/babel-plugin-transform-bigint)

> Update: Now it can convert a code using BigInt into a code using JSBI
> ([https://github.com/GoogleChromeLabs/jsbi](https://github.com/GoogleChromeLabs/jsbi)).
> It will try to detect when an operator is used for bigints, not numbers.
> _This will not work in many cases, so please use JSBI directly only if you
> know, that the code works only with bigints._

(emphasis mine)

------
SloopJon
C++ has sped up the pace of its releases, but I don't have a sense of where C
is. I didn't realize until I looked it up just now that there's a C18,
although I gather that this is even smaller a change than C95 was.

Safe to say that a feature like this would be standardized by 2022 at the
earliest?

~~~
GTP
I just found out about C18 thanks to your comment, I was still at C11. Thanks.
Anyway I think you're right, except for the fact that I don't like when
language designers release versions too quickly. I don't know the situation in
the C++ land, but as an example I think that Java took the wrong way.

~~~
jcelerier
> but as an example I think that Java took the wrong way.

I wonder what is the right way then ? Java is apparently too fast for you, and
yet it gets improvements so slowly that it is getting its marketshare eaten by
other JVM languages moving much faster.

If it was even slower it could as well be put directly next to the dusty COBOL
and RPG boxes in the IBM attic.

~~~
GTP
I think that language designers should release a new version when they
actually have something new and not because somebody decided that there has to
be a new version every x months. Also I think that the popularity of other JVM
languages, eg. Kotlin, derives not from their quick releases, but from the
fact that they observed what programmers didn't like about Java and designed
the language from the start keeping in mind those problems and addreesing them
right from the beginning.

------
drfuchs
So, if I have an array of extint(3), does it pack them nicely into
10-per-32-bit-word? Or 21-per-64-bit-word? Will a struct with six extint(5)
fields fit into 4 bytes? What about just a few global variables of extint(1)?
Will they get packed into a single byte? Did I miss where this is covered?

~~~
tom_mellior
[http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2472.pdf](http://www.open-
std.org/jtc1/sc22/wg14/www/docs/n2472.pdf) does not mention structs at all,
which is disappointing.

I quoted this language below: "_ExtInt types are bit-aligned to the next
greatest power-of-2 up to 64 bits: the bit alignment A is min(64, next power-
of-2(>=N)). The size of these types is the smallest multiple of the alignment
greater than or equal to N. Formally, let M be the smallest integer such that
A * M >= N. The size of these types for the purposes of layout and sizeof is
the number of bits aligned to this calculated alignment, A * M. This permits
the use of these types in allocated arrays using the common
sizeof(Array)/sizeof(ElementType) pattern."

But to be honest I don't understand what it's trying to say. If bit width N =
3, the next power of 2 is 4, so would that mean that "bit alignment(?)" A = 4?
Then M = 1 is the smallest integer such that A * M >= 3. Then the size of the
type would be 4 bits? That wouldn't fly with sizeof.

------
senozhatsky
Knuth's MIX computer with its 6-bit bytes and 5-byte words (IIRC) came to my
mind [0]

[0] [https://en.wikipedia.org/wiki/MIX](https://en.wikipedia.org/wiki/MIX)

------
pjmlp
Currently clang is getting it, if ISO C gets it, it is another matter.

------
mshockwave
the title is a little misleading. Since _ExtInst is just an extension of Clang
not a standard. GCC and Clang both have some hidden features that are not in
standard.

~~~
mappu
It is on the standards track, though, even if N2472 was not completely
accepted it seems like there is a process for this (or something very much
like it) to become a standard.

------
rightbyte
I feel this better be compiler extensions. Writing FPGA code has so much
specialness anyway.

------
xyzzy2020
How does this not break sizeof ?

~~~
saagarjha
It’d probably round up to the nearest byte, as it already does with the
boolean types.

~~~
barbegal
Where does the spec say that it does that? As far as I can tell C only allows
objects to have sizes in whole number of bytes, and that includes booleans.

Although a _Bool type can be used for a bit field (having size of 1 bit) but
you can't use sizeof with a bit field.

~~~
monocasa
Yeah, the unit of sizeof is number of chars, which is usually a byte.

~~~
_kst_
In C and C++, a char is by definition a byte.

A byte is CHAR_BIT bits, where CHAR_BIT is required to be at least 8 (and is
exactly 8 for the vast majority of implementations).

The word "byte" is commonly used to mean exactly 8 bits, but C and C++ don't
define it that way. If you want to refer to exactly 8 bits without ambiguity,
that's an "octet".

~~~
hermitdev
I think you worded this pretty well. One thing I'd add (and that annoys me
about C & C++) is that the size guarantees for integral types boil down to is
that CHAR_BIT = sizeof(char) and that sizeof(char) <= sizeof(short) <=
sizeof(int) <= sizeof(long). sizeof(T*) (for any T) is not even defined well,
and can be OS/compiler specific. Makes cross-platform 32/64-bit support
painful, especially because there were no strictly sized integer types before
C11 & C++11. Although C11 & C++11 define types like int32_t and int64_t,
they're not actually required to be those sizes! The various x-bit types only
have to at least be large enough to store x-bits. So, on a hypothetical 40-bit
CPU, sizeof(int32_t) could vary well be 40-bits, if that's the natural "word"
size for the CPU.

The devil is always in the details, and the devil is very, very annoying...

------
detaro
What was wrong with the actual title?

> _The New Clang _ExtInt Feature Provides Exact Bitwidth Integer Types_

~~~
moonchild
It implements a proposed feature to the c language, which is the more
interesting part of it.

~~~
pjmlp
Not all proposed features get accepted, specially in how conservative WG 14
tends to be.

~~~
hermitdev
Absolutely a true statement, but it should also be noted that WG 14 tends to
be more accepting of proposals that have working extension(s) in a major
compiler.

~~~
pjmlp
Well, that wasn't enough to rescue Annex K, nor to have a way to solve the
impending adoption issues.

~~~
wahern
Which vendor fully implemented Annex K? For several years _after_ C11 was
published no vendor fully implemented Annex K, not even the sponsor,
Microsoft. I haven't checked in awhile so maybe things have changed.

~~~
pjmlp
Well just like here, what clang is implementing with _ExtInt doesn't mean it
is what N2472 will look like in the end.

Just like Annex K turned out to look a bit different than what Microsoft
sponsored.

Which are still available to Windows developers.

[https://docs.microsoft.com/en-us/cpp/c-runtime-
library/secur...](https://docs.microsoft.com/en-us/cpp/c-runtime-
library/security-enhanced-versions-of-crt-functions?view=vs-2019)

