Run-time vs. Compile-time Functions

I have some code like this

#define cl(n) (84 + 12*(log(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH[] = {
  cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9),
...

Basically, it uses a formula to calculate some lengths I need. This works, and it runs at compile time, not on the Arduino.

I was wondering how this is done. The arduino functions are meant to run on the Arduino, and they would use the Arduino-sized variables. But if you run a function at compile time, wouldn't it use the size of variables on the PC? For example, an int is 2 bytes on the Arduino but 4 bytes on the PC.

So could it be a problem writing code that runs functions before it loads on the Arduino? I my case, it produces the correct results, but I'm wondering how this would work in general.

The log() function here is more problematical, since it should only be available at run time. It appears gcc goes ahead and calls it at compile time.

This is the right way of doing heavy calculations like this.

But you have to understand the #define. It's like a find and replace template for the compiler. Before it starts compiling it uses that to scan the code.

So it will see cl(0) and replaces it with (84 + 12*(log(0/100.0) / LOG_OF_2) + 0.5) LOG_OF_2 seems to be a macro as well so that gets replaced. And it sees a log(), which is a C math function, an thinks, he, it only uses constants, I can already calculate that! After that, it's only a sum of constants so the compiler thinks, that's neat, I can just calculate that and only store the outcome! And so it does :)

But that isn't the issue, if the machine compiling has 64 bit ints, does it use those in the constant calculations, or does it restrict itself to the size on the target machine.

I uses what it thinks it needs to use for the target. Because of the 100,0 and 0,5 it will use floats. 32-bit floats that is on a Uno. It's really not that different from writing things like: unsigned long msPerHour = 1000UL* 60 * 60 or float aQuarter = 1.0 / 4

The compiler is smart enough to recognize basic math with constants and do it at compiler time.

Harbison and Steele state that constant initializer expressions "should be evaluated just as they would on the target computer".

But, floating point expressions can be done with more precision or range than the target environment so "the value of a floating point expression may be slightly different at compile time than it would be if evaluated during program execution."

Again, throwing the log() is technically not allowed so that the expression should be done at run-time, but then when has gcc ever bothered about things like that? 8^)

Yeay, the log() is a bit weird. But I really like it that GCC does it. A log() on runtime would be very slow.... So run speed above RAM :)

KeithRB: Again, throwing the log() is technically not allowed...

By "not allowed" I assume you mean "violates the C++ standard". Please provide a reference to that part of the standard.

shawnlg: I was wondering how this is done.

By the compiler. The key-phrase is "constant folding".

But if you run a function at compile time, wouldn't it use the size of variables on the PC?

Maybe.

So could it be a problem writing code that runs functions before it loads on the Arduino?

Let's find out...

volatile int v1;
volatile int v2;
volatile int v3;
volatile int v4;
volatile int v5;

void setup() 
{
  Serial.begin( 115200 );

  Serial.println( 60L * 1000L / 10L );
  
  Serial.println( 60 * 1000 / 10 );

  v1 = 60;
  v2 = 1000;
  v3 = 10;
  v4 = v1 * v2;
  v5 = v4 / 10;

  Serial.println( v5 );
}

void loop() 
{

}

I tried it on a couple other compilers and it works there too (one of which is super strict). Are you sure that evaluating log() isn't technically allowed?

cl(0) should be infinity as well. I can't say I cast infinity floats to unsigned chars often, but I'm a bit surprised it turns into 0.

I just went by harrison and steele, which is c, not c++.

Some potentially misleading/incomplete information in this thread.

First, #defines are NOT seen by the compiler at all. #defines are expanded by the pre-processor, which is run BEFORE the compiler, by doing, literally, a search and replace the source file, and creating a temporary file containing the modified source code. The compiler ONLY sees the modified source code, with the #defines expanded. The pre-processor also expands #include's, #if's, etc. - Anything that starts with '#'. The compiler really never knows all those things ever existed.

Whether an expression in a #define gets expanded at compile time or at run time depends entirely on the contents of the expression. If the expression contains ONLY constants, it will be evaluated entirely at compile time. If it contains ONLY variable references, it will be evaluated entirely at run-time. If it contains a mix of constants and variable references, it will evaluate as much of the expression as it can at compile time, and the rest will be left for run-time evaluation. So, for example, "x * 3 * 10" will become the run-time expression "x * 30".

