Gem #95: Dynamic Stack Analysis in GNAT

by Quentin Ochem —AdaCore

Let's get started...

Determining how much stack space should be allocated to tasks is a common memory-management problem. In the absence of tool support, often the only information that developers have is the output EXCEPTION_STACK_OVERFLOW when their program crashes. GNAT offers two basic ways for users to get information on a program's stack usage -- statically or dynamically. This Gem addresses how to obtain data on dynamic stack usage. Measurement of static stack usage will be covered in a later Gem.

Computing the stack size at task termination

Let's start with a simple program that has a task whose stack size is determined at run time:

procedure Main is

   task T is
      entry E (Size : Integer);
   end T;

   task body T is
   begin
      accept E (Size : Integer) do
         declare
            V : array (1 .. Size) of Integer := (others => 0);
         begin
            null;
         end;
      end E;
   end T;

begin
   T.E (500_000);
end Main;

This program works fine, but what are its stack requirements? Is there a possibility that by adding new code which may consume additional stack, we'll hit the roof? Let's find out by compiling this with stack instrumentation:

gnatmake main.adb -bargs -u10

The "-bargs -u10" switch causes "-u10" to be passed to the GNAT binder, which will allow up to ten tasks to be instrumented and will output their stack usage upon program completion.

Compiled this way, the program outputs the following information:

  Index  | Task Name | Stack Size |      Stack usage 
       1 | t         |    2097152 | 2008872 +/- 8188

This means that out of the 2,097,152 bytes that are available for the task's stack, 2,008,872 are currently used by the program.

Adjusting the stack size

Our stack seems quite full here, and it's probably reasonable to increase its size to be on the safe side and to avoid potential exceptions when the program is extended. This can be done easily by using a pragma Storage_Size:

task T is
   pragma Storage_Size (3_000_000);
   entry E (Size : Integer);
end T;

Compiling the same program with these changes results in these numbers:

  Index  | Task Name | Stack Size |      Stack usage 
       1 | t         |    3000000 | 2008872 +/- 8188

This is much more reasonable.

Computing the stack size at run time

We're now going to create a new version of the task that can be called multiple times. Since this task is going to live longer, and do several things for different clients, we would like to be able to probe the task at different times, namely each time the entry is called. The run-time package GNAT.Task_Stack_Usage provides the means of instrumenting the task. Let's modify the task body as follows:

task T is
   pragma Storage_Size (3_000_000);
   entry E (Size : Integer; Name : String);
end T;

task body T is
begin
   loop
      accept E (Size : Integer; Name : String) do
         declare
            V : array (1 .. Size) of Integer := (others => 0);
         begin
            Put_Line ("MAX USAGE OF T AFTER " & Name & ":"
                        & Natural'Image (GNAT.Task_Stack_Usage.Get_Current_Task_Usage.Value));
         end;
      end E;
   end loop;
end T;

Note the call to Get_Current_Task_Usage, which computes the amount of stack consumed so far after each call to E. Let's now call this entry several times:

   T3.E (5_000, "OP 1");
   T3.E (100_000, "OP 2");
   T3.E (20_000, "OP 3");
   T3.E (800_000, "OP 4");

This will output:

MAX USAGE OF T AFTER OP 1: 29392
MAX USAGE OF T AFTER OP 2: 409392
MAX USAGE OF T AFTER OP 3: 411204
raised STORAGE_ERROR : EXCEPTION_STACK_OVERFLOW

Observe that the size of the stack is computed after each call, except for the last call, which results in an exception. Also note an interesting side effect: "OP 3" should take less stack space than "OP 2", so we would normally expect the number to be the same. What's happening is that in "OP 2", the string "MAX USAGE OF T AFTER OP 2: 409392" is computed first, and then Put_Line is called, which itself consumes some stack, up to the level of 411204 bytes. So 411204 is actually the maximum amount of stack space used by OP 2, even though the value displayed is less.


About the Author

Quentin Ochem has a software engineering background, specialized in software development for critical applications. He has over 10 years of experience in Ada development. He works today as a technical account manager for AdaCore, following projects related to avionics, railroad, space and defense industries. He also teaches the avionics standard DO-178B course at the EPITA University in Paris.