Arduino Memory Best Practice
(Duplicate of this.)
In your example, neither definition of MAX_ARRAY_LEN will use program space nor RAM.  If you were to use the definition somewhere, it will certainly use program space in the instruction that uses that constant:
for (int i=0 i&lt;MAX_ARRAY_LEN, i++)
  Serial.print( my_array[i] );

In this case, a CPU instruction will load the constant 3 into a register.  So you can say that the 3 &quot;used&quot; a byte in the CPU instruction, and is part of the program size.  Most people ignore that, because a loading a variable into i uses both RAM and program space.
The primary advantage of using const instead of #define is that the former allows additional compiler type-checking.
C++ has introduced another keyword, constexpr, which tells the compiler that an entire expression can be evaluated at compile time and reduced to a single constant value.  Read all about it here.
To answer your comment, #define's are not stored anywhere.  They are a simple string substituion that happens before the compiler processes the C++ code.
When you build your sketch, the source files are processed in several phases.  First, there's a preprocessor phase that consumes all the lines that start with '#'.  The results of the preprocessing are sent to the C++ compiler.  The compiler will see all the included files inline, and all #define's will be substituted.
UPDATE
For example, #define XXX 3 will cause the preprocessor to replace all occurrences of XXX in the C++ file with 3.  The C++ compiler will never see XXX, it only sees 3.  You could do a search and replace with your editor, and the C++ compiler would not know the difference.  (Well, there are a few more tricks the preprocessor can do for you, but you only asked about defining a numeric constant.)  The generated code would be identical.
There is no &quot;global space&quot; for #define symbols.  There is a &quot;dictionary&quot; in the preprocessor that is used to perform the substitution, but that is ephemeral.
PROGMEM is not related to #define or const at all.  It is an attribute of a variable that indicates it is to be stored in FLASH, not RAM.  I think the key is that #define is just string substitution, but const is a C++ attribute of a C++ variable.  All of these are equivalent:
#define MAX_ARRAY_SIZE 3
const uint8_t maxArraySize = MAX_ARRAY_SIZE;
int array[ maxArraySize ] = { 1,2,3 };

#define FOO 1,2,3
int array[] = { FOO };
const uint8_t MAX_ARRAY_SIZE = (uint8_t) sizeof(array)/sizeof(array[0]);

#define MAX_ARRAY_SIZE goofyNameThatMeansNothing
const uint8_t MAX_ARRAY_SIZE = 3;
int array[ MAX_ARRAY_SIZE ] = { 1,2,3 };

#define BAR = { 1,2,
int array[] BAR,3};
#define MAX_ARRAY_SIZE sizeof(array)/sizeof(array[0])

I hope that last 2 goofy examples help illustrate what a #define is... string substitution.
The PROGMEM attribute is a separate thing.  It can only be added to C++ variables, and it now requires the variable to also be const.  Extending the above, you could do this:
#define MAX_ARRAY_SIZE 3
const uint8_t maxArraySize = MAX_ARRAY_SIZE;
const int array[ maxArraySize ] PROGMEM = { 1,2,3 }; // a flash array, not RAM

#define FOO 1,2,3
const int array[] PROGMEM = { FOO };
const uint8_t MAX_ARRAY_SIZE = (uint8_t) sizeof(array)/sizeof(array[0]);

#define MAX_ARRAY_SIZE goofyNameThatMeansNothing
const uint8_t MAX_ARRAY_SIZE = 3;
int array[ MAX_ARRAY_SIZE ] PROGMEM = { 1,2,3 };

#define BAR PROGMEM = { 1,2,
const int array[] BAR,3};
#define MAX_ARRAY_SIZE 14

Again, in that last goofy example I used PROGMEM in the #define BAR, but it's just some characters as far as the #define is concerned.  And the last define is just plain wrong, but it will compile.
Because there is no validation of a string substitution, it is usually better to use a const type var = value; that can be validated by the compiler, both when it is defined and again when var is used in some context.
UPDATE 2
You say,

with the const type comes the PROGMEM