Expression evaluation is handled by the compiler, not the pre-processor. Compile-time expression evaluation will follow the same rules as run-time expression evaluation, including precision, which is why "10 * 10000" even in a #define expression will NOT evaluate to 100000 using an AVR compiler. Just think of the incredibly arcane bugs that could be generated if compile-time expressions were evaluated using a different set of rules and precisions than run-time expressions!

I would be very surprised if there were any significant limitations on the functions that could be used in a compile-time expressions, especially in a modern optimizing compiler....

Regards, Ray L.

septillion: Yeay, the log() is a bit weird. But I really like it that GCC does it. A log() on runtime would be very slow.... So run speed above RAM :)

Exactly. I plan on re-coding some sound waves using the actual formula used to create them. Right now, they are just hard-coded numbers in a table. I'm hoping all math functions are done at compile time.

So I wonder which functions will gcc run at compile time. Is there a way to know? Is it something in the header file that would tell us? Or does it "realize" that everything is constants and just run it?

I do realize the #defines are not seen by the compiler. I just wanted a way to easily get the log into each variable. I forgot to include the other constant with the code, but basically it's just calculating a log with some other constants thrown in the formula.

I would be very surprised if there were any significant limitations on the functions that could be used in a compile-time expressions, especially in a modern optimizing compiler....

I don't think that the log function (or any other function) is evaluated at compile time they must be done at run time.

In this case i expect to find that the PROGMEM directive is being ignored ant the array ends up in SRAM and not in flash

Mark

log is being called at run time and PROGMEM is being ignored as this compiles

int billCount =0;
int bill(int a){

  billCount++;
}

#define LOG_OF_2 5
#define cl(n) (84 + 12*(bill(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH[] = {
  cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9)};

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  Serial.print("billCount =");
  Serial.println(billCount);
  

}

void loop() {
  // put your main code here, to run repeatedly:

I can't run the code now but you should get 10.

Mark

holmes4: I don't think that the log function (or any other function) is evaluated at compile time they must be done at run time.

In this case i expect to find that the PROGMEM directive is being ignored ant the array ends up in SRAM and not in flash

Mark

No, it's definitely in PROGMEM because it runs at compile time. If it ran at run time, it couldn't store the values. I assume those values get put in the hex file that gets loaded to the arduino.

It’s definitely getting calculated at compile time because it’s getting stored in the PROGMEM. If it didn’t, then pgm_read_byte() wouldn’t return the correct values.

This code:

#define LOG_OF_2 (log(2))

#define cl(n) (84 + 12*(log(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH[] = {cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9)};

void setup()
{
  Serial.begin(115200);             //! Initialize the serial port to the PC

  Serial.println(F("NOTE_LENGTH"));
  for(unsigned int i = 0; i < sizeof(NOTE_LENGTH)/sizeof(*NOTE_LENGTH); i++)
  {
    Serial.println(pgm_read_byte(&NOTE_LENGTH[i]));
  }

  Serial.print(F("cl(0) = "));
  Serial.println(cl(0));

}

void loop()
{

}

Produces these results:

NOTE_LENGTH
0
4
16
23
28
32
35
38
40
42
cl(0) = inf

holmes4:
log is being called at run time and PROGMEM is being ignored as this compiles

int billCount =0;

int bill(int a){

billCount++;
}

#define LOG_OF_2 5
#define cl(n) (84 + 12*(bill(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH = {
 cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9)};

void setup() {
 // put your setup code here, to run once:
 Serial.begin(9600);
 Serial.print(“billCount =”);
 Serial.println(billCount);

}

