Menu

Should I use global or local variables?

Many Arduino tutorials use global variables without really telling why. And since there's no explanation, those snippets contribute to spreading bad coding practices. Here's one of them.

Let's first set the context: since Harduino is a project mostly related to the 8-bit AVR architecture, this article focuses on the latter, mostly. However the generalizations mentioned here do apply to any other — and not limited to 8-bit — architecture although the reader should be wise enough to assess to what extent. The wary developer might argue this is mitigated by compiler optimizations, which could, in some circumstances, make the use of global variables completely irrelevant. However one should understand the consequences of using global variables prior to relying upon optimizations to alleviate them.

So comes the question: why is it that important to not use global variables and why should I care?

The answer is simple: using RAM (mind stack is RAM) on AVR microcontrollers generally is a costly operation as it takes at least 3 instructions to update a variable, i.e.

  • load into some register from RAM,
  • change the register,
  • store back to RAM.

Instead, only one is required when only registers are involved. Here's for instance how an instruction such as a1++ could look like — let's say a1 is a 16-bit integer:

    80 91 02 01     lds     r24, 0x0102     ; 0x<a1>
    90 91 03 01     lds     r25, 0x0103     ; 0x<a1+0x1>
    01 96           adiw    r24, 0x01       ; 1
    90 93 03 01     sts     0x0103, r25     ; 0x<a1+0x1>
    80 93 02 01     sts     0x0102, r24     ; 0x<a1>

First the variable is loaded in a register pair, r24 and r25, then incremented and stored back into RAM. Note that it takes only one instruction to increment the register pair. Also note that it takes as many assembly instructions to load/store a variable as the variable size in bytes on an 8-bit architecture such as found on an Arduino Uno/Nano (ATmega328) Micro (ATmega32U4) and many others.

It is therefore important to minimize, as much as possible, the opportunities to let the compiler resort to RAM storage. How? Using local variables. This might sound counter intuitive as one may object that local variables are placed in the stack, right?

Well, not always.

8-bit AVR microcontrollers have 32 registers. That in and on itself greatly helps reduce RAM usage by using registers instead of the stack for local variables... provided the compiler does a decent job enough at optimizing the compiled code, of course.

If you have only global variables, your program might be at least 3 times slower to access those variables. And if you have too many local variables, the compiler will be forced to use the stack instead of registers, which can be even more costly than a mere variable located in RAM. Here's for instance how a stack frame is implemented on the AVR platform:

   cd b7       in      r28, SPL        ; Load SP's LSB
   de b7       in      r29, SPH        ; ... then the MSB
   a5 97       sbiw    r28, 0x24       ; Make room for 36 bytes
   0f b6       in      r0, SREG        ; We'll need that to restore the
   f8 94       cli                     ; previous interrupt flag state
   de bf       out     SPH, r29        ; Put back SP's new MSB
   0f be       out     SREG, r0        ; ... oh and restore interrupt flags
   cd bf       out     SPL, r28        ; Now put back SP's LSB

... for a total of 8 instructions! Fortunately compilers are smart. Here's how avr-gcc makes room for 2 bytes:

 00 d0           rcall    .+0        ; SP <-- SP-2

So what?

One of the benefits of local variables is you're telling the compiler that the related storage space is disposable and it's not required to dedicate registers to those values. And since AVR has a lot of registers, that makes a lot of opportunities to use them instead of the stack. As a consequence, your program runs faster and is less resource hungry, which is critical on a microcontroller that only has two thousand bytes of RAM for you to play with (see the ATmega328 documentation).

Here's a couple of hints.

  • Use local variables (you know that by now).
  • Make the scope of your local variables as small as possible.
  • Make your variable const if they don't change in their scope (the above makes it easier).
  • Don't use a local variable if it's only used by the next instruction and never afterwards.
  • Split functional blocks into functions.
  • Make your functions as small as possible.
  • Use global variables only if inevitable.
  • Don't count solely on optimizations to optimize your variables away.
  • Don't sacrifice performance over readability, the latter of which is subject to individual perception anyway.

These will be discussed in upcoming articles.

Posted by Vince C. 2021-08-21 Labels: global vs local variables

Log in to post a comment.

Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.