Optimization weirdness ?!?

Hi Guys,

I saw something weird with the optimization. When I make a function to replace a section of repetitive code in my sketch, the code size increases. I would have expected a decrease in code since the section is bigger than a "JMP" instruction ?

Here is the code:

#define DEBUG 1

void setup() {
  // put your setup code here, to run once:
  Serial.begin (115200);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (DEBUG)
  {
    Serial.println (F("Debug 1"));
  }
  Serial.println (F("Hello world"));
  if (DEBUG)
  {
    Serial.println (F("Debug 2"));
  }
  //dlog (F("Debug 2"));
  delay (1000);
  dlog (F("Debug 3"));
}

void dlog (String msg)
{
  if (DEBUG)
  {
    Serial.println (msg);
  }
}

Sketch uses 3,856 bytes (1%) of program storage space. Maximum is 253,952 bytes.
Global variables use 210 bytes (2%) of dynamic memory, leaving 7,982 bytes for local variables. Maximum is 8,192 bytes.

But now:

#define DEBUG 1

void setup() {
  // put your setup code here, to run once:
  Serial.begin (115200);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (DEBUG)
  {
    Serial.println (F("Debug 1"));
  }
  Serial.println (F("Hello world"));
  //if (DEBUG)
  //{
  //  Serial.println (F("Debug 2"));
  //}
  dlog (F("Debug 2"));
  delay (1000);
  dlog (F("Debug 3"));
}

void dlog (String msg)
{
  if (DEBUG)
  {
    Serial.println (msg);
  }
}

Sketch uses 3,868 bytes (1%) of program storage space. Maximum is 253,952 bytes.
Global variables use 210 bytes (2%) of dynamic memory, leaving 7,982 bytes for local variables. Maximum is 8,192 bytes.

Can someone perhaps explain that please ?

Cheers,

Pieter

A function call is expensive ?

In the first program, you only call dlog once, so the compiler probably made it in-line, to save the function call overhead. In the second, you call dlog twice, so it must not only create the function, but also the two function calls, which will increase program memory usage.

Regards,
Ray L.

RayLivingston:
In the first program, you only call dlog once, so the compiler probably made it in-line, to save the function call overhead. In the second, you call dlog twice, so it must not only create the function, but also the two function calls, which will increase program memory usage.

Regards,
Ray L.

Hey,

This was just a sample to show it - the program where I noticed it on has 100's of calls to dlog.

6v6gt:
A function call is expensive ?

String class is expensive:

Sketch uses 2,388 bytes (0%) of program storage space. Maximum is 253,952 bytes.
Global variables use 182 bytes (2%) of dynamic memory, leaving 8,010 bytes for local variables. Maximum is 8,192 bytes.
#define DEBUG 1
const char msg2[] PROGMEM  = {"Debug 2"};
const char msg3[] PROGMEM  = {"Debug 3"};