Not really.  const is the &quot;read-only&quot; attribute, and the compiler enforces that.
PROGMEM tells the AVR compiler that, if storage is required for this variable, put it in FLASH memory (a special address range) instead of RAM (a different address range).  Because of the AVR architecture, you must access FLASH differently, with special instructions.  The avr.pgmspace.h include file has some routines (macros and inline assembler) for reading from those address spaces.
I emphasized the &quot;storage space required&quot;, because the compiler can optimize away the storage.  The literal 3 constant MAX_ARRAY_LEN does not need any storage space.  For this example program:
const int MAX_ARRAY_LEN PROGMEM = 3;

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

  Serial.println( MAX_ARRAY_LEN );
}

void loop() {}

This is the generated code:
  Serial.println( MAX_ARRAY_LEN );
156:    50 e0           ldi r21, 0x00   ; 0
158:    63 e0           ldi r22, 0x03   ; 3
15a:    70 e0           ldi r23, 0x00   ; 0
15c:    82 e2           ldi r24, 0x22   ; 34
15e:    92 e0           ldi r25, 0x02   ; 2
160:    f7 c2           rjmp    .+1518  ; 0x750 &lt;_ZN5Print7printlnEii&gt;

The compiler knows that storage is not required.  The value &quot;3&quot; is pushed directly onto the stack at program location 0x158.  It did not go to RAM nor FLASH (PROGMEM) to load a 3 into a register, and then push that register's value onto the stack.
The compiler knows the value, so it uses the &quot;immediate&quot; form of the instruction to push the 3.
However, for this program:
const int MAX_ARRAY_LEN PROGMEM = 3;

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

  Serial.println( (uint16_t) &amp;MAX_ARRAY_LEN, 16 );
}

void loop() {}

The following code is generated:
  e4:   03 00           .word   0x0003  ; ????
     .
     .
     .
  Serial.println( (uint16_t) &amp;MAX_ARRAY_LEN, 16 );
 158:   50 e0           ldi r21, 0x00   ; 0
 15a:   64 ee           ldi r22, 0xE4   ; 228
 15c:   70 e0           ldi r23, 0x00   ; 0
 15e:   82 e2           ldi r24, 0x22   ; 34
 160:   92 e0           ldi r25, 0x02   ; 2
 162:   b5 c2           rjmp    .+1386      ; 0x6ce &lt;_ZN5Print7printlnEji&gt;

Because I took the address of MAX_ARRAY_LEN, the compiler knew that it had to have storage... in order for it to have an address.  The compiler dutifully locates it in the program space, at 0xE4.  You can see that address being pushed onto the stack at 0x15A.
Without the PROGMEM attribute, the following code is generated:
  Serial.println( (uint16_t) &amp;MAX_ARRAY_LEN, 16 );
 156:   50 e0           ldi r21, 0x00   ; 0
 158:   60 e0           ldi r22, 0x00   ; 0
 15a:   72 e0           ldi r23, 0x02   ; 2
 15c:   84 e2           ldi r24, 0x24   ; 36
 15e:   92 e0           ldi r25, 0x02   ; 2
 160:   b5 c2           rjmp    .+1386      ; 0x6cc &lt;_ZN5Print7printlnEji&gt;

This is a RAM address, 0x200.  Long before setup, a 3 is copied from the program space into RAM.  It is using both program space and RAM.
But this is only because I took the address of MAX_ARRAY_LEN.  In almost all cases, you will be using these as scalar values.  The compiler will not allocate any storage space for them, and the PROGMEM attribute is irrelevant.
You have another question:

Is there any performance hits when reading from Flash vs SRAM?

Yes.  Reading from flash takes twice as many CPU cycles.  It's always a juggling act.  If you need to squeeze every cycle out, make sure your constants do not even need storage space (i.e., don't take their address).  And if it's a PROGMEM array or structure, consider using a cached value (i.e., loaded into a RAM array or structure).  Sometimes you don't have a choice.  If you are out of RAM, you will have to load them from PROGMEM every time you need them, with a small penalty.  RAM, SPEED, SIZE... pick two.