4k of prog space freed up when initializing an int

My sketch was taking up 99% of available program space (99% or 30720 bytes on a NANO), so I started to optimize more of the code.

I have a function outside the main loop that receives string commands from a BLE module.

This function processes these commands to do certain things in a large case statement. When making some small changes, all of a sudden, the code space dropped by over 4k bytes down to 83%.

I tracked it down to one single statement at the top of this function where I was declaring a simple counter variable:

void  ProcessMsg(String msg) {
     unsigned int cnt;

switch(cmd-0) {
  ....
  case 'g':     
    cnt=0;
    ...
    break;
   ....
  }
}

What I changed was that I moved that int declaration to inside the one case block that used the 'cnt' counter to make it more readable. Inside the case statement, I did have a "cnt=0;" statement to initialize the variable, so I combined those two statements into this one statement inside the case block:

switch(cmd-0) {
  case 'g':     
    unsigned int cnt=0;
    ...
    break;
...
}

As soon as I did that, the code space droped by 4,466 bytes (83% of total space available). Literally, just by adding "= 0" frees up 4k of space.

If I leave the int declaration inside the case block and do not initialize it with a value, it does not reduce the code space (still @ 99%)

switch(cmd-0) {
  case 'g':     
    unsigned int cnt;
    cnt=0;
    ...
    break;
...
}

And if I move that line back outside the case statement and initialize it with a value, the code space doesn't change either.

void  ProcessMsg(String msg) {
     unsigned int cnt=0;
....
}

The program code space is only reduced by 16% when this statement is both inside the case block and initialized.

Why?

(I don't want to post my entire sketch, it's very long).

Which Arduino, exactly, are you using?

If it is the original Arduino Nano, do not use Strings. They cause unpredictable program crashes and memory problems. Use C-strings (zero terminated character arrays) instead.

Note: it is almost impossible to diagnose a problem like yours without seeing ALL the code.

jremington: Which Arduino, exactly, are you using?

RobotDyn Nano V3 (CH340G version)

https://robotdyn.com/nano-v3-atmega-328-usb-ttl-ch340g-micro-usb.html

jremington: If it is the original Arduino Nano, do not use Strings. They cause unpredictable program crashes and memory problems. Use C-strings (zero terminated character arrays) instead.

But this isn't a program crash or a memory problem. This is a compiler problem. It's not even running on the Nano at this point, I haven't even uploaded it. The code runs fine, BTW under both scenarios.

Note: it is almost impossible to diagnose a problem like yours without seeing ALL the code.

No, it is not a compiler problem. It is a user-created problem. It is possible that some rearrangement you made caused the compiler to (correctly) optimize away a bunch of "dead code".

jremington: No, it is not a compiler problem. It is a user-created problem.

No need to be snarky about it.

I was just wondering if this has been seen before and is a known issue.

I assure you that the chance of discovering a "compiler problem" is essentially zero. Millions of people use this compiler every single day. The known problems are few and carefully documented.

It is much, much safer to assume that you made some error, rather than blame the compiler.

jremington: It is possible that some rearrangement you made caused the compiler to (correctly) optimize away a bunch of "dead code".

Literally the only change I make is "=0" on the declaration line and it uses up 4k more memory. I can switch back and forth between 83% and 99% by adding "=0" to that line. No other changes are made.

jremington: I assure you that the chance of discovering a "compiler problem" is essentially zero. Millions of people use this compiler every single day. The known problems are few and carefully documented.

That's fine, and I totally understand that. But what could cause a scenario like this? As millions of people use this compiler every single day, isn't it just a little surprising that NOBODY has seen something like this before.

I'm not asking for a solution, as there's actually no problem to solve. I'm trying to figure out why the compiler would allocate 4k+ more space when initializing a variable vs. not initializing it, and only inside a case block.

Still, I don't see the need to be so snarky about someone simply asking a question.

You haven't posted ALL your code, so no one has a clue what the real problem is.

But what could cause a scenario like this?

From reply #3:

It is possible that some rearrangement you made caused the compiler to (correctly) optimize away a bunch of "dead code".

"dead" = unused

Please explain how it is possible that code "runs fine", when you have not uploaded it.

It's not even running on the Nano at this point, I haven't even uploaded it. The code runs fine, BTW under both scenarios.

jremington: It is possible that some rearrangement you made caused the compiler to (correctly) optimize away a bunch of "dead code".

Yeah, I get that. But first, there is no dead code.

Second, assuming there is dead code how can simply adding a "=0" to a variable declaration optimize out 4k of said dead code?

switch(cmd-0) {
  case 'g':     
    unsigned int cnt=0;
    ...
    break;
...
}

Unless g is the last case, your code is not legal.
If you turn up compiler warning, it will tell you that.

What specifically is the cnt variable used for? declaring variables inside switch..case statements is highly problematic to begin with, and without seeing how the variable is used its impossible to make any useful comments on why the compiler is behaving as it does.

arduino_new:

switch(cmd-0) {

case ‘g’:    
   unsigned int cnt=0;
   …
   break;

}



Unless *g* is the last case, your code is not legal.
If you turn up compiler warning, it will tell you that.

Thanks for your reply. ‘g’ is not the last case, and I’m not getting any compiler warnings to that effect. Can you please elaborate what’s not legal?

For the switch statement in my code, I’m following the syntax on this page: https://www.arduino.cc/en/Tutorial/switchCase :

switch (range) {
    case 0:    // your hand is on the sensor
      Serial.println("dark");
      break;
    case 1:    // your hand is close to the sensor
      Serial.println("dim");
      break;
    case 2:    // your hand is a few inches from the sensor
      Serial.println("medium");
      break;
    case 3:    // your hand is nowhere near the sensor
      Serial.println("bright");
      break;
  }

