4

Assuming to have a counter which counts from 0 to 100, is there an advantage of declaring the counter variable as uint16_t instead of uint8_t.

Obviously if I use uint8_t I could save some space. On a processor with natural wordsize of 16 bits access times would be the same for both I guess. I couldn't think why I would use a uint16_t if uint8_t can cover the range.

  • What was your answer? – Barmar Aug 22 '21 at 19:35
  • I couldn't think of any. I asked for range of values for the counterand was told 0 -100.Obviously if I use uin8_t I could save some space.On a processor with natural wordsize of 16 bits access times would be the same for both I guess. I couldn't think why I would use a uint16_t if uni8_t can cover the range. – C-ode Menon Aug 22 '21 at 19:47
  • It could make a performance different depending on the hardware. If it has 16 bit arithmetic registers, loading and storing 16 bit quantities might be more efficient. – Barmar Aug 22 '21 at 19:48
  • I incorporated OP's comment to the question so that, IMHO, it is no more too broad and it makes it an interesting one – Roberto Caboni Aug 22 '21 at 20:36
  • 2
    If it's just one variable, and your compiler is decent, it'll probably put it in a register instead of in memory, so there'll be no savings of space. Selecting types to "save space" is usually only worth considering when you have a (fairly large) array of objects of the type. – Nate Eldredge Aug 22 '21 at 20:55

3 Answers3

9

Using a wider type than necessary can allow the compiler to avoid having to mask the higher bits.

Suppose you were working on a 16 bit architecture, then using uint16_t could be more efficient, however if you used uint16_t instead of uint8_t on a 32 bit architecture then you would still have the mask instructions but just masking a different number of bits.

The most efficient type to use in a cross-platform portable way is just plain int or unsigned int, which will always be the correct type to avoid the need for masking instructions, and will always be able to hold numbers up to 100.

If you are in a MISRA or similar regulated environment that forbids the use of native types, then the correct standard-compliant type to use is uint_fast8_t. This guarantees to be the fastest unsigned integer type that has at least 8 bits.

However, all of this is nonsense really. Your primary goal in writing code should be to make it readable, not to make it as fast as possible. Penny-pinching instructions like this makes code convoluted and more likely to have bugs. Also because it is harder to read, the bugs are less likely to be found during code review.

You should only try to optimize like this once the code is finished and you have tested it and found the particular part which is the bottleneck. Masking a loop counter is very unlikely to be the bottleneck in any real code.

