Why it is promoted to int?

Go To Last Post
28 posts / 0 new
Author
Message
#1
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I have a simple function that should wait some number of ticks of Timer/Counter 2 (the target is ATMega48, C++).

#define byte uint8_t
void blockingDelay(byte ticksToWait) {
  byte currentTime=TCNT2;
  while ((TCNT2-currentTime)<ticksToWait);
}

By a lot of debugging and disassembly studying I have found the Atmel Studio 7 promotes the (TCNT2-currentTime) to unsigned int:

;void blockingDelay(byte ticksToWait) {
;  byte currentTime=TCNT2;
  56:	40 91 b2 00 	lds	r20, 0x00B2
;  while ((TCNT2-currentTime)<ticksToWait);
  5a:	e2 eb       	ldi	r30, 0xB2	; 178
  5c:	f0 e0       	ldi	r31, 0x00	; 0
  5e:	50 e0       	ldi	r21, 0x00	; 0
  60:	90 e0       	ldi	r25, 0x00	; 0
  62:	20 81       	ld	r18, Z
  64:	30 e0       	ldi	r19, 0x00	; 0
  66:	24 1b       	sub	r18, r20
  68:	35 0b       	sbc	r19, r21
  6a:	28 17       	cp	r18, r24
  6c:	39 07       	cpc	r19, r25
  6e:	cc f3       	brlt	.-14     	; 0x62 <_ZL13blockingDelayh+0xc>
  70:	08 95       	ret

I need to convert it back to byte:

void blockingDelay(byte ticksToWait) {
  byte currentTime=TCNT2;
  while ((byte)(TCNT2-currentTime)<ticksToWait);
}

to get the expected result (and don't be stuck in an endless loop at some TCNT2 values):

;void blockingDelay(byte ticksToWait) {
;  byte currentTime=TCNT2;
  56:	20 91 b2 00 	lds	r18, 0x00B2
;  while ((byte)(TCNT2-currentTime)<ticksToWait);
  5a:	e2 eb       	ldi	r30, 0xB2	; 178
  5c:	f0 e0       	ldi	r31, 0x00	; 0
  5e:	90 81       	ld	r25, Z
  60:	92 1b       	sub	r25, r18
  62:	98 17       	cp	r25, r24
  64:	e0 f3       	brcs	.-8      	; 0x5e <_ZL13blockingDelayh+0x8>
  66:	08 95       	ret

Why?

 

(As a side note it is funny the compiler uses LDS for reading the register at first and uses LD Z for the other reads. Maybe it wants to increase instructions usage?)

Last Edited: Sat. Sep 12, 2020 - 02:01 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

read up on the C standard. Integer promotion is something that catches us all out at some stage. Once you get to 32bit processors, it is usually inconsequential.

You want to blame avrgcc as this is the compiler toolchain, not Atmel Studio7.

 

https://stackoverflow.com/questi...

Last Edited: Sat. Sep 12, 2020 - 11:48 AM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Why?

Simply because it is a rule of the C language. Fortunately a simple rule.

If the rank is smaller than int then

 

If an int can represent all values of the original type, the value is converted to an int;
otherwise, it is converted to an unsigned int.

 

So actually uint8_t would be promoted for the operation to int not unsigned int.

It's the kind of thing that crops up more frequently in embedded pogramming, particularly for 8 bit micro, where it is much more common to be working with types smaller than int.

To read up on it, the phrases to look for are 'integer promotions' and 'the usual arithmetic conversions'.

EDIT: I see this is all explained in that stackoverflow link from #2

 

Adding a cast is the correct solution. The subtraction is still conceptually performed as int, the result is then conceptualy converted to uint8_t by the cast, that result is then conceptually promoted to int (but can't now be negative value) for the comparison. The compiler can optimise all of this to use only 8 bit operations since it knows in this case the result is the same AS IF it had performed the promotions.

 

 

Last Edited: Sat. Sep 12, 2020 - 12:01 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Thank you for the explanation. It makes sense now. However I wonder what they were thinking when they designed the standards.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

They were thinking that most targets would be 16-bit with reasonable support resources.

 

I don't think that anyone thought of using a HLL on an ATtiny2313.   Or that you would ever have a single chip.  e.g. with clock, ROM, RAM, peripherals, ... which all run from a single VCC rail.

 

David.

Last Edited: Sat. Sep 12, 2020 - 01:43 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Smajdalf wrote:
However I wonder what they were thinking when they designed the standards.
in baseball, "covering a base" (don't know the cricket term)

ISO/IEC 9899:201x

[page 62]

6.2.6 Representations of types

6.2.6.1 General

1 The representations of all types are unspecified except as stated in this subclause.

2 Except for bit-fields, objects are composed of contiguous sequences of one or more bytes, the number, order, and encoding of which are either explicitly specified or implementation-defined.

 

[page 572]

Annex J

(informative)

Portability issues

1 This annex collects some information about portability that appears in this International Standard.

J.1 Unspecified behavior

...

— Many aspects of the representations of types (6.2.6).

...

via ISO/IEC JTC1/SC22/WG14 - C: Approved standards

 


covering a base : definition of covering a base and synonyms of covering a base (English)

Type Layout | avr-gcc - GCC Wiki

 

"Dare to be naïve." - Buckminster Fuller

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Years ago I got bitten by the same thing.  When doing embedded programming, especially, you learn to be very liberal with your use of casts.  They also act as a form of code clarification, so a double bonus.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Question about casts: If a cast is not needed (lets say the value is int_16 and you cast it to int_16), does that add to the executable code. 

 

Yes, I know that I should  write a little code block to test with, but I am far away from AS7 at the moment.

 

Thanks

Jim

 

Until Black Lives Matter, we do not have "All Lives Matter"!

 

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I would not expect an unnecessary cast - or even a necessary cast - to add any overhead: it doesn't change the variable but rather the way it's being interpreted by the subsequent operation.

 

But I'm not a C lawyer, so the real world may vary... I'm doing a lot of casting to and from ints to uints to make sure that comparisons in particular work properly.

 

Neil

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Smajdalf wrote:
Maybe it wants to increase instructions usage?)

Yes, "it" gains some advantage by "increase instructions".  I don't know what, though.  Perhaps you can tell why?  And specify what "it" is.

 

Perhaps you instructed the compiler to do naive code generation with -O0 or similar?  Did you give us all of the relevant build information so that can be verified?

 

Perhaps C++ is really the bloated weasel that some have claimed -- didja ever think of that?  [probably Johan is not listening]  Maybe you had better change to a more efficient language.  You are already complaining about promotion that has been done for some decades.

 

Perhaps with your free C++ toolchain you are getting what you pay for.  Nah, just because you didn't pay for it doesn't mean you cannot complain.

 

Perhaps you can give enough context in the generated code so that it can be pointed out how subsequent references end up making  the generated sequence something other than non-optimal.  Can you do that -- post the generated code for the whole routine?  Point out all the non-optimal sequences, and tell the ideal.

You can put lipstick on a pig, but it is still a pig.

I've never met a pig I didn't like, as long as you have some salt and pepper.

Last Edited: Sat. Sep 12, 2020 - 08:29 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

ka7ehk wrote:

Question about casts: If a cast is not needed (lets say the value is int_16 and you cast it to int_16), does that add to the executable code. 

No it won't add anything.

For situations where it isn't actually necessary (ie. code already does the right thing according to the rules) it can still be useful for clarity, or to say 'yes, i know what I'm doing, I really do mean to assign this int8_t to a uint16_t' for example which is perfectly fine according to C rules without any cast but might look a little suspicious.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

barnacle wrote:

I would not expect an unnecessary cast - or even a necessary cast - to add any overhead: it doesn't change the variable but rather the way it's being interpreted by the subsequent operation.

Right, it is just telling the compiler which code to generate; for example, signed vs. unsigned operations.  I would be astonished if a cast to a smaller or equal data size generates additional code.  Casting to a larger data size will generally generate code to extend the smaller data into the larger data size (e.g. zero extension or sign extension).

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Well strictly speaking it is specifying a conversion. It is an explicit conversion, compared to implicit conversions (eg. like integer promotions, or the usual arithmetic conversions, or when assigining a value of one type to another type) which happen, well, implicitly, as according to the rules.

If you cast something to the same type it already is, it's not gong to change the generated code.

If you cast something to the same type that it was going to be implicitly converted to anyway, it's not going to change the generated code.

eg.

int8_t a;

uint16_t b = a;

uint16_t b = (uint16_t)a; // same as above

EDIT

uint16_t b = (uint16_t)(int8_t)a; // same as above (I haven't checked, but it will be I'm sure)

 

If it does change the code then, by definition, you can say it was not an unnecessary cast :)

 

Last Edited: Sat. Sep 12, 2020 - 10:39 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

I'm thinking of my increasingly feeble mind (iota by iota) and not being able to remember the rules of promotion very easily. Actually, that is mostly because I never learned them, but that is a less interesting story. 

 

Jim

 

Until Black Lives Matter, we do not have "All Lives Matter"!

 

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

And as I've said elsewhere - I consider hidden implicit promotion in C to be one of its less well thought out features (along with ints that change their size depending on the hardware you're compiling for)... when it's explicit you can see what's going on and can be certain that it's what you intended... but it can still catch you out.

 

Neil

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

You could think of it a bit like adding extra brackets. In some cases you need to add the brackets to force the code to be evaluated the right way.

In other cases it would do what you want accorrding to the precedence rules without needing the extra brackets, in which case adding the extra brackets dosen't hurt and won't change the code, so if you think it makes the code clearer with the extra brackets then go ahead.

 

For example

uint16_t b, c;

uint32_t a = b * c;

On a platform with 16 bit int, b and c are already same as unisgned int, so not changed by promotion, and the multiply is done as unsigned int which is 16 bit. So result can clearly overflow 16 bits.

 

To force the multiply to be done as unsigned 32 bit, need to expllcitly convert (cast) one of the operands. It is only necessary to cast one of them, the other will then be implicitly converted also to uint32_t by the rules of the usual arithmetic cinversions.

uint32_t a = (uint32_t)b * c;

 

But if you're not sure of the rules, or just think it's clearer, it doesn't hurt and won't increse the code to cast both of them

uint32_t a = (uint32_t)b * (uint32_t)c;

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

Smajdalf wrote:

I have a simple function that should wait some number of ticks of Timer/Counter 2 (the target is ATMega48, C++).

#define byte uint8_t
void blockingDelay(byte ticksToWait) {
  byte currentTime=TCNT2;
  while ((TCNT2-currentTime)<ticksToWait);
}

I need to convert it back to byte:

void blockingDelay(byte ticksToWait) {
  byte currentTime=TCNT2;
  while ((byte)(TCNT2-currentTime)<ticksToWait);
}

to get the expected result (and don't be stuck in an endless loop at some TCNT2 values)

Under what circumstances will the loop become endless?

 

Did you consider how to deal with timer wrap-around?

 

Let's say TCNT2 has value of 250 and ticksToWait is 10. Then currentTime will become 250 and the expression (TCNT2-currentTime) will never become larger than 5. So in that case you can never leave the loop.

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

That's precisely why you need the uint8_t cast

 

current_time = 250

 

TCNT2    (TCNT2 - 250) as int       (uint8_t)(TCNT2 - 250)

250               0                                             0

251               1                                             1

252               2                                             2

253               3                                             3

254               4                                             4

255               5                                             5

   0              -250                                          6

   1              -249                                          7

   2              -248                                          8

   3              -247                                          9

   4              -246                                        10

 

EDIT:

current_time would have been better named as start_time

Last Edited: Sun. Sep 13, 2020 - 01:16 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

delete

 

I'll try to add something better-

 

A problem with doing this unsigned math timer calculation, no matter if its 8 bits or 32, is you should protect from a failing calculation when you get close to the unsigned limit. For instance, it will depend on the loop time and timer prescale what you upper value can be. Pass in 255 to the function with a timer prescale of /1 and it could be a while before you get out of the function.  A simple solution is to limit the max value to something you know is safe- if( ticksToWait > 250 ) ticksToWait = 250;

Last Edited: Sun. Sep 13, 2020 - 04:13 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

MrKendo wrote:

That's precisely why you need the uint8_t cast

 

current_time = 250

 

TCNT2    (TCNT2 - 250) as int       (uint8_t)(TCNT2 - 250)

250               0                                             0

251               1                                             1

252               2                                             2

253               3                                             3

254               4                                             4

255               5                                             5

   0              -250                                          6

   1              -249                                          7

   2              -248                                          8

   3              -247                                          9

   4              -246                                        10

 

EDIT:

current_time would have been better named as start_time

Yes, this can be visualized using the PROGRAMMER mode of a calculator.  For example, 4 - 250 will display in HEX as something like 0xFFFFFF0A (or 0xFF0A as a 16-bit value).  Casting that down to a single unsigned byte will give 0x0A or 10.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

jaksel wrote:

Under what circumstances will the loop become endless?

 

The loop will become endless near the 8-bit rollover value of 255.  Imagine the goal is to count 10 ticks, with the start time = 250.  This requires the timer to count up to 260 (260 - 250 = 10), but an 8-bit counter can never count up to 260, so the signed subtraction will yield an endless result loop of

 

250 - 250 = 0

251 = 250 = 1

2

3

4

5

0 - 250 = -250

1 - 250 = -249

-248

-247

..........

-1

0

1

2

3

4

5

-250

-249

-248

-247

........

 

Only by forcing the subtraction result to be interpreted as unsigned will the subtraction result be able to advance to 10.

 

 

Last Edited: Sun. Sep 13, 2020 - 03:51 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

kk6gm wrote:

jaksel wrote:

 

Under what circumstances will the loop become endless?

 

The loop will become endless near the 8-bit rollover value of 255.  Imagine the goal is to count 10 ticks, with the start time = 250.  This requires the timer to count up to 260 (260 - 250 = 10), but an 8-bit counter can never count up to 260, so the signed subtraction will yield an endless result loop of

 

250 - 250 = 0

251 = 250 = 1

2

3

4

5

0 - 250 = -250

1 - 250 = -249

-248

-247

..........

-1

0

1

2

3

4

5

-250

-249

-248

-247

........

 

Only by forcing the subtraction result to be interpreted as unsigned will the subtraction result be able to advance to 10.

 

 

 

I know - my question was directed towards the OP. I just gave that example but I think you missed my explanation: "... the expression (TCNT2-currentTime) will never become larger than 5. So in that case you can never leave the loop."

 

My point is that this is not a problem caused by C's integer promotion rules; it is a problem caused by not taking into account timer wrap-around in the C code.

 

EDIT: here is one solution. Note that there is a slight chance that the code misses the time during which the timer reaches the desired value...

void blockingDelay(byte ticksToWait)
{
  byte currentTime=TCNT2;
  byte exitTime = currentTime + ticksToWait;
  while (TCNT2 != exitTime);
}

 

/Jakob Selbing

Last Edited: Sun. Sep 13, 2020 - 05:37 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

kk6gm wrote:
Yes, this can be visualized using the PROGRAMMER mode of a calculator.  For example, 4 - 250 will display in HEX as something like 0xFFFFFF0A (or 0xFF0A as a 16-bit value).  Casting that down to a single unsigned byte will give 0x0A or 10.

yes, assuming 2's complement signed representation (a pretty safe assumption), it boils down to simply truncating the bit pattern to 8 bits.

In the C spec, probably due to allowing for different types of signed representation, it is described in terms of values, seems a bit long winded but sometimes it is easier to think this way.

What it says is

 

1 When a value with integer type is converted to another integer type other than _Bool, if
the value can be represented by the new type, it is unchanged.

 

2 Otherwise, if the new type is unsigned, the value is converted by repeatedly adding or
subtracting one more than the maximum value that can be represented in the new type
until the value is in the range of the new type.

 

3 Otherwise, the new type is signed and the value cannot be represented in it; either the
result is implementation-defined or an implementation-defined signal is raised.

 

So our case of an int with value -246, what value do you get if converted to uint8_t.

We are in case 2)

The maximim value that can be represented in uint8_t is 255, one more than that is 256.

So add 256 to -246, result 10.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

jaksel wrote:

EDIT: here is one solution. Note that there is a slight chance that the code misses the time during which the timer reaches the desired value...

void blockingDelay(byte ticksToWait)
{
  byte currentTime=TCNT2;
  byte exitTime = currentTime + ticksToWait;
  while (TCNT2 != exitTime);
}

 

It is a fundamental Law of Physics.   Sh*t happens.

 

You should make an unsigned subtraction and check the signed result.   Ok,  TCNT2 will rollover pretty quick.   But there will be many occasions when latency misses matching the exact count.

 

If you do the same thing with a 16-bit Timer you will get BIG rollovers.

 

David.

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

jaksel wrote:
My point is that this is not a problem caused by C's integer promotion rules; it is a problem caused by not taking into account timer wrap-around in the C code.

 

Not really.

The question is, what's the easiest way to allow for timer wrap around.

Assuming an unsigned 8 bit counter, given some starting timestamp time_start, and some later timestamp time_now

uint8_t time_start;

uint8_t time_now;

The number of ticks from time_start to time_now is safely (assuming time_now is at most 255 ticks later) given by

uint8_t delta = time_now - time_start;

This works across wrap around, so seems like the easiest way to handle wrap around, nothing more is needed.

The problem was not appreciating that if you have this as part of a larger expression

while ((time_now - time_start) < 10)

then due to integer promotion the result of (time_now - time_start) is of type int, which becomes a negative value around the wrap. The result needs to be converted to uint8_t before making the comparison, either by using an intermediate variable or by using a cast.

The solution

uint8_t start_time = TCNT2;

while ((uint8_t)(TCNT2 - start_time) < ticks_to_wait);

is perfectly sound in terms of handling wrap.

EDIT:

but bear in mind what curtvm said in #19.

I'm generally assuming that the counter tick rate is somewhat slower then the instruction clock.

If you set ticks_to_wait as 255, there is only one possible value for TCNT2 for you to break out the loop.

Not a problem if timer tick is much slower than instruction clock and if you can't be interrupted, you will hit that value.

If that isn't the case, limiting the size of ticks_to_wait sounds like a very good idea!

But this is separate issue to handling wrap.

 

 

Last Edited: Sun. Sep 13, 2020 - 06:54 PM
  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

MrKendo wrote:

The problem was not appreciating that if you have this as part of a larger expression

while ((time_now - time_start) < 10)

then due to integer promotion the result of (time_now - time_start) is of type int, which becomes a negative value around the wrap. 

What I mean is that you cannot really blame integer promotion rules for doing something that is artihmetically and logically correct in this situation. My guess was that the OP simply forgot about wrap-around issues and thus got the impression that integer promotion rules mess up how the code works.

 

Clearly the expression (time_now - time_start) can become negative at wrap-around and this has to be dealt with somehow e.g. explicit cast or calculating the timer ending value WITH wrap-around. But I must admit that it is hard to intuitively read and understand how the solution with the explicit cast actually handles wrap-around!

/Jakob Selbing

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

jaksel wrote:
What I mean is that you cannot really blame integer promotion rules for doing something that is artihmetically and logically correct in this situation

 

I got the impression the OP thought it was arithmetically and logically correct for the subtraction of 2 uint8_t to be performed as uint8_t and yield a result as a uint8_t, in which case the wrap is handled, but that's not the way it works due to integer promotion.

But you may be right.

Who knows. Well, the OP knows :)

 

  • 1
  • 2
  • 3
  • 4
  • 5
Total votes: 0

If the target time is at most 128 ticks in the future while((TCNT2-target) & 0x80) should do the trick.

One might want to cast each to unsigned int to ensure that one does

not run afoul of the rules for bitwise arithmetic for signed integers.

Iluvatar is the better part of Valar.