On an Uno, I get 1832 bytes free with all things present, and 1820 bytes free with only one structure. So: observation duplicated!
I suspect you're still running into "unexpected compiler optimization." Let's see - we can examine the binary with various tools that are included in the arduino download.
First, we can look at the compile time memory allocation with the "nm" utility:
All structures:
BillW-MacOSX-2<10004> avr-nm -nSC *elf | grep " [bBdD] "
00800100 D __data_start
00800100 00000002 D __malloc_heap_end
00800102 00000002 D __malloc_heap_start
00800104 00000002 D __malloc_margin
00800106 00000010 d vtable for HardwareSerial
00800124 B __bss_start
00800124 D __data_end
00800124 D _edata
00800124 00000001 b timer0_fract
00800125 00000004 b timer0_millis
00800129 00000004 b timer0_overflow_count
0080012d 0000009d b Serial
008001ca 00000002 B __brkval
008001cc 00000002 B __flp
Only one Structure:
BillW-MacOSX-2<10005> avr-nm -nSC *elf | grep " [bBdD] "
00800100 D __data_start
00800100 00000002 D __malloc_heap_end
00800102 00000002 D __malloc_heap_start
00800104 00000002 D __malloc_margin
00800106 00000010 d vtable for HardwareSerial
00800124 B __bss_start
00800124 D __data_end
00800124 D _edata
00800124 00000001 b timer0_fract
00800125 00000004 b timer0_millis
00800129 00000004 b timer0_overflow_count
0080012d 0000009d b Serial
008001ca 00000002 B __brkval
008001cc 00000002 B __flp
they're identical. Also, there isn't any mention of any of the ms* data structures in either case! So, the compiler seems to be optimizing away the structures. We can confirm by looking at the code produced with avr-objdump. Here's the code where it converts ms.ii to a string, from the Single Structure version:
return __itoa_ncheck (__val, __s, __radix);
6ee: 4a e0 ldi r20, 0x0A ; 10 (RADIX)
6f0: b8 01 movw r22, r16
6f2: 8b e0 ldi r24, 0x0B ; 11 (constant 11)
6f4: 90 e0 ldi r25, 0x00 ; 0
6f6: 0e 94 1e 06 call 0xc3c ; 0xc3c <__itoa_ncheck>
}
#endif
String & String::operator = (const char *cstr)
{
if (cstr) copy(cstr, strlen(cstr));
So it has managed to figure out that ms.ii is 11, and it doesn't need the reset of the structure.
So why is there more free memory in the version with more than one structure?
One of the things I noticed during the above analysis is that the compiler has gone and converted a lot of the function calls to blocks of inline code. After all, they're only used once, so it's smaller to leave out the call instruction (and perhaps some argument handling). Functions like freeMemory() (which is short, anyway), and the String functions (including the internal String functions like String::reserve()) just don't wind up in the final binary as functions:
BillW-MacOSX-2<10016> avr-nm -nSC *elf | grep freeMemory
BillW-MacOSX-2<10017> avr-nm -nSC *elf | grep println
0000031a 00000104 t Print::println(int, int) [clone .constprop.8] (println does show up. Big, used twice.)
BillW-MacOSX-2<10018> avr-nm -nSC *elf | grep String
BillW-MacOSX-2<10019>
Now, this turns out to be particularly interesting because the freeMemory function allocates a temporary value on the stack - I don't think it's going to work right if it gets inlined; its result will be based on the stack frame where the temp actually gets allocated, rather that the stack frame at the time function is called.
There's a "noinline" attribute that you can use; I applied it to freeMemory(), and indeed the values reported do change! They're still bigger for the case with multiple structures, though...
At this point... I'm sorta bored; presumably the "many" example uses less memory because when the "single" code inlines more functions, it needs more stack space for the local variables used by those functions, which all get combined, but going through the code to prove that seems more trouble than it's worth. Can't we leave it at "trying to judge allocation behavior from very small programs is difficult"?
Here's a version crafted to make sure that the ms structures are actually allocated. Uses all of them, dumps all of all of them to a "volatile" port that the compiler MUST do... It behaves as expected - uses more RAM when there are more structures:
#include "MemoryFree.h"
typedef struct MyStruct
{
int ii;
char c[10];
};
#define ONLY1 1
const MyStruct ms = { 11, "123456789" };
#if ONLY1 == 0
const MyStruct ms2 = { 10, "234567890" };
const MyStruct ms3 = { 13, "234567890" };
const MyStruct ms4 = { 12, "234567890" };
const MyStruct ms5 = { 12, "234567890" };
const MyStruct ms6 = { 12, "234567890" };
const MyStruct ms7 = { 17, "234567890" };
#endif
void setup()
{
Serial.begin(115200);
Serial.println(freeMemory());
}
void loop()
{
delay(1000);
dumpstr(&ms);
#if ONLY1 == 0
dumpstr(&ms2);
dumpstr(&ms3);
dumpstr(&ms4);
dumpstr(&ms5);
dumpstr(&ms6);
dumpstr(&ms7);
#endif
Serial.print("Freemem: ");
Serial.println(freeMemory());
}
void dumpstr(MyStruct *m)
{
PORTB = m->ii;
for (byte i = 0; i < sizeof m->c; i++) {
PORTB = m->c[i];
}
}