The Ghost in the Compiler: Why your Arduino code disappears (An Assembly Visual Proof)

Hi everyone, costycnc here.

Have you ever written a piece of C++ code that was logically perfect, but your Arduino completely ignored it? No errors during compilation, the IDE says "Done uploading", but the hardware simply behaves as if those lines of code do not exist.

The truth is: they don't exist.

The C++ compiler is obsessed with speed and memory optimization. If it thinks a variable or an operation is useless, it brutally deletes it from the final .hex file.

To prove this to you without the usual confusing C++ abstractions, I performed a simple reverse engineering experiment using pure AVR Assembly.

The Experiment: Using NOPs as Visual Anchors

I wrote a small sketch in the Arduino IDE. I used 10 NOP (No Operation) instructions as "markers" before and after my target code. A NOP takes exactly 1 clock cycle and outputs 0x0000 in machine code, making it very easy to spot in a disassembler.

Here is the C++ code I compiled:

void setup() {
  // 10 NOPs before (Visual Marker)
  __asm__ __volatile__ (
    "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t"
    "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t"
  );

  // The target code
  char ch = 'A';
  __asm__ __volatile__("" :: "r" (ch)); // Force the compiler to load 'ch'
  ch += 1; // Increment the character

  // 10 NOPs after (Visual Marker)
  __asm__ __volatile__ (
    "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t"
    "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t" "nop\n\t"
  );
}

void loop() {}

The Shocking Truth Under the Hood

I took the compiled .hex file and ran it through an AVR disassembler tool to see the actual hardware instructions generated for the ATmega328P.

Here is what the CPU actually executes between our NOP markers:

; --- 11 NOPs (The compiler shifted one up for pipeline optimization) ---
NOP
NOP
...
LDI R24, 65    ; Load immediate value 65 (ASCII for 'A') into Register 24

; --- 10 NOPs after ---
NOP
NOP
...

What did the compiler do?

Looking at the disassembly, we can discover two incredible things that explain all the confusion:

  1. The ASCII Conversion: The letter 'A' vanished. The microcontroller only understands numbers, so the compiler immediately converted it to 65 and generated a single LDI (Load Immediate) instruction.
  2. The Disappearing Code: Where is ch += 1;? It's completely gone. The compiler noticed that after adding 1 to ch, the variable is never used again in the sketch. It decided that executing the addition was a waste of Flash memory and clock cycles, so it erased it.

Why this creates confusion (and how to fix it)

This is why Assembly is so powerful: it removes the fog of C++. If you don't know Assembly, you might spend days debugging why a manual delay loop or an interrupt variable isn't working.

If you want to stop the compiler from playing tricks on your variables, you must use the volatile keyword:

volatile char ch = 'A';

This tiny keyword tells the compiler: "Do not optimize. Do not assume. Force the hardware to write and read this value from the RAM every single time."

I hope this visual approach helps you understand what is really happening inside the silicon!

Best regards,
costycnc

If any code in a sketch does not do anything that effects the operation of the sketch how would I notice that the compiler had optimised it away and why would it matter ?

Please post an example sketch that demonstrates that optimisation has caused a problem

Here is the exact visual proof from my disassembler tool using the manual loop example sketch.

If you look closely at the screenshot between the NOP markers, you can see exactly where the code was optimized away by the compiler:

  • At address L016B: The compiler generates a CALL 0x0070, which is the first digitalWrite(13, HIGH) function call.
  • At address L016E: Just three instructions later, it generates another CALL 0x0070, which is the second digitalWrite(13, LOW) function call.

Where is the loop?

The entire while(timeout < 20000) structure with its increments and comparisons is completely missing. There are no CPI (Compare Immediate), BRNE (Branch if Not Equal), or loop counters between the two function calls.

The compiler placed the "turn ON" and "turn OFF" commands almost back-to-back. Physically, the LED pin will toggle in a matter of nanoseconds, making it completely invisible to the human eye.

Without looking at the Assembly output like this, a beginner would be completely lost trying to debug why their logic "failed", when in reality, the compiler just optimized the loop into non-existence!

i don't think it's surprising that optimizers ignore useless code or possibly reorders instructions

