Gem #77: Where did my memory go? (Part 1)

Let's get started…

Unless your coding standard forbids any dynamic allocation, memory management is a constant concern during system development. You might want to limit the amount of memory that your application requires, or you might have memory leaks (allocation chunks that are never returned to the system). The latter is a critical concern for long-running applications.

Part I: Storage Pools

The standard Ada memory management mechanism is the storage pool, as defined in package System.Storage_Pools. A storage pool is a tagged type that allows you to override the standard "new" operator and the associated Unchecked_Deallocation procedure. A given pool can be associated with one or more access types. The GNAT run-time comes with a number of predefined storage pools, and you can also create your own. One basic implementation, for instance, would be to instrument the pool operations to print a trace to the console every time memory is allocated or freed, and then post-process those traces with an external tool afterward.

This is of course a little tedious, so GNAT provides the package GNAT.Debug_Pools as a much more advanced storage pool implementation that can, at any time during program execution, display the currently allocated memory (along with a backtrace of the code at the point of allocation). It will also detect invalid memory references (for instance, attempting to dereference a pointer to already deallocated memory). The implementation is efficient andl imposes only a very small overhead on your application.

Here is a short example demonstrating the use of debug pools:

with GNAT.Debug_Pools;

package My_Package is
   Pool : GNAT.Debug_Pools.Debug_Pool;

   type Integer_Access is access Integer;
   for Integer_Access'Storage_Pool use Pool;
end My_Package;

with My_Package;
with Ada.Unchecked_Deallocation;
procedure Main is
   procedure Unchecked_Free is
      new Ada.Unchecked_Deallocation (Integer, Integer_Access);
   Ptr : Integer_Access;
   Ptr := new Integer;
   Ptr.all := 1;
   Unchecked_Free (Ptr);
   Ptr.all := 2;  --  raises exception
end Main;

The variable My_Package.Pool should be shared as much as possible among all your access types. It's not necessary to create one per access type.

As noted in the main procedure, the last reference to Ptr is invalid, and will result in an exception raised from the debug pool (rather than some erroneous behavior depending on the system).

GNAT.Debug_Pools provides various subprograms to analyze current memory usage, in particular the total amount of memory currently allocated, as well as which part of the code did the allocations. The backtraces are also useful when analyzing double-deallocation scenarios, since a debug pool shows both where the memory was allocated and by what piece of code it was first deallocated.

However, GNAT's debug pools are rather heavy to put in place in existing code, since you need to add a "for Type_Name'Storage_Pool use Pool" to every access type, and there is no mechanism to define a single default storage pool for all types. GNAT.Debug_Pools can also give false warnings when dereferencing a pointer to aliased data on the stack (which was never allocated via a "new" operator, but was accessed via an 'Access attribute).

In the next Gem we will discuss an alternative approach to controlling and instrumenting dynamic allocation and deallocation, by overriding the low-level memory management support itself.