Search code examples
cperformanceintegermicro-optimization

What are the pros and cons of int, unsigned int, uint_fastN_t, and int_fastN_t?


I have a variable and the type does not matter; the possible range will easily fit in any of the aforementioned types. For example a loop counter. Etc.

What are the pros and cons of using:

  • int
  • unsigned int
  • int_fastN_t
  • uint_fastN_t

Note to any potential close-voters: I am not asking which ones to use, because that would be opinion-based, but I am asking, what factors should I consider when choosing one over the other?


Solution

    • int
      This type pretty much only has one single benefit and that is backwards-compatibility of non-critical code where integer limits/sizes do not matter. You should almost never use it, unless you are stuck with some backwards compatibility case with old C90.

    • unsigned int
      Same arguments as with int - this type is mostly to be used in old code where integer sizes don't matter. It is slightly more useful though, because it can be regarded as a safe common type to use in expressions that are subject to implicit integer promotion. If you use unsigned int or cast an operand to it, then it is no longer subject to integer promotion, default argument promotion and similar. You can also safely use unsigned int in bit-wise arithmetic, where int should be avoided.

      Furthermore, small integer types like short, signed char etc are similarly dangerous because they are subject to implicit promotion, potential signedness change due to integer promotion, potential to hide overflows/wrap-around due to promotion, etc etc.

    • intn_t (where n corresponds to the word size of the target)
      Compared to int, the intn_t has the great advantage of being portable and of fixed width. But only if n is large enough not to make the operand subject of integer promotion, for example int32_t on a 32/64 bit computer.

      intn_t also has the great advantage of being guaranteed to be in 2's complement form, which is not guaranteed for int (until the release of C23).

      A little detail is that intn_t isn't guaranteed to be supported unless the target supports it. This doesn't apply to 99% of all real-world systems, only to some oddball DSP CPUs that have 16 bit bytes.

      Notably intn_t should only be used where you actually need signed numbers. If you don't need signed numbers then use the uintn_t equivalent.

    • uintn_t
      All the same benefits of intn_t but with yet another benefit, namely that it is also safe to use for bitwise-arithemtic. Making this the most universal integer type of them all.

    • int_fastn_t
      This is mostly used during manual micro-optimization, where size isn't important but you suspect that a larger integer type would result in faster code. It could also have its uses when writing widely portable code.

      For example if you develop code with arithmetic that needs to be at least 16 bit and should run on both 16 and 32 bit CPUs. Doing the arithmetic on int16_t might not be optimal for the 32-bitter because of limited instructions, alignment etc. But if you do the arithmetic on int_fast16_t then it will not needlessly burden the 16 bitter as 32 bit arithmetic would have, since the type would still be 16 bits on that target. But at the same time it still allows the 32 bitter to pick a larger type if that gives better performance.

      Another advantage is that int_fastn_t is mandatory for all C compilers for 2's complement targets, including freestanding implementations (embedded systems, oddball DSP compilers etc).

      The disadvantages vs intn_t is that int_fastn_t isn't fixed width, where exact width matters, and that using int_fastn_t enforces a manual speed-over-size optimization which we might normally let the picked compiler options decide.

    • uint_fastn_t

      Same advantages/disadvantages as int_fastn_t but is also suitable for bitwise-arithmetic.

    • int_leastn_t/uint_leastn_t

      These are oddball types with very specialized use. Basically you say that "I need the variable to be at least n bits but other than that you may do as you please". Unlike the fast types, this gives the compiler more freedom in picking either size or speed optimizations, or not optimization at all.

      There's not a lot of use for these types save when you need to optimize for size and at the same time be portable across multiple targets. For example in my 16 vs 32 bit CPU example above, the int_least16_t would still be 16 bits on the 16 bit CPU, but on the 32 bit CPU the compiler might chose to leave it as 16 bits in case that would give a size benefit.

      For example a 32 bit PowerPC with support for VLE encoding (basically: use 16 bit instructions when possible) might benefit from the least types, since you could leave the C code the same, no matter if the machine code uses VLE or not.


    Integer picker program
    I hacked together a tl;dr integer picker. Tweak the true/false per #define and let it pick the suitable integer type.

    Tested with gcc 13.2 -std=c2x. https://godbolt.org/z/zz9GTf35M

    // pick true/false for each of these parameters:
    #define CODE_NEEDS_TO_BE_WIDELY_PORTABLE           true
    #define CODE_MUST_BE_C90                           false
    #define I_NEED_NEGATIVE_NUMBERS                    false
    #define I_NEED_BITWISE_ARITHMETIC                  false
    #define I_CARE_ABOUT_NUMERICAL_LIMITS              true
    #define I_HAVE_SPECIAL_OPTIMIZATION_REQUIREMENTS   false
    
    _Static_assert(!(CODE_NEEDS_TO_BE_WIDELY_PORTABLE && CODE_MUST_BE_C90), 
                    "Contradicting requirements. C90 isnt portable.");
    
    #include <stdio.h>
    
    int main (void)
    {
      if(CODE_NEEDS_TO_BE_WIDELY_PORTABLE)
        if(!I_NEED_NEGATIVE_NUMBERS || I_NEED_BITWISE_ARITHMETIC)
          if(!I_HAVE_SPECIAL_OPTIMIZATION_REQUIREMENTS)
            puts("Use uintn_t");
          else
            puts("Use uint_fastn_t/uint_leastn_t depending on the situation.");
        else
          if(!I_HAVE_SPECIAL_OPTIMIZATION_REQUIREMENTS)
            puts("Use intn_t");
          else
            puts("Use uint_fastn_t/uint_leastn_t depending on the situation.");
      else
        if(!I_NEED_NEGATIVE_NUMBERS || I_NEED_BITWISE_ARITHMETIC)
          if(!CODE_MUST_BE_C90 || I_CARE_ABOUT_NUMERICAL_LIMITS)
            puts("Use uintn_t");
          else if(CODE_MUST_BE_C90 && I_CARE_ABOUT_NUMERICAL_LIMITS)
            puts("Use home-brewed type system replicating uintn_t from stdint.h");
          else
            puts("Use unsigned int");
        else
          if(!CODE_MUST_BE_C90 || I_CARE_ABOUT_NUMERICAL_LIMITS)
            puts("Use intn_t");
          else if(CODE_MUST_BE_C90 && I_CARE_ABOUT_NUMERICAL_LIMITS)
            puts("Use home-brewed type system replicating intn_t from stdint.h");
          else
            puts("Use int");
    }