Tom V
  • 4,827
  • 2
  • 5
  • 22
  • I'm not really buying the efficiency arguments. Many 16 bitters have instruction support for 8 bit, allowing them to generate equally effective code for 8 bit variables. And some restricted 16 bitters like 68HC12 only got one 16 bit data register, so 16 bit arithmetic might actually be slower than 8 bit, if it means it has to do more loading/stacking or compare vs index registers etc. – Lundin Aug 23 '21 at 14:22
  • @SteveSummit Instead of coming up with such analogies, why don't you name the number of known targets where this would actually lead to slower code? Because I know of none - including weirdo TI DSPs with 16 bit bytes, because compilers for those won't let this code compile to begin with. – Lundin Aug 23 '21 at 14:54
  • 1
    @Lundin You are correct that some 16-bit architectures are equally fast with 8 bits, but I still think this is likely to be what the the interviewer had in mind. I will change "would be most efficient" to "could be most efficient". – Tom V Aug 23 '21 at 15:37
  • @Lundin you will notice that I thought better of posting that analogy, and deleted it almost immediately. But I agree with Tom V's comment just above (I'd change the wording to "could be more efficient"), and I stand by my opinion that using an 8-bit type as a loop index variable is almost always a silly idea at best. – Steve Summit Aug 23 '21 at 15:42
  • [I'm aware, though, that just as C and C++ are now almost completely different languages, so too are "C for hosted applications" and "C for embedded", and my personal experience doing embedded programming is rather small and obsolete, so I have unwatched the [embedded] tag, which will hopefully prevent me from annoying you by posting hostist opinions in threads like this in future.] – Steve Summit Aug 23 '21 at 15:55
2

Obviously if I use uint8_t I could save some space.

Actually, that's not necessarily obvious! A loop index variable is likely to end up in a register, and if it does there's no memory to be saved. Also, since the definition of the C language says that much arithmetic takes place using type int, it's possible that using a variable smaller than int might actually end up costing you space in terms of extra code emitted by the compiler to convert back and forth between int and your smaller variable. So while it could save you some space, it's not at all guaranteed that it will — and, in any case, the actual savings are going to be almost imperceptibly small in the grand scheme of things.

If you have an array of some number of integers in the range 0-100, using uint8_t is a fine idea if you want to save space. For an individual variable, on the other hand, the arguments are pretty different.

In general, I'd say that there are two reasons not to use type uint8_t (or, equivalently, char or unsigned char) as a loop index:

  1. It's not going to save much data space (if at all), and it might cost code size and/or speed.

  2. If the loop runs over exactly 256 elements (yours didn't, but I'm speaking more generally here), you may have introduced a bug (which you'll discover soon enough): your loop may run forever.

The interviewer was probably expecting #1 as an answer. It's not a guaranteed answer — under plenty of circumstances, using the smaller type won't cost you anything, and evidently there are microprocessors where it can actually save something — but as a general rule, I agree that using an 8-bit type as a loop index is, well, silly. And whether or not you agree, it's certainly an issue to be aware of, so I think it's a fair interview question.

See also this question, which discusses the same sorts of issues.

Steve Summit
  • 45,437
  • 7
  • 70
  • 103
  • Using `uint8_t` as the loop iterator gives _much faster code at best_. On pretty much any mainstream 8 bitter. AVR, PIC, 8051, R8C, HC08, STM8 and so on. And on some 16 bitters as well. It is quite possible that declaring it as a larger type blocks optimizations on certain compilers. – Lundin Aug 23 '21 at 16:41
  • Well, I would sort it into the myths of optimizations. We had exactly that argument at work and I proofed them on a 32bitter, that sorting and indexing with uint32_t isn't faster than with uint16_t. Changing the code to uint16_t saved me 1200 B RAM to store the index, but the runtime of the sorting was 71µs w/ uint16_t vs. 73µs w/ uint32_t. I wonder, when interviews come up with the point, that the right datatstructures and algorithms fot the job gives much better optimization results, like optimizing BubbleSort 16 vs 32bit won't change its O(n^2) avg. runtime, but using e.g. Heapsort would. – kesselhaus Aug 24 '21 at 01:51
  • On Cortex M, using *unsigned* instead of *uint8_t* as a loop counter actually saves a few bytes (~4 bytes or so) of .text for each loop, if I remember correctly (GCC, -O1). – Tagli Aug 24 '21 at 08:20
  • @Tagli I don't see any difference there either. gcc ARM none-eabi with -O3: https://godbolt.org/z/j4cP33xns – Lundin Aug 24 '21 at 14:03
1

The interview question doesn't make much sense from a platform-generic point of view. If we look at code such as this:

for(uint8_t i=0; i<n; i++)
  array[i] = x;

Then the expression i<n will get carried out on type int or larger because of implicit promotion. Though the compiler may optimize it to use a smaller type if it doesn't affect the result.

As for array[i], the compiler is likely to use a type corresponding to whatever address size the system is using.

What the interviewer was fishing for is likely that uint32_t on a 32 bitter tend to generate faster code in some situations. For those cases you can use uint_fast8_t, but more likely the compiler will perform optimizations no matter.

The only optimization uint8_t blocks the compiler from doing, is to allocate a larger variable than 8 bits on the stack. It doesn't however block the compiler from optimizing out the variable entirely and using a register instead. Such as for example storing it in an index register with the same width as the address bus.

Example with gcc x86_64: https://godbolt.org/z/vYscf3KW9. The disassembly is pretty painful to read, but the compiler just picked CPU registers to store anything regardless of the type of i, giving identical machine code between uint8_t anduint16_t. I would have been surprised if it didn't.

On a processor with natural wordsize of 16 bits access times would be the same for both I guess.

Yes this is true for all mainstream 16 bitters. Some might even manage faster code if given 8 bits instead of 16. Some exotic systems like DSP exist, but in case of lets say a 1 byte=16 bits DSP, then the compiler doesn't even provide you with uint8_t to begin with - it is an optional type. One generally doesn't bother with portability to wildly exotic systems, since doing so is a waste of everyone's time and money.

The correct answer: it is senseless to do manual optimization without a specific system in mind. uint8_t is perfectly fine to use for generic, portable code.

Lundin
  • 195,001
  • 40
  • 254
  • 396