12
$\begingroup$

In most statically typed languages, integers are offered in 8-bit types and increasing powers of 2. However, C has a new keyword _BitInt which as I understand allows the creation of integers with arbitrary bit widths. And Ada allows the programmer to specify the range of types, which implicitly defines the number of bits used.

A big reason for having fixed size power of 2 integer types is those are the native integer types on processors. But allowing the programmer to define the exact number of bits to use is more versatile and expressive. Are there any other advantages of having fixed power of 2 integers, and what does implementing a custom width integer take?

$\endgroup$
3
  • 1
    $\begingroup$ Think of HLS applications, where arbitrary bit width makes sense. $\endgroup$
    – SK-logic
    Jul 12, 2023 at 8:17
  • 3
    $\begingroup$ Related: Why don't many languages have integer range types? $\endgroup$
    – user23013
    Jul 12, 2023 at 8:54
  • 2
    $\begingroup$ This goes way back, PL/I and COBOL allow you to specify integer sizes. FIXED BIN(10) $\endgroup$
    – Barmar
    Jul 12, 2023 at 16:08

8 Answers 8

11
$\begingroup$

You need to distinguish between two meanings of “5-bit integer”:

  1. An integer which occupies 5 bits of storage, so that three of them will fit into a 16-bit word.
  2. An integer which can hold values between 0 and 31 (or -16 and +15).

1 is always going to be slow and cumbersome, a headache for the compiler and a bore for the CPU. Its positive qualities are that it saves memory if you have a million of these things, and that the CPU has a built-in instruction saying “copy three [or twelve] of these at once”. If you can guarantee “no overflow, ever”, then the CPU also has a built-in “copy three or twelve at once” instruction.

2 is easily achieved at no cost or complication. Assuming 16-bit words for ease of exposition, encode 0 as 0000, 1 as 0800, 2 as 1000, and so on up to 31 as F800. Then all arithmetic and comparison will work using the built-in instructions. Moreover, it is impossible to have a value outside the range 0 to 31.

$\endgroup$
7
  • 2
    $\begingroup$ I'd argue what you're talking about relates more to storage than the type itself. It would seem perfectly resonable to store 5-bit integers byte-aligned by default, just like bitfield structs are, but occasionally pack them tighter, like a bitset does $\endgroup$
    – abel1502
    Jul 12, 2023 at 8:03
  • 2
    $\begingroup$ As for "will always be slow", CPUs are not the only possible target of compilation. $\endgroup$
    – SK-logic
    Jul 12, 2023 at 8:20
  • $\begingroup$ "achieved at no cost" — a shift will be needed to convert the 32-bit result of a multiplication, and division will require a shift and a mask. $\endgroup$ Jul 12, 2023 at 14:01
  • 5
    $\begingroup$ did you mean "add three or twelve at once" in that second one? $\endgroup$
    – ilkkachu
    Jul 12, 2023 at 15:21
  • 3
    $\begingroup$ "a bore for the CPU". Is the robot uprising going to happen sooner if we don't keep giving them interesting things to do? $\endgroup$
    – Barmar
    Jul 12, 2023 at 16:09
4
$\begingroup$

Pro: Extracting bit fields

One advantage of ints with arbitrary sizes in bits is that non-power-of-two fields can be more easily extracted from blobs of binary data. E.g., a 6-byte piece of binary data could be represented as an i48, and if this represented a packet with two 4-bit fields and two 20-bit fields, this could easily be represented as (i4, i4, i20, i20).

Some languages might allow a simple cast from the former to the latter, while others might require unions or a library to do the translation, but regardless, having types for non-power-of-two integers allows representing these fields in a more precise way that avoids encoding invalid states in the way that, e.g., (i8, i8, i32, i32) would (even if this would have the same representation in memory).

$\endgroup$
1
  • 1
    $\begingroup$ I’ll note that these aren’t always handled well. Swift’s Unicode.Scalar type was changed from i21 to i32 very early on — before 1.0, iinm — because LLVM didn’t handle it very well. $\endgroup$
    – Bbrk24
    Jul 12, 2023 at 4:17
4
$\begingroup$

Pro (enumarate all widths)

  • implementing the compiler is more, but a lot easier work
  • the total number is usually small and reflects what hardware can do
  • C++-style bitfields are usually sufficient if needed at all; most code dealing with bits can be expressed with bit-wise operations adequately
  • Java is successful with essentially having only 32bit signed int (as of today, arrays are still restricted to int indices)
  • programmers may not understand that 21bit integers are still represented as 32bit integers because the hardware or memory layouting algorithm has no means to optimize out the dead space

