It seems that uint32_t
is much more prevalent than uint_fast32_t
(I realise this is anecdotal evidence). That seems counter-intuitive to me, though.
Several reasons.
In summary the "fast" types are worthless garbage. If you really need to figure out what type is fastest for a given application you need to benchmark your code on your compiler.
For practical purposes, uint_fast32_t
is completely useless. It's defined incorrectly on the most widespread platform (x86_64), and doesn't really offer any advantages anywhere unless you have a very low-quality compiler. Conceptually, it never makes sense to use the "fast" types in data structures/arrays - any savings you get from the type being more efficient to operate on will be dwarfed by the cost (cache misses, etc.) of increasing the size of your working data set. And for individual local variables (loop counters, temps, etc.) a non-toy compiler can usually just work with a larger type in the generated code if that's more efficient, and only truncate to the nominal size when necessary for correctness (and with signed types, it's never necessary).
The one variant that is theoretically useful is uint_least32_t
, for when you need to be able to store any 32-bit value, but want to be portable to machines that lack an exact-size 32-bit type. Practically, speaking, however, that's not something you need to worry about.
uint32_t
is guaranteed to have nearly the same properties on any platform that supports it.1
uint_fast32_t
has very little guarantees about how it behaves on different systems in comparison.
If you switch to a platform where uint_fast32_t
has a different size, all code that uses uint_fast32_t
has to be retested and validated. All stability assumptions are going to be out the window. The entire system is going to work differently.
When writing your code, you may not even have access to a uint_fast32_t
system that isn't 32 bits in size.
uint32_t
won't work differently (see footnote).
Correctness is more important than speed. Premature correctness is thus a better plan than premature optimization.
In the event I was writing code for systems where uint_fast32_t
was 64 or more bits, I might test my code for both cases and use it. Barring both need and opportunity, doing so is a bad plan.
Finally, uint_fast32_t
when you are storing it for any length of time or number of instances can be slower than uint32
simply due to cache size issues and memory bandwidth. Todays computers are far more often memory-bound than CPU bound, and uint_fast32_t
could be faster in isolation but not after you account for memory overhead.
1 As @chux has noted in a comment, if unsigned
is larger than uint32_t
, arithmetic on uint32_t
goes through the usual integer promotions, and if not, it stays as uint32_t
. This can cause bugs. Nothing is ever perfect.
Why do many people use
uint32_t
rather thanuint32_fast_t
?
Note: Mis-named uint32_fast_t
should be uint_fast32_t
.
uint32_t
has a tighter specification than uint_fast32_t
and so makes for more consistent functionality.
uint32_t
pros:
uint32_t
cons:
uint_fast32_t
pros:
uint_fast32_t
cons:
uint32_fast_t
. Looks like many just don't need and use this type. We didn't even use the right name!uint_fast32_t
is only a 1st order approximation.In the end, what is best depends on the coding goal. Unless coding for very wide portability or some niched performance function, use uint32_t
.
There is another issue when using these types that comes into play: their rank compared to int/unsigned
Presumably uint_fastN_t
could be the rank of unsigned
. This is not specified, but a certain and testable condition.
Thus, uintN_t
is more likely than uint_fastN_t
to be narrower the unsigned
. This means that code that uses uintN_t
math is more likely subject to integer promotions than uint_fastN_t
when concerning portability.
With this concern: portability advantage uint_fastN_t
with select math operations.
Side note about int32_t
rather than int_fast32_t
: On rare machines, INT_FAST32_MIN
may be -2,147,483,647 and not -2,147,483,648. The larger point: (u)intN_t
types are tightly specified and lead to portable code.
In many cases, when an algorithm works on an array of data, the best way to improve performance is to minimize the number of cache misses. The smaller each element, the more of them can fit into the cache. This is why a lot of code is still written to use 32-bit pointers on 64-bit machines: they don’t need anything close to 4 GiB of data, but the cost of making all pointers and offsets need eight bytes instead of four would be substantial.
There are also some ABIs and protocols specified to need exactly 32 bits, for example, IPv4 addresses. That’s what uint32_t
really means: use exactly 32 bits, regardless of whether that’s efficient on the CPU or not. These used to be declared as long
or unsigned long
, which caused a lot of problems during the 64-bit transition. If you just need an unsigned type that holds numbers up to at least 2³²-1, that’s been the definition of unsigned long
since the first C standard came out. In practice, though, enough old code assumed that a long
could hold any pointer or file offset or timestamp, and enough old code assumed that it was exactly 32 bits wide, that compilers can’t necessarily make long
the same as int_fast32_t
without breaking too much stuff.
In theory, it would be more future-proof for a program to use uint_least32_t
, and maybe even load uint_least32_t
elements into a uint_fast32_t
variable for calculations. An implementation that had no uint32_t
type at all could even declare itself in formal compliance with the standard! (It just wouldn’t be able to compile many existing programs.) In practice, there’s no architecture any more where int
, uint32_t
, and uint_least32_t
are not the same, and no advantage, currently, to the performance of uint_fast32_t
. So why overcomplicate things?
Yet look at the reason all the 32_t
types needed to exist when we already had long
, and you’ll see that those assumptions have blown up in our faces before. Your code might well end up running someday on a machine where exact-width 32-bit calculations are slower than the native word size, and you would have been better off using uint_least32_t
for storage and uint_fast32_t
for calculation religiously. Or if you’ll cross that bridge when you get to it and just want something simple, there’s unsigned long
.