i think the ultimate goal of a compiler is to read a requirements specification and generate cod that optimally implements that requirement in terms of speed or space.

i've seen checksum requirements in mobile phones specified as logic diagrams for implementation in code.

perhaps AI will generate code from flow charts, state machine, data flow ... specifications

compilers (parser generators) have been specified using BNF using yacc or bison

However, there is a massive gap between "what the programmer thinks they required" and "what the compiler actually understood". ... the discussion was about principiants, that with an avr compiler uploader tool directly from web, understand better c++ and can write also few lines of asm directly in tool , to understand better.I not did that ottimice is not good, i did that principiants not understand when compiler not return any error but code not working,and only asm can explain this!

that statement is hard to parse. i asked gmail's grammar editor for some help and it gave me:

My point was that beginners using a web-based AVR compiler and uploader tool can better understand C++ by writing a few lines of assembly directly in the tool. I am not suggesting that optimization is bad; rather, beginners often struggle when a compiler returns no errors, yet the code does not function as expected. In these cases, looking at the assembly is often the only way to explain what is happening.

exactly what i want did , but i use "my english" because many users acuse me because use ai to compound message!

It would be useful if you could provide a solution. And coding in assembly is not a solution :wink:

yes , i give a solution! use asm to understand "volatile". in c++ is hidden or eliminated!

Well, you told the compiler, that you want LED on and then off. The compiler fullfiled that.

You also said, you do not care about timeout value, as you did not used it anywhere and it is not volatile, so anything else could use it.

Do not complain, that your code is optimised, when you* explicitly asked compiler to optimise it.

*) or your tool of choice - Arduino IDE

... avr-gcc -c -g -Os ...

looks pretty explicite for me :slight_smile:

Really?
I don't see anything shocking about this.

I think... In order for such cases not to be a surprise for students, they need to study not assembler (which does not help to predict such situations), but the methods of the compiler.

Why optimalisation?

The compiler starts with loading Arduino.h, witch #includes more header files (I counted 45). This could result in a program far bigger than any Arduino board can hold.
A function that is not called? Remove.
A variable that is not used? Remove.
A variable that was read before and still is in the working registers? Don't read again.
A comparison may save a number of instructions when flipped.

Optimalisation is not nice-to-have but a necessity. Getting used to its behavior may take some time.

Be aware that some parts of your sketch end up in your board, some tell the compiler what to do or not to do.

off topic!

A couple of comments on your reply

  1. You know better than to post screenshots of code rather than using code tags
  2. How long should the user expect the LED to be on for ?
  3. How will the user know that it came on for a shorter period ?
  4. Could the user get an inkling of what is going on using Arduino functions ?

void setup()
{
    pinMode(13, OUTPUT);
    unsigned int timeOut = 0;
//    volatile unsigned int timeOut = 0;  //does this declaration make a difference ?
    digitalWrite(13, HIGH);
    unsigned long start = micros();
    while (timeOut < 32000)
    {
        timeOut++;
    }
    digitalWrite(13, LOW);
    unsigned long end = micros();
    Serial.begin(115200);
    Serial.println(end - start);
}

void loop()
{
}

  1. If the user knows enough to disassemble the program and interpret what they see then I suspect that they will already have a good idea what is going on

Like some other experienced contributors to the forum you find it difficult or even impossible to put yourself in the position of a beginner. You cannot forget what you already know and what seems obviou or easy to you is like black magic to a beginner

As it happens, I like your example and the explanation of what the assembled program is doing but then again I am not a beginner

My intention is not to force beginners to master Assembly syntax or become computer architects. I am simply trying to use a different tool where the C++ toolchain fails to explain what is happening.

I don't understand, if a variable is NOT used and it is not marked volatile the compiler will discard it. Been that way for a while.

t is obvious for someone who has studied computer science, but for a 'newborn' beginner, it is not obvious at all!

I don't know C++ either. So how is a beginner to understand.

Just suppose that every removal of a function (collected from 45 header files plus a sketch) generated a warning, think of what that would do to your screen. You'd never be able to find the important ones. I'd consider that a failure of a system.

i not refer at explain error ... but explain what happen!