Does using 8 or 16bit variables save up RAM?

Status
Not open for further replies.

WS2812B

Member
It may be a stupid question:

I know that on 8bit AVRs RAM can be saved up by using uint8_t or similar variables if the value wont exceed the range.

My question is if using 8bit or 16bit variables will save up RAM on a Teensy 3.6 board. I am asking this because the OctoWS2811 library uses an IntArray to store 24bit RGBValues.
 
It's often a trade-off between RAM usage and speed, since 32bit aligned memory reads are faster. But in that special case, if I remember well, the 24 bit RGB data is packed to not to waste 8 bits.

Edit: You got already the answer, thus why do you re-ask? Why Int instead of 3 bytes for RGB?

If you need deeper understanding of the memory organisation in modern ARM Cortex M4 MCUs, you'd study the reference manual. Or, in a more empiric way, just write some test code with your different variable formats, compile it with the different optimization options and look at the verbose output of the compiler which will tell you how much RAM and flash memory are used.
 
I got the answer why it uses 32bit sections but I also asked the above question and got no answer. That's why I 're-ask'. Thank you.
 
If you declare something with an explicit length (uint8_t, uint16_t, etc.) it will take that many bytes in memory (i.e uint8_t will take 1 byte, uint16_t will take 2 bytes, etc.). Note, anything larger than 1 byte may be subject to padding, depending on the underlying architecture, and the ABI (application binary interface) that the compiler uses. Some processors will generate an exception if you use a 2/4/8 byte memory instruction and the address is not aligned properly. Other processors will execute the load/store more slowly if the address is unaligned. You can even get systems that some memory accesses can deal with unaligned data, and some can't (typically this would be uncached memory). IIRC, on the Teensy 3.2, there are logically two banks of memory, and unaligned accesses work everywhere except when the access crosses that boundary, but you only see this if you are explicitly converting char * pointers into int *.

For example, consider this example structure:

Code:
struct foo {
    uint8_t a;
    uint16_t b;
    uint8_t c;
    uint16_t d;
};

On many systems, there would be a 1 byte hole between the fields a' and 'b' and a 3 byte hole between 'c' and 'd'.

The ISO C/C++ standards demand that the compiler behave as if the types smaller than int be converted to int or unsigned int before doing arithmetic/logical operations on it. Some processors have 8/16/32/64-bit arithmetic operations, some only have 1 or 2 sets and the other sizes must be done by multiple instruction sequences. Typically processors will have memory operations that take a 1, 2, or 4 byte item and load it into a 32-bit or 64-bit register to do arithmetic/logical operations. It is rare for a processor to provide 3, 5, 6, or 7 byte memory operations (except for special string handling instructions).

Thus if you declare something to be uint8_t, it can save memory if you have a large array. When it is being processed in the registers, it is likely to be done in a 32-bit register (on the Teensy) using instructions that take 32-bit inputs and produce 32-bit outputs. It is then converted back to 8-bit when you store it to memory. If the value is used in another calculation before storing it to memory, the compiler may need to do a conversion from 32-bits down to 8-bits.

This storage to memory (or an in-register conversion) will eliminate the upper bits. So if you have an uint8_t value that has 255 in it, and you add 1 to the value, it will be 0 instead of 256, if you read the value.

For example, consider this case:
Code:
    uint8_t a = 250;
    // ...
    if (a + 10 > 251) {
        println "yes";
    else {
        println "no";
    }

This will print "yest" because the a is logically converted to int and 250 + 10 is 260, which is valid.

But if you wrote:
Code:
    uint8_t a = 250;
    // ...
    a += 10;
    // ...
    if (a > 251) {
        println "yes";
    else {
        println "no";
    }

It would print "no" because storing the value back into a truncates the value.
 
Last edited:
Every time you compile with Arduino, you should see a message like this:

Code:
Global variables use 12528 bytes (4%) of dynamic memory, leaving 249616 bytes for local variables.

If you are unsure about memory usage, you can try adding variables to your program and watch this number.

You should be aware of the padding issues MichaelMeissner mentioned. Sometimes the padding gives unexpected results, possibly allocating your variable in space that was previously "wasted" for padding, sometimes add the space for the new variable plus padded. Also you should know the compiler is very good about knowing if you never actually use the variable and not allocating it at all, such as cases where it is always a known value.

Also you should know local variables are allocated on a dynamically changing stack, so Arduino can only show you the usage for your global variables. Most Arduino code and libraries are designed to allocate larger data, like the OctoWS2811 buffers, as global variables.

But the point is Arduino gives you the actual memory usage info every time you compile. You can observe this info to get answers specific to your actual program.
 
Status
Not open for further replies.
Back
Top