Contra (allow all widths)

  • getting the compiler to translate the first tiny program is tricky, because you have to start with templates if you do it like Tyr and expose Integer as template or you need to expose the width as special syntax in your language which might be fine if you declare i\d+ an integer keyword.
  • if size is exposed as template, templates can abstract over size
  • LLVM can create code for such integers, so there is no real extra compiler implementation cost if you use an LLVM back-end anyway
  • code operating on uncommon bit-widths is a lot easier to read (note that such code is very uncommon in most domains)
$\endgroup$
3
$\begingroup$

Advantage

Packing data is possible, both to the compiler and the user. An extreme example of this are u1 ints, a.k.a, booleans (as one bit unsigned integer has only two possible representations).

So if your language is focused or has an option to optimize to size (code or memory), this can be relevant. You can possibly pack 8 sequential booleans in a octec, instead of 8 sequential int_fast_t (the performante choice) that in some machines will be wasteful as 8x64 = 512 bits.

Disadvantage 1: slow

It will be slow. To the point that the users complain about it.

Modern languages gives integers that are power of two, multiple of octets because modern CPUs have them. And because CPUs have then, they are fast. As in hardware fast. As in there is nothing more fast than this is possible fast. And users like that.

Disadvantage 2: trashed on multithread

You may encounter situations where your compiler (or someone compilers) do the packing sometimes, but sometimes not. The problem with packed data is that CPUs may don't have how to write on packed data in an atomic way. The CPU will need to load the whole word, bitfidle it to simulate the partial write, and write back the whole memory world.

So, sometimes an a.b = true; in one thread, a.c = false; in another, in a packed struct, will result in impossible to reason results, another name for undefined behaviour.

$\endgroup$
2
$\begingroup$

It simplifies the language and the compiler. The types offered are typically those that the CPU can directly work with using single instructions. Having those basic types available in the language means it is trivial to compile operations on those types to efficient machine code.

Most languages allow you to create custom types, so in most cases you can create a custom type for arbitrary wide integers. Thus, there is no need for the language to have explicit support for them. It also avoids the issue mentioned by Martin Kochanski, and leaves it up to you to decide how integers that don't exactly fit the built-in types behave and are stored.

$\endgroup$
2
$\begingroup$

TL;DR: Nothing prohibits a hybrid approach.

Having few choices has an obvious advantage in guiding selection. Those few pervade all the code, leading to re-use and better testing. And they can be chosen to be reasonably efficient in hardware.

Having free choice has the advantage of allowing you to choose exactly what you want. The biq question is whether it is also what you need, and whether being more deliberate would not be an advantage (exact vs. least vs. fast).

As an example, C (and C++) has a set of common sizes (with guaranteed minimal limits), and got a set of guaranteed sizes (using aliases), with exact, least, and fast variants.
C++ could add a template for selecting them generically. And it would not be that hard to synthesize bigger sizes as needed.

So, why not have a way to freely choose, preferably with aliases to request the fast/least option, and either add aliases for the common ones, or just recommend them in the docs?

$\endgroup$
1
$\begingroup$

Most of the things in the world appear in arbitrary numbers but it may be cases when the number is known and fixed, like the number days in a week, the number of values in quaternion (4) or the number of dimensions in the Universe (3). In such cases range specification is informative.

It may also be cases when specifying the number of bits adds to the clarity. For instance, if we write a simulator of some hardware containing various registers, it is nice to define the number of bits as used by these registers.

So generally a useful feature, and I remember programming tasks relevant to both cases.

$\endgroup$
0
$\begingroup$

If you don't have predefined widths, then you can't have predefined maximum and minimum values of a type. You also can't have a predefined identity value, or a predefined zero value used for field initialisation.

Furthermore, while variables of arbitrary size (i.e. number of bits, number of digits etc.) if the language allows them to be packed for storage it might result in problems passing them by reference, or- in the general case- taking their address.

So while they are extremely useful, they might not be a good choice for the base language types.

$\endgroup$
3
  • 2
    $\begingroup$ "If you don't have predefined widths, then you can't have predefined maximum and minimum values of a type [...]" => What? There's no reason you can't have i7::MIN and i7::MAX, 0 is always 0, etc... are you talking about a different concept altogether? $\endgroup$ Jul 13, 2023 at 9:18
  • $\begingroup$ @MatthieuM. I would suggest that things like that are precomputed rather than predefined, i.e. are compile-time evaluation of expression to appear in constants. $\endgroup$ Jul 13, 2023 at 10:25
  • $\begingroup$ Isn't that an implementation detail? The compiler will put the value either way. $\endgroup$ Jul 13, 2023 at 11:25

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .