Trying to understand SRAM usage

So I’m trying to reduce the memory usage of my code and am playing around with PROGMEM. I wanted to first make sure I understood how Arduino uses SRAM, so I wrote a basic sketch to measure memory usage with various variable configurations:

First, the sketch with no variables at all:

//#include <avr/pgmspace.h>
#include <MemoryFree.h>

//char* text0[]={
//  "1.Water Change "};
//char* text1[]={
//  "1. Hour        ",
//  "2. Minute      ",
//  "3. Day         ",
//  "4. Month       ",
//  "5. Year        "};
//char* text2[]={
//  "1. Time        ",
//  "2. Date        ",
//  "3. Light Times ",
//  "4. Temps       ",
//  "5. ATO Level   ",
//  "6. Dosing Pumps",
//  "7. Return      "};

//char buffer[17];  

void setup()			  
{
  Serial.begin(9600);
}

void loop()			  
{
  Serial.println();
  Serial.print(("loop freeMemory()="));
  Serial.println(freeMemory());
  func0();
  func1();
  func2();
  while(1);
}

void func0(){
  Serial.println();//text0[0]);
  Serial.print(("func0 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );

}

void func1(){
  Serial.println();//text1[0]);
  Serial.print(("func1 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );
}

void func2(){
  Serial.println();//text2[0]);
  Serial.print(("func2 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );
}

Then I simply un-commented the char array definitions without actually calling them in the functions:

//#include <avr/pgmspace.h>
#include <MemoryFree.h>

char* text0[]={      // <------- no longer commented out
  "1.Water Change "};
char* text1[]={
  "1. Hour        ",
  "2. Minute      ",
  "3. Day         ",
  "4. Month       ",
  "5. Year        "};
char* text2[]={
  "1. Time        ",
  "2. Date        ",
  "3. Light Times ",
  "4. Temps       ",
  "5. ATO Level   ",
  "6. Dosing Pumps",
  "7. Return      "};

//char buffer[17];  

void setup()			  
{
  Serial.begin(9600);
}

void loop()			  
{
  Serial.println();
  Serial.print(("loop freeMemory()="));
  Serial.println(freeMemory());
  func0();
  func1();
  func2();
  while(1);
}

void func0(){
  Serial.println();//text0[0]);
  Serial.print(("func0 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );

}

void func1(){
  Serial.println();//text1[0]);
  Serial.print(("func1 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );
}

void func2(){
  Serial.println();//text2[0]);
  Serial.print(("func2 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );
}

Then I actually used the arrays in the functions:

//#include <avr/pgmspace.h>
#include <MemoryFree.h>

char* text0[]={
  "1.Water Change "};
char* text1[]={
  "1. Hour        ",
  "2. Minute      ",
  "3. Day         ",
  "4. Month       ",
  "5. Year        "};
char* text2[]={
  "1. Time        ",
  "2. Date        ",
  "3. Light Times ",
  "4. Temps       ",
  "5. ATO Level   ",
  "6. Dosing Pumps",
  "7. Return      "};

//char buffer[17];  

void setup()			  
{
  Serial.begin(9600);
}

void loop()			  
{
  Serial.println();
  Serial.print(("loop freeMemory()="));
  Serial.println(freeMemory());
  func0();
  func1();
  func2();
  while(1);
}

void func0(){
  Serial.println(text0[0]);  // <------- no longer commented out
  Serial.print(("func0 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );

}

void func1(){
  Serial.println(text1[0]);
  Serial.print(("func1 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );
}

void func2(){
  Serial.println(text2[0]);
  Serial.print(("func2 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );
}

Finally, I moved the array definitions to the functions instead of defining them globally:

//#include <avr/pgmspace.h>
#include <MemoryFree.h>

//char buffer[17];  

void setup()			  
{
  Serial.begin(9600);
}

void loop()			  
{
  Serial.println();
  Serial.print(("loop freeMemory()="));
  Serial.println(freeMemory());
  func0();
  func1();
  func2();
  while(1);
}

void func0(){
  char* text0[]={
  "1.Water Change "};
  Serial.println(text0[0]);
  Serial.print(("func0 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );

}

void func1(){
  char* text1[]={
  "1. Hour        ",
  "2. Minute      ",
  "3. Day         ",
  "4. Month       ",
  "5. Year        "};
  Serial.println(text1[0]);
  Serial.print(("func1 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );
}

void func2(){
  char* text2[]={
  "1. Time        ",
  "2. Date        ",
  "3. Light Times ",
  "4. Temps       ",
  "5. ATO Level   ",
  "6. Dosing Pumps",
  "7. Return      "};
  Serial.println(text2[0]);
  Serial.print(("func2 freeMemory()="));
  Serial.println(freeMemory());
//  strcpy_P(buffer, (char*)pgm_read_word(&(text0[0]))); 
//  Serial.println( buffer );
}

Below are the results I got from the freeMemory function:
No Definitions: Compile size: 2690, Free mem in loop: 1757, free men in fxns: 1753
Defined, not used: Compile size 2898, loop: 1549, fxns: 1545
Defined & used: Compile size 2986, loop: 1523, Fxns: 1519
Defined in fxns: compile size 2788, loop 1709, fxns: 1705

Compile
size
Free Mem
in Loop
Free mem
in Fxns
No Variables 2690 1757 1753
Defined, not
used
2898 1549 1545
Defined gobally 2986 1523 1519
Defined locally
in fxns
2788 1709 1705

So my first question is why is more memory used (both at compile and SRAM) when the variables are defined but not used? I thought the compiler would simply optimize out the unused variables. The compile and free RAM sizes are different from both the code with no variables and the code in which they are actually used.

Second, I was taught that when variables are locally defined within a function in C that the memory was allocated when the function is called, then released when the function ends. When I do this in the last sketch, the free memory does increase, but it is the same when each function is called, despite the fact that the arrays are different sizes.

Am I missing something, or am I simply using the freeMemory function incorrectly?

I think the compiler is constrained from optimizing away globally defined variables as they are (theoretically) visible to other compilation units linked to the sketch later - the compiler has no knowledge of the sketch compilation model, its just C / C++. Perhaps some compiler flags could be changed to affect this.

Anyway all variables are in RAM unless you put them in Flash using PROGMEM (and access them explicitly from Flash). Stack-allocated variables only take up space when they are active, but note that string constants are effectively always global in extent.

(Note "extent" has a technical meaning here, the lifetime that a value exists - compare "scope" which is the subset of program text that can refer to a variable. String constants used locally in a function have global extent but local scope).

Hmm - that still doesn't explain the difference in memory usage between the sketches where they are defined but unused vs. defined and used. Also, why does the compile size and memory usage go down when they are defined within the functions?

You say all variables are in RAM unless specified with PROGMEM; Aruduino doesn't dynamically allocate space on the heap/stack? That seems to be a very inefficient use of memory in a device that's already memory-constrained.

I tried compiling the code below, leaving the various variables undefined, defined but unused, and then used and the compile size was the same unless they were actually used, meaning the compiler does do some checking/optimizing.

char *text[] = {"test"};
char *text2[] = {"hello"};
//char text[] = "test";
//char text2[] = "hello";
//long x = 2;
//long y = 3;

void setup(){
}

void loop(){
//  x=3;
//  y=4;
//  text[0] = 'a';
//  text2[0] = 'b';
  *text[0] = 'a';
  *text2[0] = 'b';
  while(1);
}

// x/y undefined; 466, defined 466, x used 494, both used 522
// text undefined 466, defined 466, text used 478, 490
// *text undefined 466, defined 466, *text used 492, 506

Ok, so there seems to be something about pointers. If a pointer is declared but unused, it seems to still take up memory. Perhaps because it's a pointer the compiler can't verify that the data is not used and so does not optimize it out...