david_2018:
What specifically is the cnt variable used for? declaring variables inside switch…case statements is highly problematic to begin with, and without seeing how the variable is used its impossible to make any useful comments on why the compiler is behaving as it does.

Just a simple counter. I’m dumping 100 lines of raw GPS data stream:

    while (cnt < 100) {
      if (GPSSerial.available() > 0) {
        char c = GPSSerial.read();
        if (c == '

) cnt++; // counts the number of GPS lines printed
        if (c == ’


 and cnt % 10 == 0) {  // every ten lines print an error summary
          Serial.println();
          Serial.print(F("Count: "));
          Serial.print(cnt);
          Serial.print(F("\t FailCS:"));
          Serial.println((float)gps.failedChecksum());
        }
        Serial.write(c);
      }
    }

Ok, I'm now getting a warning " crosses initialization of 'unsigned int cnt' unsigned int cnt=0;"

If I put the "unsigned int cnt=0;" declaration outside of the case statement, it uses up 4k more memory.

If I add {} blocks around each case statement to create scope, and put the "unsigned int cnt=0;" inside the case block {}, it adds 4k memory.

Only when I don't have {} scope around the case block and put the declaration inside the case block (without scoping it), then I get 4k memory savings, but compiler warnings.

Code still runs fine either way, though.

So I'm still investigating. Stay tuned.

arduino_new: switch(cmd-0) {  case 'g':        unsigned int cnt=0;    ...    break; ... }

Unless g is the last case, your code is not legal. If you turn up compiler warning, it will tell you that.

Ok, thanks to @arduino_new, this allowed me to better locate the problem. This was in fact the crux of the case. Thank you.

It now allows me to better formulate the question.

Because the case statement wasn't the last statement in the switch, the compiler would not compile any case block code below the 'g' case where I have the unsigned int declared.

Since this particular case statement was near the bottom of the switch, most other cases worked in testing. I just never tested the few cases below the 'g' case, leading me to believe the code was still working. It wasn't.

I moved the 'g' block to the top of the switch and sure enough, 'g' was the only case that worked.

But this leads me to my followup question. I now have two distinct cases, one which generates lots of compiler errors and one that doesn't. The two cases are below:

This code generates lots of compiler warnings and ignores any case code below the first 'g' case (i.e. only 'g' works):

switch(cmd) {
    case 'g': // g= gps dump     
    unsigned int cnt=0;
    .......
    break;
....
}

This nearly identical code generates zero compiler errors or warnings and compiles code for all cases below the first 'g' case (i.e. all cases work)

switch(cmd) {
    case 'g': // g= gps dump     
      unsigned int cnt;
      cnt=0;
      .......
    break;
....
}

So now I better understand the problem, why would initializing the unsigned int cause the errors and ignored code, and not initializing it in the declaration generate no errors and perfectly working code?

Thanks.

switch(cmd) {
    case 'g': // g= gps dump     
        {
            unsigned int cnt=0;
            .......
        }
        break;
    ....
}

RayLivingston: switch(cmd) {    case 'g': // g= gps dump            {            unsigned int cnt=0;            .......        }        break;    .... }

Thanks, yes, adding the scope will prevent the compiler from ignoring the rest of the case blocks when initializing the variable, but the question is why does not initializing the variable (vs. initializing it) make the code compile without warnings or errors and without ignoring the rest of the case blocks? Whereas if I do initialize the variable, the code breaks?

You won't get any good answers unless you post your entire sketch, as previously requested.

Here's the most simple example of this.

In "case 1:" below, if the variable declaration is "int cnt;" the code compiles and works perfectly without warnings or errors.

If that one line is changed to "int cnt=0;" the compiler throws the warnings and completely ignores all case blocks below case #1, it still compiles and runs, although the binary is 128 bytes smaller.

I'm not asking how to fix it, as that we already know.

What I find curious is why does initializing the cnt variable cause the code to break, when not initializing it doesn't? If there's a problem with scoping variables inside a case statement (which we know there is), shouldn't both scenarios break the code?

int cmd;

void setup() {
  randomSeed(analogRead(0));  
  while (!Serial);
  Serial.begin(115200);        
}
void loop() {
  cmd=random(1, 6);
  Serial.print("Command = ");
  Serial.print(cmd);
  switch (cmd) {
    case 1:
      int cnt;  // change to:  int cnt=0; 
      cnt++;
      Serial.print(" Case 1 Executed");
      break;
    case 2:       
      Serial.print(" Case 2 Executed");
    break;
    case 3: 
      Serial.print(" Case 3 Executed");
    break;
    case 4: 
      Serial.print(" Case 4 Executed");
      break;
    case 5: 
      Serial.print(" Case 5 Executed");
    break;
  }
  Serial.println();
  delay(100);
}

Works fine for me, using v1.8.9, compiled for Nano.

From the C++ standard:

It is possible to transfer into a block, but not in a way that bypasses declarations with initialization. A program that jumps from a point where a local variable with automatic storage duration is not in scope to a point where it is in scope is ill-formed unless the variable has POD type (3.9) and is declared without an initializer.

Basically, once you get into ill-formed / undefined behaviour territory, the compiler can do anything at all. It doesn't have to produce anything, and if it does, what it produces doesn't have to make sense. That's why the code with the initializer uses less memory - the compiler basically reasoned "Skipping over initialization is not allowed, therefore I can assume it never happens, therefore all this code is dead and can be removed."

I'm actually surprised that the code compiles at all - I thought ill-firmed programs were required to produce a diagnostic.