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.