Request for an explanation: secondary stack

:waving_hand: Hello, I am matklad! I am primarily a Zig/Rust person, but I’ve always been envious of Ada memory management, as it seems to have a super-power, not available in RAII or manually managed languages — returning variable size data without the use of the heap.

Is there some gears-level explanation of how that actually works? the closest I know is On the Ada Secondary Stack, and Allocation in the Real-World , but that doesn’t really look inside the box, only notices its properties.

As a specific example, the following program feels like magic to me:

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Demo is

   -- Define a type for an array of integers
   type Int_Array is array (Positive range <>) of Integer;

   -- Function to create an array of integers from 1 to N
   function Make_Array (N : Positive) return Int_Array is
      Result : Int_Array(1 .. N);
   begin
      for I in Result'Range loop
         Result(I) := I;
      end loop;
      return Result;
   end Make_Array;

   -- Function to choose between two arrays
   function Choose_Array (N1, N2 : Positive; Use_First : Boolean) return Int_Array is
      A1 : constant Int_Array := Make_Array(N1);
      A2 : constant Int_Array := Make_Array(N2);
   begin
      if Use_First then
         return A1;
      else
         return A2;
      end if;
   end Choose_Array;

begin
   -- Call Choose_Array from main and print its elements
   declare
      A : Int_Array := Choose_Array(3, 7, Use_First => False);
   begin
      for I in A'Range loop
         Put(A(I));
         Put(" ");
      end loop;
      New_Line;
   end;

end Demo;

We allocate two dynamically-sized arrays, return one of them based on runtime condition, and somehow that all works without heap memory :exploding_head: ? Indistinguishable from magic!

What are the exact runtime data structures and compiler lowerings that make this possible? How does this interact with SPARK?

Thanks!

2 Likes

Just a precursor: the Ada language intentionally leaves the implementation of returning variable-sized values up to the vendor. The secondary stack is just one way (GNAT uses this method).

I haven’t looked at recent versions of GNAT but when I did GNAT allocated a large space of RAM for it and they used a “mark release” method where the calling function set a mark on the sec stack and passed the current address to the function it called

The called function would then place the bounds of the return value along with the value itself. After the calling function was done with the returned value it would release the mark allowing that memory to be used by a higher scope later.

Note that for really really large allocations GNAT did fall back to heap allocation if the sec stack was too small. However for my embedded runtimes I forced it to non heap only and raised an exception if too much memory was requested

2 Likes

This was discussed on Reddit.

https://www.reddit.com/r/ada/comments/lwsedi/please_explain_secondary_stack_to_a_c_programmer/

1 Like