void setup() {
  // put your setup code here, to run once:
  Serial.begin (9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (DEBUG)
  {
    Serial.println (F("Debug 1"));
  }
  Serial.println (F("Hello world"));
  //if (DEBUG)
  //{
  //  Serial.println (F("Debug 2"));
  //}
  cheapPrint(msg2);
  
  cheapPrint(msg3);
  
  delay (1000);
}


void cheapPrint(const char* message)
{
  int len = strlen_P(msg2);
  for (int i = 0; i < len; i++)
  {
    char myChar =  pgm_read_byte_near(message + i);
    Serial.print(myChar);
  }
  Serial.println();
}

dewitpj:
I saw something weird with the optimization. When I make a function to replace a section of repetitive code in my sketch, the code size increases. I would have expected a decrease in code since the section is bigger than a "JMP" instruction ?

Did you mean a CALL instruction?

BulldogLowell:
String class is expensive:

Sketch uses 2,388 bytes (0%) of program storage space. Maximum is 253,952 bytes.

Global variables use 182 bytes (2%) of dynamic memory, leaving 8,010 bytes for local variables. Maximum is 8,192 bytes.






#define DEBUG 1
const char msg2 PROGMEM  = {“Debug 2”};
const char msg3 PROGMEM  = {“Debug 3”};

void setup() {
  // put your setup code here, to run once:
  Serial.begin (9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  if (DEBUG)
  {
    Serial.println (F(“Debug 1”));
  }
  Serial.println (F(“Hello world”));
  //if (DEBUG)
  //{
  //  Serial.println (F(“Debug 2”));
  //}
  cheapPrint(msg2);
 
  cheapPrint(msg3);
 
  delay (1000);
}

void cheapPrint(const char* message)
{
  int len = strlen_P(msg2);
  for (int i = 0; i < len; i++)
  {
    char myChar =  pgm_read_byte_near(message + i);
    Serial.print(myChar);
  }
  Serial.println();
}

I am not so much worried about the memory size as the sketch size. I do things like:

dlog ("Modem returned : "+ret);

(ret is a string)

It just really took me by surprised that it would increase the sketch size when I am reusing code. As in - a CALL is “bigger” than if+Serial.print - but I guess it could be…

dewitpj:
I am not so much worried about the memory size as the sketch size. I do things like:

dlog ("Modem returned : "+ret);

(ret is a string)

Exactly my point, you are chasing a mouse and running past the elephant. Removing String class saved in both but mainly 1500 bytes less of Program space.

look up strcat() if you want to concatenate without String class. Save the overheard and avoid the potential for fragmentation that may eventually cause issues... particularly if you are space starved.

char message[32] = "";
strcpy(message, msg2);
strcat(message, "returned string");// or any c string that fits in message!

That is why debug statements are usually implemented as macros.

BulldogLowell:
Exactly my point, you are chasing a mouse and running past the elephant. Removing String class saved in both but mainly 1500 bytes less of Program space.

look up strcat() if you want to concatenate without String class. Save the overheard and avoid the potential for fragmentation that may eventually cause issues... particularly if you are space starved.

char message[32] = "";

strcpy(message, msg2);
strcat(message, "returned string");// or any c string that fits in message!

Yeah - I can't do that - I use the string class for other things as well. I am happy with the elephant, it's the mouse that was the odd one out :wink:

dewitpj:
Yeah - I can't do that - I use the string class for other things as well. I am happy with the elephant, it's the mouse that was the odd one out :wink:

what String thing can you do that a string thing can't?

:wink:

aarg:
That is why debug statements are usually implemented as macros.

I will look into this, it might be the answer I am looking for - given that I do the macro's on my PC based C code...

(Urgh - this 5 min limit to posts is killing me - I will answer the rest as they are lifted)

dewitpj:
I will look into this, it might be the answer I am looking for - given that I do the macro's on my PC based C code...

(Urgh - this 5 min limit to posts is killing me - I will answer the rest as they are lifted)

In combination with preprocessor directives #IFDEF and #ENDIF I believe. It will omit your debug code entirely from the code when your debug variable is not defined. Actually, macros are optional with this method, it could be a function. You just have to make the function itself conditional.

aarg:
In combination with preprocessor directives #IFDEF and #ENDIF I believe. It will omit your debug code entirely from the code when your debug variable is not defined. Actually, macros are optional with this method, it could be a function. You just have to make the function itself conditional.

Yip, hence the #define DEBUG 1 at the start - if I change this to 0, the compiler removes all the code - quite handy IMHO :slight_smile:

If I had time I woud have tried this on a Linux platform with GCC, but I couldn’t be bother ATM :slight_smile:

I guess the next optimization to look into is like you say, omitting the function as a whole:

if (DEBUG)
{
dlog (“bla”);
}

vs

dlog (“bla”);

but then having the if (DEBUG) check in dlog, as per the sample code…

dewitpj:
Yip, hence the #define DEBUG 1 at the start - if I change this to 0, the compiler removes all the code - quite handy IMHO :slight_smile:

If I had time I woud have tried this on a Linux platform with GCC, but I couldn't be bother ATM :slight_smile:

I guess the next optimization to look into is like you say, omitting the function as a whole:

if (DEBUG)
{
dlog ("bla");
}

vs

dlog ("bla");

but then having the if (DEBUG) check in dlog, as per the sample code...

Wrong. That actually creates code. More like:

#IFDEF DEBUG
  dlog ("bla");
#ENDIF

aarg:
Wrong. That actually creates code. More like:

#IFDEF DEBUG

dlog ("bla");
#ENDIF

Not quite - the compiler knows that it's always false and removes the code :slight_smile:

For the time thou, the code looks clearner with the "dlog" option and since I am quite far from production, I might just keep it like that.

Anyways - thanks for the time, you have given me some food for thought.

Cheers !

void dlog (String msg)

In your initial example, using dlog()forces the compiler to call code to turn a FlashStingHelper variable into a String variable. When you just call Serial.println(), it handles the FlashStringHelper directly, and avoids that extra code.

  dlog (F("Debug 2"));
 124:   60 e7           ldi     r22, 0x70
 126:   70 e0           ldi     r23, 0x00   ;; Load pointer to flash
 128:   ce 01           movw    r24, r28
 12a:   01 96           adiw    r24, 0x01
 12c:   0e 94 9d 04     call    0x93a   ; <String::String(__FlashStringHelper const*)>   ;; convert to "String"
 130:   ce 01           movw    r24, r28
 132:   01 96           adiw    r24, 0x01
 134:   0e 94 7a 00     call    0xf4    ; <dlog(String)>  ;; call dlog() with the String.

Note that if DBEUG is a constant, the compiler will optimize away any if (DEBUG) { }, so that ends up taking zero space.

If I "correctly" convert dlog to use a flashStringHelper, the code size goes down to 2092 bytes, very slightly smaller than the non-dlog version (2096 bytes), which is what I'd expect.

My guess is that calling the function requires creating a String object from the string in flash memory, while passing that string to the Serial.println(...) function doesn't.

The disassembler is useful for exploring this kind of thing:
avr-objdump -d -S -j .text Program.elf >disam.txt

Program.elf is the compiled sketch. Use Sketch->Export Compiled Binary to get that.

avr-objdump can be found in ArduinoInstall\hardware\tools\avr\bin