void loop() {
 // put your main code here, to run repeatedly:




I can't run the code now but you should get 10.

Mark

Try this:

#include <Arduino.h>
#include <Math.h>

#define PP

#define x(y) log(y)

int a = 2;

void setup() {
#ifdef PP
  static const int z = x(2);
#else  
  static const int z = x(a);
#endif  
  for (int i=0; i<3; i++)
  {
    Serial.begin(9600);
    Serial.print(z);
    Serial.print(a++);
  }
}

void loop() {
}

Run it first with #PP defined, then again with it commented out. You’ll find the latter is ~1K larger, and uses ~10 more bytes of RAM, because it has to actually include log() into the object file., because the argument is no longer a compile-time constant.

Regards,
Ray L.

Which optimizations are used by the compiler depends on what options are passed to it - some aggressive optimizations may/can lead to standards violation, you have to check the compiler documentation and work out what the Arduino IDE is passing to it.

OK, now I’m super confused.

Check out the assembly for this code:

#define LOG_OF_2 (log(2))

double a = 0;

double notlog(double x)
{
  return x + a;
}

#define cl(n) (84 + 12*(log(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH[] = {cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9)};

void setup()
{
  Serial.begin(115200);             //! Initialize the serial port to the PC

  for(unsigned int i = 0; i < sizeof(NOTE_LENGTH)/sizeof(*NOTE_LENGTH); i++)
  {
    Serial.print("NOTE_LENGTH[");
    Serial.print(i);
    Serial.print("] @ ");
    Serial.print((unsigned long)(&NOTE_LENGTH[i]), HEX);
    Serial.print(" = ");
    Serial.print(pgm_read_byte(&NOTE_LENGTH[i]));
    Serial.print(" or ");
    Serial.print(NOTE_LENGTH[i]);
    Serial.println("?");   
  }
}

void loop()
{

}

Which produces these results:

NOTE_LENGTH[0] @ C2 = 0 or 6?
NOTE_LENGTH[1] @ C3 = 4 or 184?
NOTE_LENGTH[2] @ C4 = 16 or 16?
NOTE_LENGTH[3] @ C5 = 23 or 0?
NOTE_LENGTH[4] @ C6 = 28 or 0?
NOTE_LENGTH[5] @ C7 = 32 or 184?
NOTE_LENGTH[6] @ C8 = 35 or 184?
NOTE_LENGTH[7] @ C9 = 38 or 184?
NOTE_LENGTH[8] @ CA = 40 or 184?
NOTE_LENGTH[9] @ CB = 42 or 184?

It absolutely produces code that it could call at runtime, and appears to not be making a const array at all.

#define cl(n) (84 + 12*(log(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH[] = {cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9)};
 1ee:	60 e0       	ldi	r22, 0x00	; 0
 1f0:	70 e0       	ldi	r23, 0x00	; 0
 1f2:	cb 01       	movw	r24, r22
 1f4:	0e 94 e3 05 	call	0xbc6	; 0xbc6 <log>
 1f8:	28 e1       	ldi	r18, 0x18	; 24
 1fa:	32 e7       	ldi	r19, 0x72	; 114
 1fc:	41 e3       	ldi	r20, 0x31	; 49
 1fe:	5f e3       	ldi	r21, 0x3F	; 63
 200:	0e 94 bc 04 	call	0x978	; 0x978 <__divsf3>
 204:	20 e0       	ldi	r18, 0x00	; 0
 206:	30 e0       	ldi	r19, 0x00	; 0
 208:	40 e4       	ldi	r20, 0x40	; 64
 20a:	51 e4       	ldi	r21, 0x41	; 65
 20c:	0e 94 23 06 	call	0xc46	; 0xc46 <__mulsf3>
 210:	20 e0       	ldi	r18, 0x00	; 0
 212:	30 e0       	ldi	r19, 0x00	; 0
 214:	48 ea       	ldi	r20, 0xA8	; 168
 216:	52 e4       	ldi	r21, 0x42	; 66
 218:	0e 94 58 04 	call	0x8b0	; 0x8b0 <__addsf3>
 21c:	20 e0       	ldi	r18, 0x00	; 0
 21e:	30 e0       	ldi	r19, 0x00	; 0
 220:	40 e0       	ldi	r20, 0x00	; 0
 222:	5f e3       	ldi	r21, 0x3F	; 63
 224:	0e 94 58 04 	call	0x8b0	; 0x8b0 <__addsf3>
 228:	0e 94 24 05 	call	0xa48	; 0xa48 <__fixunssfsi>
 22c:	60 93 c2 00 	sts	0x00C2, r22
 230:	08 95       	ret

But those addresses getting Serial.printed? Well…they actually do contain the NOTE_LENGTH const array.

000000c2 <__trampolines_end>:
  c2:	00 04       	cpc	r0, r0
  c4:	10 17       	cp	r17, r16
  c6:	1c 20       	and	r1, r12
  c8:	23 26       	eor	r2, r19
  ca:	28 2a       	or	r2, r24

And FWIW, the values in the const array are what is getting lpm-ed when the values are printed:

    Serial.print(pgm_read_byte(&NOTE_LENGTH[i]));
 192:	fe 01       	movw	r30, r28
 194:	64 91       	lpm	r22, Z
 196:	4a e0       	ldi	r20, 0x0A	; 10
 198:	50 e0       	ldi	r21, 0x00	; 0
 19a:	8e e3       	ldi	r24, 0x3E	; 62
 19c:	91 e0       	ldi	r25, 0x01	; 1
 19e:	0e 94 b5 03 	call	0x76a	; 0x76a <_ZN5Print5printEhi>

This is really strange.

OK, one last test (and apparently I’m the only one that still cares). I switched to using the notlog function which shouldn’t be something that it can calculate at compile time. Also, I’m using Arduino 1.6.8.

Here is the code:

#define LOG_OF_2 (log(2))

double a = 0;

double notlog(double x)
{
  return x + a;
}

#define cl(n) (84 + 12*(notlog(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH[] = {cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9)};

void setup()
{
  Serial.begin(115200);             //! Initialize the serial port to the PC
}

void loop()
{
  a++;
  Serial.print("a = ");
  Serial.println(a);
  for(unsigned int i = 0; i < sizeof(NOTE_LENGTH)/sizeof(*NOTE_LENGTH); i++)
  {
    Serial.print("NOTE_LENGTH[");
    Serial.print(i);
    Serial.print("] @ ");
    Serial.print((unsigned long)(&NOTE_LENGTH[i]), HEX);
    Serial.print(" = ");
    Serial.print(pgm_read_byte(&NOTE_LENGTH[i]));
    Serial.print(" or ");
    Serial.print(NOTE_LENGTH[i]);
    Serial.println("?");   
  }
}

I’m honestly surprised it builds. It seems like I should get an error. When I try to run it, it doesn’t even run. These are the results:

a =

And the assembly? Check this out:

#define cl(n) (84 + 12*(notlog(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH[] = {cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9)};
     20c: 28 e1       ldi r18, 0x18 ; 24
     20e: 32 e7       ldi r19, 0x72 ; 114
     210: 41 e3       ldi r20, 0x31 ; 49
     212: 5f e3       ldi r21, 0x3F ; 63
     214: 0e 94 60 07 call 0xec0 ; 0xec0 <__divsf3>
     218: 20 e0       ldi r18, 0x00 ; 0
     21a: 30 e0       ldi r19, 0x00 ; 0
     21c: 40 e4       ldi r20, 0x40 ; 64
     21e: 51 e4       ldi r21, 0x41 ; 65
     220: 0e 94 af 08 call 0x115e ; 0x115e <__mulsf3>
     224: 20 e0       ldi r18, 0x00 ; 0
     226: 30 e0       ldi r19, 0x00 ; 0
     228: 48 ea       ldi r20, 0xA8 ; 168
     22a: 52 e4       ldi r21, 0x42 ; 66
     22c: 0e 94 f8 06 call 0xdf0 ; 0xdf0 <__addsf3>
     230: 20 e0       ldi r18, 0x00 ; 0
     232: 30 e0       ldi r19, 0x00 ; 0
     234: 40 e0       ldi r20, 0x00 ; 0
     236: 5f e3       ldi r21, 0x3F ; 63
     238: 0e 94 f8 06 call 0xdf0 ; 0xdf0 <__addsf3>
     23c: 0e 94 cd 07 call 0xf9a ; 0xf9a <__fixunssfsi>
     240: 60 93 68 00 sts 0x0068, r22

double a = 0;

double notlog(double x)
{
  return x + a;
     244: 2a e0       ldi r18, 0x0A ; 10
     246: 37 ed       ldi r19, 0xD7 ; 215
     248: 43 e2       ldi r20, 0x23 ; 35
     24a: 5c e3       ldi r21, 0x3C ; 60
     24c: c7 01       movw r24, r14
     24e: b6 01       movw r22, r12
     250: 0e 94 f8 06 call 0xdf0 ; 0xdf0 <__addsf3>
}

#define cl(n) (84 + 12*(notlog(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH[] = {cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9)};
     254: 28 e1       ldi r18, 0x18 ; 24
     256: 32 e7       ldi r19, 0x72 ; 114
     258: 41 e3       ldi r20, 0x31 ; 49
     25a: 5f e3       ldi r21, 0x3F ; 63
     25c: 0e 94 60 07 call 0xec0 ; 0xec0 <__divsf3>
     260: 20 e0       ldi r18, 0x00 ; 0
     262: 30 e0       ldi r19, 0x00 ; 0
     264: 40 e4       ldi r20, 0x40 ; 64
     266: 51 e4       ldi r21, 0x41 ; 65
     268: 0e 94 af 08 call 0x115e ; 0x115e <__mulsf3>
     26c: 20 e0       ldi r18, 0x00 ; 0
     26e: 30 e0       ldi r19, 0x00 ; 0
     270: 48 ea       ldi r20, 0xA8 ; 168
     272: 52 e4       ldi r21, 0x42 ; 66
     274: 0e 94 f8 06 call 0xdf0 ; 0xdf0 <__addsf3>
     278: 20 e0       ldi r18, 0x00 ; 0
     27a: 30 e0       ldi r19, 0x00 ; 0
     27c: 40 e0       ldi r20, 0x00 ; 0
     27e: 5f e3       ldi r21, 0x3F ; 63
     280: 0e 94 f8 06 call 0xdf0 ; 0xdf0 <__addsf3>
     284: 0e 94 cd 07 call 0xf9a ; 0xf9a <__fixunssfsi>
     288: 60 93 69 00 sts 0x0069, r22

<snipped to get under 9000 characters>

#define cl(n) (84 + 12*(notlog(n/100.0) / LOG_OF_2) + 0.5)
PROGMEM const unsigned char NOTE_LENGTH[] = {cl(0), cl(1), cl(2), cl(3), cl(4), cl(5), cl(6), cl(7), cl(8), cl(9)};
     494: 28 e1       ldi r18, 0x18 ; 24
     496: 32 e7       ldi r19, 0x72 ; 114
     498: 41 e3       ldi r20, 0x31 ; 49
     49a: 5f e3       ldi r21, 0x3F ; 63
     49c: 0e 94 60 07 call 0xec0 ; 0xec0 <__divsf3>
     4a0: 20 e0       ldi r18, 0x00 ; 0
     4a2: 30 e0       ldi r19, 0x00 ; 0
     4a4: 40 e4       ldi r20, 0x40 ; 64
     4a6: 51 e4       ldi r21, 0x41 ; 65
     4a8: 0e 94 af 08 call 0x115e ; 0x115e <__mulsf3>
     4ac: 20 e0       ldi r18, 0x00 ; 0
     4ae: 30 e0       ldi r19, 0x00 ; 0
     4b0: 48 ea       ldi r20, 0xA8 ; 168
     4b2: 52 e4       ldi r21, 0x42 ; 66
     4b4: 0e 94 f8 06 call 0xdf0 ; 0xdf0 <__addsf3>
     4b8: 20 e0       ldi r18, 0x00 ; 0
     4ba: 30 e0       ldi r19, 0x00 ; 0
     4bc: 40 e0       ldi r20, 0x00 ; 0
     4be: 5f e3       ldi r21, 0x3F ; 63
     4c0: 0e 94 f8 06 call 0xdf0 ; 0xdf0 <__addsf3>
     4c4: 0e 94 cd 07 call 0xf9a ; 0xf9a <__fixunssfsi>
     4c8: 60 93 71 00 sts 0x0071, r22

I sort of feel sorry for the compiler. It seems to be as confused as I am :frowning:

Hmm…maybe I will do one more test with IAR Workbench to see how it handles this stuff.