3. The Primary and Secondary Stacks¶
This chapter covers the primary and secondary stacks, explaining their purposes and how to control their sizes.
3.1. The Primary Stack¶
In GNAT, each user-defined task (if any) has a dedicated primary stack, as does the language-defined “environment task” that ultimately calls the main subprogram.
At run time, this primary stack is used by the compiler-generated code for several purposes, including representing subprogram calls. Each subprogram call typically represents a new entry on the stack, popped when the call returns. In addition, the stack is used for subprogram calls to pass data for the parameters, including the results of calls to functions that return values of constrained types. The stack is used for other purposes as well.
3.1.1. The Default Primary Stack Size¶
Applications vary widely, including, for example, the depth to which subprograms call each other, so the required size of each primary stack is application dependent. Therefore, the default stack sizes provided by the implementation for the environment task and for application-defined tasks may or may not suffice.
In GNAT the same default value is applied to both the environment task and to application-defined tasks. The default primary stack size is determined by the underlying operating system, or in the case of bareboard targets by the specific run-time library.
For bareboard targets, the default stack size varies across specific platforms because different classes of target boards will have different ranges of RAM available. For example, the Stellaris LM3S board, with a Cortex-M3 MCU, has a default stack size of 2K bytes in GNAT, whereas the STM32F4 targets have Cortex-M4 MCUs and a 4K default stack size.
You can determine the default primary stack size for a given bareboard
run-time library by examining the linker scripts used to lay out memory. These
files are located in the run-time library’s board support package (BSP) files.
On the ARM STM32 targets, for example, in
common-RAM.ld the following
declaration appears near the top
_DEFAULT_STACK_SIZE = 4*1024;
whereas for the LM3S (in
lm3s-ram.ld) a different value is specified
_DEFAULT_STACK_SIZE = 2*1024;
In all cases the symbol name is the same, only the name of the linker script file varies.
3.1.2. Per-Task Primary Stack Size¶
The primary stack size for a task object can be specified either on the
type declaration, if explicitly defined, or directly on the task
object declaration. To do so, use the
Storage_Size aspect (or pragma). For
example, the following code defines a task type with a primary stack
size of 20KB:
task type My_Task_Type with Storage_Size => 20 * 1024;
Every task object of that type will have that primary stack size.
For an anonymously-typed task object:
task My_Task with Storage_Size => 20 * 1024;
That one task object will then have that primary stack size.
If you want to have a task type that does not apply the same stack size to every object of the type, you can use a discriminant:
task type My_Task_Type (Primary_Stack : Positive) with Storage_Size => Primary_Stack;
Each task object will specify a value for the Primary_Stack discriminant, thereby defining the primary stack size for that individual object:
T1 : My_Task_Type (Primary_Stack => 20 * 1024); T2 : My_Task_Type (Primary_Stack => 4 * 1024);
3.1.3. Environment Task Primary Stack Size¶
The application environment task is “created” by the target execution environment. Therefore, everything required for the environment task’s execution, including the primary stack size, must be known before the application starts. For non-bareboard targets, refer to your operating system’s manual.
For bareboard targets, the mechanism for specifying the environment task’s primary stack size is implementation-defined. In GNAT, there are two ways to do so:
Edit the default value in the linker script
Specify the default using a linker switch when building
You can change the linker script to specify a different value for the
_DEFAULT_STACK_SIZE symbol. The new default will be applied whenever
the corresponding run-time library is linked with an application. The
same default will be used in each build.
If, however, you do not want to use the same default, even if you have changed the default in the run-time library, you can specify the different primary stack size using a linker switch. Thus the difference will be project-specific (or even invocation-specific) rather than specific to the individual run-time library.
The linker switch is of the form
X is is the number of bytes required. The value can be either
in decimal or hexadecimal (with a leading “0x”).
For example, to set a primary stack size of 20K bytes the switch would be, in decimal:
or, in hexadecimal:
Note how the switch defines the
__stack_size symbol, which is used
inside the linker scripts (e.g.,
common-RAM.ld). If that symbol
is defined the corresponding value is used instead of that of the
Although you can add the switch to an invocation of the builder on the
command line (using the
-largs switch), we suggest more permanently
specifying it in the project file, like so:
package Linker is for Default_Switches ("ada") use ("-Wl,--defsym=__stack_size=20480"); end Linker;
Note that there are no spaces anywhere within the string.
However you specify it, if the primary stack size value is too large the builder will not create the executable image.
3.2. The Secondary Stack¶
GNAT returns objects from functions via registers (if small) or via the primary stack. For the latter, the caller of the function typically allocates space for the return object on its primary stack before the call. However, Ada allows functions to return objects of unconstrained types, for example unbounded array types such as String, and unconstrained discriminated record types. In this case the caller does not know the size of the returned object at the point of the call.
To resolve this problem, GNAT provides each task with a secondary stack that objects of unconstrained types are returned on. On native and cross targets using the full run-time, the secondary stack by default is allocated dynamically on the heap. For cross and bareboard platforms, where stack sizes are fixed, secondary stacks are statically allocated and cannot grow at run time. In both cases the secondary stack is allocated independently from the primary stack.
3.2.1. Fixed Secondary Stack Allocation¶
Secondary stack allocation on Ravenscar, ZFP, and Cert run-times is fixed,
as they do not permit dynamic allocation of memory by the run-time. For these
targets, the secondary stack is allocated statically at bind time using the
default size given by the
parameter. This default value can be changed using the gnatbind
switch if a more suitable value is required. For example, to specify a default
secondary stack size of 20KB for all tasks:
gnatbind -D20k main.ali
Like all gnatbind switches, the
-D switch can be included in the
binder section of a project file.
The default secondary stack size can be overridden on a per-task basis if
individual tasks have different secondary stack requirements. This is achieved
Secondary_Stack_Size aspect that takes the size of the
secondary stack in bytes. For example, to specify a 20KB secondary stack for
task A_Task with Secondary_Stack_Size => 20 * 1024;
3.2.2. Dynamic Secondary Stack Allocation¶
Dynamic secondary stack allocation allows GNAT to size the secondary stack to the needs of each task at run time without input from the developer and is used by default on the full GNAT run-time used by native and many cross targets. The maximum size a secondary stack for a task can grow to is the smaller of 2GB or the amount of free memory on the target.
The initial memory allocated to the secondary stack is governed by the
System.Parameters.Runtime_Default_Sec_Stack_Size and grows
in increments of this value unless an object larger than this value needs
to be placed on the stack. In this case, the stack will grow by the size
of the object.
The value of
Runtime_Default_Sec_Stack_Size is suitable for nearly all
applications, balancing the number of heap allocations required during the life
of the stack against stack fragmentation. However, if the default allocation
size is unsuitable, it can be modified via the gnatbind
Per-task increment sizes can also be specified via the
task A_Task with Secondary_Stack_Size => 20 * 1024;
In certain circumstances there can be a performance benefit by increasing the default secondary stack size to reduce the number of memory allocations performed by the secondary stack. These situations include string operations within a loop that cause a string to grow on each iteration. In this case it is advised to set the secondary stack size for the task performing the operation (or the default secondary stack size) to the expected final size of the string in question.
3.2.3. Disabling the Secondary Stack¶
The secondary stack can be disabled by using pragma Restrictions (No_Secondary_Stack); This will cause an error to be raised at compile time for each call to a function that returns an object of unconstrained type. When this restriction is in effect, Ada tasks (excluding the environment task) will not have their secondary stacks allocated.