Adaのメモリ管理 ── アクセス型・Controlled型・ストレージプールの使い分け

· · Ada, GNAT, メモリ管理, アクセス型, RAII, ストレージプール

Xでポスト はてなブックマーク
更新履歴(初版のみ・2026年11月01日公開)
初版公開
この記事を引用する(DOI: 10.5281/zenodo.23855031)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「Adaのメモリ管理 ── アクセス型・Controlled型・ストレージプールの使い分け」合同会社小村ソフト. https://doi.org/10.5281/zenodo.23855031 https://comcomponent.com/blog/ada-memory-management/

DOI(最新版)
10.5281/zenodo.23855031
DOI(この版)
10.5281/zenodo.23855032

Ada の規格はガベージコレクション(GC)を前提にしていません。GNAT の既定の確保先は malloc と free を使い、使わなくなった領域を自動では回収しません1。その代わり Ada にはヒープを使わずに済ませる書き方と、ヒープを使うときの規則を型に書く仕組みがあります。

この記事では Ada のメモリ管理を次の順に説明します。

  1. 値をスタックに置く。長さが実行時に決まる値も、new を書かずに関数から返せる。
  2. ヒープが要るときはアクセス型を使い、何を指せるか、どこまで寿命があるかを型で決める。
  3. 解放は Unchecked_Deallocation で行う。名前のとおり検査はないので、直接呼ぶ場所を減らす。
  4. 解放を Controlled 型の Finalize に任せる。C++ の RAII と同じ考え方。
  5. 確保先をストレージプールで決める。上限を付けたり、確保の回数を数えたりできる。

例はすべて Ubuntu 24.04 の GNAT 13.3.0 でコンパイルして実行し、出力をそのまま載せています。メモリの誤用の確認には valgrind 3.22.0 を使いました。言語の規則は Ada 2022 の参照マニュアル(RM)で確かめています。Ada そのものの特徴は「Ada言語の魅力」で紹介しています。

Adaで値が置かれる場所

Ada では、宣言した変数は通常スタックに置かれます。長さが実行時に決まる配列も、new を書かずに変数として宣言できます。ヒープ(Ada の用語ではストレージプール)を使うのは、new を書いたときです2。

Adaで値が置かれる場所宣言した変数はスタックに、長さの決まらない戻り値はGNATではセカンダリスタックに、newで作った値はアクセス型のストレージプールに置かれることを示す。宣言した変数スタック長さの決まらない戻り値セカンダリスタック(GNAT)newで作った値ストレージプール既定のプールはmallocとfree

図1: 宣言した変数はスタックに、new で作った値はストレージプールに置かれる。

この記事で扱う仕組みを、置き場所ごとにまとめると次のようになります。

置き場所 確保 解放 この記事の節
スタック 変数の宣言 スコープを出たとき 長さの決まらない値をnewなしで返す
セカンダリスタック(GNAT) 長さの決まらない値を返す関数 呼び出し側のスコープを出たとき 同上
ストレージプール new Unchecked_Deallocation、または Controlled 型・コンテナ アクセス型以降

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全15件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

長さの決まらない値をnewなしで返す

C で長さが実行時に決まる文字列を返すには、呼び出し側にバッファを用意させるか、関数の中で malloc して呼び出し側に free させるのが普通です。Ada では長さの決まっていない String をそのまま戻り値にできます。

with Ada.Text_IO; use Ada.Text_IO;

procedure Repeat_Demo is
   --  戻り値の長さは、呼び出すまで決まらない
   function Repeat (C : Character; N : Natural) return String is
      Result : constant String (1 .. N) := (others => C);
   begin
      return Result;
   end Repeat;

   N : Natural := 3;
begin
   for I in 1 .. 3 loop
      declare
         Line : constant String := Repeat ('*', N);
      begin
         Put_Line (Line & " length =" & Natural'Image (Line'Length));
      end;
      N := N * 2;
   end loop;
end Repeat_Demo;
*** length = 3
****** length = 6
************ length = 12

new も解放も書いていません。Line の長さは Repeat が返した値で決まります。このプログラムを valgrind で動かすと、ヒープの確保は0回でした。

この記事の環境の GNAT はこのような戻り値をセカンダリスタックに置きます。GNAT のバインダーの資料はセカンダリスタックを「関数が返す、長さの決まっていない型(たとえば長さの決まっていない String)のオブジェクトを置く場所」と説明しています3。多くの環境ではセカンダリスタックは必要に応じて伸び、ヒープ上のブロックの連なりとして実装されます3。GNAT の資料は関数の戻り値にセカンダリスタックを使うのは一部のターゲットだとも書いています4。

ブロックを出たあとに領域が使い直されるかを、1 MB の文字列を 10,000 回返すループで確かめました。

with Ada.Text_IO; use Ada.Text_IO;

procedure SS_Loop is
   function Repeat (C : Character; N : Natural) return String is
      Result : constant String (1 .. N) := (others => C);
   begin
      return Result;
   end Repeat;

   Total : Long_Long_Integer := 0;
begin
   --  1 MB の文字列を 10,000 回返す。合計で約 10 GB になる
   for I in 1 .. 10_000 loop
      declare
         Line : constant String := Repeat ('*', 1_000_000);
      begin
         Total := Total + Long_Long_Integer (Line'Length);
      end;
   end loop;
   Put_Line ("total =" & Long_Long_Integer'Image (Total));
end SS_Loop;
total = 10000000000
==32596==     in use at exit: 1,000,048 bytes in 1 blocks
==32596==   total heap usage: 1 allocs, 0 frees, 1,000,048 bytes allocated

合計 10 GB の文字列を返しても、ヒープの確保は 1,000,048 バイトの1回だけでした。この1回はセカンダリスタックを伸ばすためのブロックと考えられ、そのあとの繰り返しでは新しい確保はありませんでした。new で作った値と違い、プログラムの側で解放を書く必要はありません。

セカンダリスタックの使われ方関数が長さの決まらない値を返すとセカンダリスタックに積まれ、呼び出し側のブロックで使われ、ブロックを出ると戻されることを示す。Repeatを呼ぶ結果をセカンダリスタックに積むdeclareブロックでLineとして使うブロックを出る積んだ領域を戻す

図2: 長さの決まらない戻り値は呼び出し側のブロックを出ると戻される。

GNAT のこの使い方は pragma Restrictions (No_Secondary_Stack) を付けると確かめられます。この制限は、生成コードがセカンダリスタックを参照しないことをコンパイル時に保証するものです4。上の Repeat と同じ関数をこの制限の下でコンパイルすると、次のエラーになりました。

pragma Restrictions (No_Secondary_Stack);

procedure Nosec is
   function Repeat (C : Character; N : Natural) return String is
      Result : constant String (1 .. N) := (others => C);
   begin
      return Result;
   end Repeat;

   Line : constant String := Repeat ('*', 3);
begin
   null;
end Nosec;
nosec.adb:7:07: error: violation of restriction "No_Secondary_Stack" at line 1
nosec.adb:10:04: error: violation of restriction "No_Secondary_Stack" at line 1
nosec.adb:10:30: error: violation of restriction "No_Secondary_Stack" at line 1

この制限を付けたコードでは、呼び出し側が長さの決まった配列を用意し、関数はそこに書き込む形にします。

アクセス型で「何を指せるか」を決める

ヒープに置く必要があるのは、作った値をスコープの外でも使いたい場合や、リストや木のように要素同士をつなぐ場合です。Ada ではこのときアクセス型を使います。アクセス型は C のポインタに近いものですが、何を指せるかを型の宣言で決めます2。

プール固有のアクセス型と汎用のアクセス型

access だけで宣言した型はプール固有のアクセス型です。その型の new で作った値(そのストレージプールの要素)だけを指せます。access all で宣言した型は汎用のアクセス型で、aliased を付けて宣言した変数も指せます2。

with Ada.Text_IO; use Ada.Text_IO;

procedure Access_Kinds is
   --  プール固有のアクセス型: new で確保したものだけを指せる
   type Int_Ptr is access Integer;

   --  汎用のアクセス型: aliased な変数も指せる
   type Int_Ref is access all Integer;

   P : constant Int_Ptr := new Integer'(10);

   Counter : aliased Integer := 20;
   R       : constant Int_Ref := Counter'Access;
begin
   R.all := R.all + 1;
   Put_Line ("P.all   =" & Integer'Image (P.all));
   Put_Line ("Counter =" & Integer'Image (Counter));
end Access_Kinds;
P.all   = 10
Counter = 21

逆の組み合わせはコンパイルエラーになります。プール固有の型で変数を指そうとした場合と、aliased の付いていない変数を指そうとした場合です。

procedure Access_Errors is
   type Int_Ptr is access Integer;
   type Int_Ref is access all Integer;

   Plain   : Integer := 1;
   Counter : aliased Integer := 2;

   P1 : Int_Ptr := Counter'Access;  --  プール固有の型は変数を指せない
   R1 : Int_Ref := Plain'Access;    --  aliased でない変数は指せない
begin
   null;
end Access_Errors;
access_errors.adb:8:27: error: result must be general access type
access_errors.adb:8:27: error: add "all" to type "Int_Ptr" defined at line 2
access_errors.adb:9:20: error: prefix of "Access" attribute must be aliased

この規則のおかげで、どの変数がポインタで指されうるかを宣言から読み取れます。

アクセス型の種類と指せるものプール固有のアクセス型はnewで作った値だけを指し、汎用のアクセス型はnewで作った値に加えてaliasedを付けた変数も指せることを示す。プール固有のアクセス型(access)newで作った値汎用のアクセス型(access all)aliasedを付けた変数aliasedのない変数どのアクセス型も指せない

図3: 汎用のアクセス型だけが aliased を付けた変数を指せる。

寿命の検査(アクセシビリティ検査)

C でよくある誤りに、関数のローカル変数のアドレスを、関数を抜けたあとも残る場所に保存してしまうものがあります。Ada はこれをアクセシビリティ検査で防ぎます。変数の寿命(アクセシビリティレベル)がアクセス型の寿命より短い場合、その変数を指す値をその型に入れることはできません5。

コンパイル時に分かる場合はコンパイルエラーになります。次のパッケージはライブラリ水準(プログラムの終わりまで残る)の変数 Saved に、引数の変数を指す値を保存しようとしています。

package Registry is
   type Int_Ref is access all Integer;

   Saved : Int_Ref;  --  ライブラリ水準の変数。プログラムの終わりまで残る

   procedure Remember_Static (X : aliased in out Integer);
end Registry;

package body Registry is
   procedure Remember_Static (X : aliased in out Integer) is
   begin
      Saved := X'Access;
   end Remember_Static;
end Registry;
registry.adb:4:16: error: non-local pointer cannot point to local object

引数を匿名のアクセス型(access Integer)にすると、呼び出し側が何を渡すかはコンパイル時には分かりません。この場合、検査は実行時に行われ、失敗すると Program_Error が発生します5。

package Registry is
   type Int_Ref is access all Integer;

   Saved : Int_Ref;  --  ライブラリ水準の変数。プログラムの終わりまで残る

   Global_Value : aliased Integer := 100;

   procedure Remember (X : access Integer);
end Registry;

package body Registry is
   procedure Remember (X : access Integer) is
   begin
      Saved := Int_Ref (X);  --  実引数の寿命を、実行時に確かめる
   end Remember;
end Registry;
with Ada.Text_IO; use Ada.Text_IO;
with Registry;    use Registry;

procedure Remember_Demo is
   procedure Try_Local is
      Local : aliased Integer := 1;
   begin
      Remember (Local'Access);
      Put_Line ("saved a local object");
   end Try_Local;
begin
   Remember (Global_Value'Access);
   Put_Line ("saved:" & Integer'Image (Saved.all));

   Try_Local;
end Remember_Demo;
saved: 100

raised PROGRAM_ERROR : registry.adb:4 accessibility check failed

ライブラリ水準の Global_Value を渡したときは保存でき、Try_Local のローカル変数を渡したときは保存の時点で止まりました。C で同じことをすると保存はでき、関数を抜けたあとにその値を使うと未定義の動作になります。Ada では保存しようとした行で止まります。

アクセシビリティ検査が行われる時点寿命の短い変数を指す値を寿命の長いアクセス型に入れようとすると、コンパイル時に分かればコンパイルエラーになり、匿名アクセス型の引数のように実行時まで分からなければProgram_Errorになることを示す。分かる分からない(匿名アクセス型の引数)寿命の短い変数を指す値を保存するコンパイル時に寿命が分かるかコンパイルエラー実行時に検査するProgram_Error

図4: 寿命の検査は、分かる場合はコンパイル時に、分からない場合は実行時に行われる。

null を許さないアクセス型

アクセス型の変数は初期値を書かなければ null になります。引数や変数の型に not null を付けると、その型は null を含まなくなります2。null を渡した場合は、呼び出しの時点で Constraint_Error になります。

with Ada.Text_IO; use Ada.Text_IO;

procedure Not_Null_Demo is
   type Int_Ptr is access Integer;

   --  引数に null が来ないことを、型の側で約束する
   procedure Show (P : not null Int_Ptr) is
   begin
      Put_Line ("value =" & Integer'Image (P.all));
   end Show;

   A : constant Int_Ptr := new Integer'(5);
   B : Int_Ptr;  --  初期値を書かないアクセス型の変数は null になる
begin
   Show (A);
   Show (B);
end Not_Null_Demo;
not_null_demo.adb:13:04: warning: variable "B" is read but never assigned [-gnatwv]
value = 5

raised CONSTRAINT_ERROR : not_null_demo.adb:16 access check failed

1つ目はコンパイル時の警告、2つ目は実行時の出力です。Show の中で null を確かめる必要はなくなり、null が渡された場合は Show の中ではなく呼び出した行(16行目)が報告されます。

Unchecked_Deallocationで解放する

new で確保した値は Ada.Unchecked_Deallocation をインスタンス化した手続きで解放します。慣例でこの手続きを Free と名付けます。RM の規則では、Free (X) のあと X は null になり、X がすでに null なら何もしません6。

名前に Unchecked とあるとおり、この手続きは同じ領域を指すほかのポインタを調べません。次の例では、A と B が同じ値を指している状態で A を解放しています。

with Ada.Text_IO; use Ada.Text_IO;
with Ada.Unchecked_Deallocation;

procedure Dangling_Demo is
   type Int_Ptr is access Integer;
   procedure Free is new Ada.Unchecked_Deallocation (Integer, Int_Ptr);

   A : Int_Ptr := new Integer'(42);
   B : constant Int_Ptr := A;  --  同じオブジェクトを指す2本目のポインタ
begin
   Free (A);
   Put_Line ("A = null : " & Boolean'Image (A = null));
   Put_Line ("B = null : " & Boolean'Image (B = null));
   Put_Line ("B.all    =" & Integer'Image (B.all));  --  解放済みの領域を読む
end Dangling_Demo;
A = null : TRUE
B = null : FALSE
B.all    = 1613378576

A は null になりましたが、B は元のアドレスを持ったままです。存在しないオブジェクトを指すこの値を RM はダングリング参照と呼び、それを参照したときの実行を erroneous(結果が規格で定まらない)としています6。GNAT 13.3 では例外は出ず、B.all は 42 ではない値を返しました。同じプログラムを valgrind で動かすと、解放済みの領域を読んだと報告されます。

==6788== Invalid read of size 4
==6788==    at 0x10BAD8: _ada_dangling_demo (dangling_demo.adb:14)
==6788==    by 0x10B8DE: main (b~dangling_demo.adb:248)
==6788==  Address 0x5004040 is 0 bytes inside a block of size 4 free'd
==6788==    at 0x484988F: free (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==6788==    by 0x4B08E48: __gnat_free (in /usr/lib/x86_64-linux-gnu/libgnat-13.so)
==6788==    by 0x10B954: _ada_dangling_demo (dangling_demo.adb:11)

このトレースから、GNAT の既定のプールが __gnat_free を通して C の free を呼んでいることも分かります。

Unchecked_Deallocationのあとに残るポインタFree(A)でAはnullになるが、同じ領域を指していたBは元のアドレスを持ったままで、Bを参照するとerroneousになり、valgrindは解放済み領域の読み取りとして報告することを示す。AとBが同じ値を指すFree(A)AはnullになるBは元のアドレスのままB.allを読むerroneous(結果は定まらない)valgrind: Invalid read

図5: Free は渡した変数だけを null にし、同じ領域を指す別の変数は残る。

逆に、解放しなければ領域は戻りません。次のループは同じ変数に1,000回 new の結果を入れ、前の値を指すポインタを失っています。

procedure Leak_Demo is
   type Int_Ptr is access Integer;
   P : Int_Ptr;
begin
   for I in 1 .. 1_000 loop
      P := new Integer'(I);  --  前の領域を指すポインタは、ここで失われる
   end loop;
end Leak_Demo;
==10504==     in use at exit: 4,000 bytes in 1,000 blocks
==10504== 4,000 bytes in 1,000 blocks are definitely lost in loss record 1 of 1
==10504==    at 0x4846828: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==10504==    by 0x4B08DE1: __gnat_malloc (in /usr/lib/x86_64-linux-gnu/libgnat-13.so)
==10504==    by 0x10A6FF: _ada_leak_demo (leak_demo.adb:6)

GC のある言語なら回収される領域が、GNAT ではプログラムの終わりまで残りました。GNAT の資料も、既定のプール(System.Pool_Global)は malloc と free を使い、自動での回収はしないと書いています1。

規格のほうも GC を前提にしていません。Ada 2012 の改訂では参照マニュアルからガベージコレクションについての記述が削除されました。注釈付きの参照マニュアル(AARM)は、その代わりに Ada 2012 がサブプールを用意したこと、サブプールのほうが GC よりずっと予測しやすいことを書いています7。

SPARKの所有権の規則

Ada の部分言語である SPARK は、アクセス型に所有権の規則を課します。確保した領域に読み書きできるアクセス値は、同時に1つだけです8。前の Dangling_Demo と同じ並びを SPARK のコードとして書き、GNATprove 15.1 で --mode=check_all を付けて検査しました。

with Ada.Unchecked_Deallocation;

procedure Dangling_Spark with SPARK_Mode is
   type Int_Ptr is access Integer;
   procedure Free is new Ada.Unchecked_Deallocation (Integer, Int_Ptr);

   A : Int_Ptr := new Integer'(42);
   B : Int_Ptr := A;  --  A の所有権は B に移る
   V : Integer;
begin
   Free (A);
   V := B.all;
   Free (B);
end Dangling_Spark;
dangling_spark.adb:11:10: error: "A" is not readable
   11 |   Free (A);
      |         ^ here
  object was moved at line 8 [E0010]
    8 |   B : Int_Ptr := A;  --  A の所有権は B に移る
      |                  ^ here

B := A の時点で所有権が B に移るので、A を解放しようとした行が検査で止まりました。GNAT でコンパイルして実行した Dangling_Demo では、同じ誤りでも例外は出ず、valgrind で初めて分かりました。SPARK での証明の進め方は「AIが書いたコードを SPARK で証明する」で扱っています。

同じ誤りをAdaとSPARKで扱ったときの違い2つのポインタで同じ領域を指して片方を解放する誤りが、Adaとして実行すると検出されずに解放済み領域を読み、SPARKとして検査するとGNATproveが所有権の移動を理由にエラーにすることを示す。2つのポインタで同じ領域を指して解放するAdaとして実行するSPARKとして検査する検出されずに解放済み領域を読むGNATproveが所有権の移動でエラーにする

図6: 同じ誤りが SPARK では所有権の規則によって検査の段階で見つかる。

Ada のまま書く場合は、Free を呼ぶ場所を、値の持ち主がはっきりしている少数の箇所に限ります。その代表が次の Controlled 型です。

Controlled型でスコープを出るときに後始末する

Ada.Finalization の Controlled 型を継承すると、3つの手続きを上書きできます9。

手続き 呼ばれるとき
Initialize 初期値を書かずに宣言されたとき
Adjust 代入でコピーされたあと、コピー先に対して
Finalize スコープを出るとき、代入で上書きされる前、Free で解放されるとき

呼ばれる順番を確かめるために、呼ばれるたびに名前を表示する型を作りました。Adjust ではコピーされた側だと分かるように名前を小文字にしています。

with Ada.Finalization;

package Tracked is
   type Resource is new Ada.Finalization.Controlled with record
      Name : Character := '?';
   end record;

   overriding procedure Initialize (R : in out Resource);
   overriding procedure Adjust     (R : in out Resource);
   overriding procedure Finalize   (R : in out Resource);
end Tracked;
with Ada.Characters.Handling; use Ada.Characters.Handling;
with Ada.Text_IO;             use Ada.Text_IO;

package body Tracked is
   overriding procedure Initialize (R : in out Resource) is
   begin
      Put_Line ("  Initialize " & R.Name);
   end Initialize;

   --  コピーされた側だと分かるように、名前を小文字にする
   overriding procedure Adjust (R : in out Resource) is
   begin
      Put_Line ("  Adjust     " & R.Name & " -> " & To_Lower (R.Name));
      R.Name := To_Lower (R.Name);
   end Adjust;

   overriding procedure Finalize (R : in out Resource) is
   begin
      Put_Line ("  Finalize   " & R.Name);
   end Finalize;
end Tracked;
with Ada.Finalization;
with Ada.Text_IO; use Ada.Text_IO;
with Tracked;     use Tracked;

procedure Raii_Demo is
begin
   Put_Line ("enter block");
   declare
      --  初期値を書かないので Initialize が呼ばれる
      A : Resource;
      --  集成体で初期化するので Initialize は呼ばれない
      B : Resource := (Ada.Finalization.Controlled with Name => 'B');
   begin
      A.Name := 'A';
      Put_Line ("assign B := A");
      B := A;
      Put_Line ("leave block");
   end;
   Put_Line ("after block");

   Put_Line ("enter block with exception");
   begin
      declare
         C : Resource;
      begin
         C.Name := 'C';
         raise Constraint_Error;
      end;
   exception
      when Constraint_Error =>
         Put_Line ("handled Constraint_Error");
   end;
end Raii_Demo;
enter block
  Initialize ?
assign B := A
  Finalize   B
  Adjust     A -> a
leave block
  Finalize   a
  Finalize   A
after block
enter block with exception
  Initialize ?
  Finalize   C
handled Constraint_Error

出力から次のことが読み取れます。

  • Initialize は初期値のない A に対してだけ呼ばれ、集成体で初期化した B には呼ばれていません。RM も Initialize を既定の初期化のときに呼ぶと定めています9。
  • B := A ではまず上書きされる B の Finalize、次にコピーした値の Adjust が呼ばれました。
  • ブロックを出るとき、宣言と逆の順(B のコピー、A の順)に Finalize が呼ばれました。
  • 例外でブロックを抜けた C にも Finalize が呼ばれています。
Controlled型の手続きが呼ばれる順番初期値なしの宣言でInitialize、代入では上書きされる側のFinalizeとコピー先のAdjust、スコープを出るときと例外で抜けるときに宣言と逆順のFinalizeが呼ばれることを示す。初期値なしで宣言するInitializeB := Aを実行するBの古い値をFinalizeコピーした値をAdjustスコープを出る・例外で抜ける宣言と逆の順にFinalize

図7: 代入では古い値の Finalize とコピーの Adjust が、スコープを出るときには逆順の Finalize が呼ばれる。

例外で抜けたときにも Finalize が呼ばれるので、ヒープの解放をここに書けば、呼び出し側が Free を書き忘れることはなくなります。C++ でデストラクタに解放を書く RAII と同じ考え方です。C++ 側の RAII は「マルチスレッドの実務ベストプラクティス C++編」で扱っています。

コピーさせたくない資源には Limited_Controlled を使います。代入ができない型になるので、Adjust はありません。次の節の例はこちらを使います。

ストレージプールで確保先を決める

new がどこから領域を取るかは、アクセス型ごとのストレージプールで決まります。何も指定しなければ処理系の既定のプールが使われ、GNAT ではそれが malloc と free を使う System.Pool_Global です1。Ada ではこのプールを型ごとに差し替えたり、上限を付けたりできます10。

Storage_Sizeで上限を付ける

アクセス型に Storage_Size を指定すると、その型の new は指定した大きさ以上の領域を持つ処理系定義のプールから確保します。領域を使い切ると、それ以上は確保せずに Storage_Error を発生させます10。

with Ada.Text_IO; use Ada.Text_IO;

procedure Bounded_Demo is
   --  この型の new で使える領域を 64 バイトに限る
   type Int_Ptr is access Integer
     with Storage_Size => 64;

   P     : Int_Ptr;
   Count : Natural := 0;
begin
   loop
      P := new Integer'(Count);
      Count := Count + 1;
   end loop;
exception
   when Storage_Error =>
      Put_Line ("Storage_Error after" & Natural'Image (Count) & " allocations");
end Bounded_Demo;
Storage_Error after 8 allocations

8回確保したところで、9回目の new が Storage_Error になりました。1回の確保で使う量は処理系が決めるので、回数は Integer の大きさから単純には求まりません。このプールの領域はアクセス型を宣言したスコープを出ると回収されます10。メモリの上限を決めておきたい組込み機器や、長時間動く処理で、型ごとの使用量に枠を設けたいときに使えます。

Storage_Sizeを指定したアクセス型の確保Storage_Sizeを指定したアクセス型でnewを実行すると、プールに空きがあれば確保し、使い切っていればStorage_Errorになり、アクセス型を宣言したスコープを出るとプールの領域が回収されることを示す。あるないInt_Ptrでnewを実行するプールに空きがあるか確保するStorage_Errorアクセス型を宣言したスコープを出るプールの領域を回収する

図8: Storage_Size を指定した型は、枠を使い切ると Storage_Error になり、スコープを出ると枠ごと回収される。

自分でプールを書く

System.Storage_Pools の Root_Storage_Pool を継承し、Allocate・Deallocate・Storage_Size を上書きすると、自分のプールを作れます10。次のプールは確保と解放の回数と使用中のバイト数を数え、実際の確保は GNAT の既定のプールに任せます。System.Pool_Global は GNAT 固有のユニットなので、ほかの処理系では別の確保先が必要です。

with System;
with System.Storage_Elements; use System.Storage_Elements;
with System.Storage_Pools;    use System.Storage_Pools;

package Counting_Pools is
   --  確保と解放の回数、使用中のバイト数を数えるプール
   type Counting_Pool is new Root_Storage_Pool with record
      Allocations   : Natural       := 0;
      Deallocations : Natural       := 0;
      In_Use        : Storage_Count := 0;
   end record;

   overriding procedure Allocate
     (Pool                     : in out Counting_Pool;
      Storage_Address          : out System.Address;
      Size_In_Storage_Elements : Storage_Count;
      Alignment                : Storage_Count);

   overriding procedure Deallocate
     (Pool                     : in out Counting_Pool;
      Storage_Address          : System.Address;
      Size_In_Storage_Elements : Storage_Count;
      Alignment                : Storage_Count);

   overriding function Storage_Size
     (Pool : Counting_Pool) return Storage_Count;

   procedure Report (Pool : Counting_Pool; Label : String);
end Counting_Pools;
with Ada.Text_IO;      use Ada.Text_IO;
with System.Pool_Global;  --  GNAT の既定のプール(GNAT 固有のユニット)

package body Counting_Pools is
   Backing : System.Pool_Global.Unbounded_No_Reclaim_Pool
     renames System.Pool_Global.Global_Pool_Object;

   overriding procedure Allocate
     (Pool                     : in out Counting_Pool;
      Storage_Address          : out System.Address;
      Size_In_Storage_Elements : Storage_Count;
      Alignment                : Storage_Count) is
   begin
      Backing.Allocate (Storage_Address, Size_In_Storage_Elements, Alignment);
      Pool.Allocations := Pool.Allocations + 1;
      Pool.In_Use      := Pool.In_Use + Size_In_Storage_Elements;
   end Allocate;

   overriding procedure Deallocate
     (Pool                     : in out Counting_Pool;
      Storage_Address          : System.Address;
      Size_In_Storage_Elements : Storage_Count;
      Alignment                : Storage_Count) is
   begin
      Backing.Deallocate (Storage_Address, Size_In_Storage_Elements, Alignment);
      Pool.Deallocations := Pool.Deallocations + 1;
      Pool.In_Use        := Pool.In_Use - Size_In_Storage_Elements;
   end Deallocate;

   overriding function Storage_Size
     (Pool : Counting_Pool) return Storage_Count is
   begin
      return Storage_Count'Last;  --  上限は設けない
   end Storage_Size;

   procedure Report (Pool : Counting_Pool; Label : String) is
   begin
      Put_Line (Label & ": allocations =" & Natural'Image (Pool.Allocations)
                & ", deallocations =" & Natural'Image (Pool.Deallocations)
                & ", in use =" & Storage_Count'Image (Pool.In_Use) & " bytes");
   end Report;
end Counting_Pools;

このプールを前の節の Limited_Controlled と組み合わせます。Buffer はヒープ上の配列を1つだけ持ち、Finalize でそれを解放する型です。アクセス型 Byte_Array_Ptr には Storage_Pool => Pool を指定し、この型の new が Counting_Pool を通るようにしています。

with Ada.Finalization;
with Counting_Pools;

package Buffers is
   Pool : Counting_Pools.Counting_Pool;

   type Byte_Array is array (Positive range <>) of Character;
   type Byte_Array_Ptr is access Byte_Array
     with Storage_Pool => Pool;

   --  ヒープ上の配列を1つだけ持ち、スコープを出ると解放する型
   type Buffer is new Ada.Finalization.Limited_Controlled with record
      Data : Byte_Array_Ptr;
   end record;

   procedure Allocate (B : in out Buffer; Length : Positive);
   overriding procedure Finalize (B : in out Buffer);
end Buffers;
with Ada.Unchecked_Deallocation;

package body Buffers is
   procedure Free is new Ada.Unchecked_Deallocation (Byte_Array, Byte_Array_Ptr);

   procedure Allocate (B : in out Buffer; Length : Positive) is
   begin
      Free (B.Data);  --  既に持っていれば先に返す(null なら何もしない)
      B.Data := new Byte_Array (1 .. Length);
   end Allocate;

   overriding procedure Finalize (B : in out Buffer) is
   begin
      Free (B.Data);
   end Finalize;
end Buffers;
with Counting_Pools; use Counting_Pools;
with Buffers;        use Buffers;

procedure Pool_Demo is
begin
   Report (Pool, "start      ");

   declare
      X, Y : Buffer;
   begin
      Allocate (X, 100);
      Allocate (Y, 200);
      Allocate (X, 50);  --  X の100バイトを返してから50バイトを取り直す
      Report (Pool, "in block   ");
   end;  --  X と Y の Finalize が残りを解放する

   Report (Pool, "after block");
end Pool_Demo;
start      : allocations = 0, deallocations = 0, in use = 0 bytes
in block   : allocations = 3, deallocations = 1, in use = 268 bytes
after block: allocations = 3, deallocations = 3, in use = 0 bytes

ブロックの中では3回確保して1回解放し、ブロックを出たあとは確保と解放の回数がそろって使用量が0に戻りました。Pool_Demo は Free を1度も呼んでいません。解放は Allocate の中の取り直しと、Buffer の Finalize だけで行われています。valgrind の --leak-check=full でも、このプログラムでは何も報告されませんでした。

使用中の 268 バイトは要素数の合計(250)より多い値です。Allocate に渡された大きさを1回ずつ表示すると、108、208、60 バイトでした。要素数より大きい分には、配列の添字の範囲を持つための領域などが含まれると考えられますが、内訳は処理系が決めるもので、この記事では確かめていません。

数えるプールを通る確保と解放Buffer型のnewとFreeがアクセス型に指定したCounting_PoolのAllocateとDeallocateを呼び、Counting_Poolは回数を数えてからGNATの既定のプールに確保と解放を任せることを示す。Buffer内のnewCounting_PoolのAllocateFinalizeのFreeCounting_PoolのDeallocate回数と使用量を数えるGNATの既定のプールmallocとfree

図9: アクセス型にプールを指定すると、その型の new と Free はすべてそのプールを通る。

このように確保を数えるプールは、テストで「処理のあとに使用量が0に戻るか」を確かめるのに使えます。GNAT には解放済みの領域の参照などを検出する GNAT.Debug_Pools も用意されています11。この記事では試していません。

サブプール

Ada 2012 では1つのプールを複数のサブプールに分ける System.Storage_Pools.Subpools が加わりました12。new (Subpool) T のように確保先のサブプールを指定でき、サブプールごとまとめて解放できます。処理単位ごとに確保した値を最後にまとめて捨てるような使い方に向いています。この記事ではサブプールの例は動かしていません。

標準コンテナで手書きのポインタを減らす

アクセス型と Controlled 型を組み合わせれば安全な型を作れますが、可変長の列や辞書のたびに自分で書く必要はありません。Ada 2005 以降の Ada.Containers にはベクタ・リスト・マップ・集合などがあり13、要素の確保と解放はコンテナが行います。

長さの違う String のように、大きさが値ごとに違う要素は Indefinite_ の付いたコンテナに入れます。

with Ada.Containers.Indefinite_Vectors;
with Ada.Text_IO; use Ada.Text_IO;

procedure Containers_Demo is
   --  長さの違う String をそのまま要素にできる
   package String_Vectors is new Ada.Containers.Indefinite_Vectors
     (Index_Type => Positive, Element_Type => String);

   Names : String_Vectors.Vector;
begin
   Names.Append ("Ada");
   Names.Append ("SPARK");
   Names.Append ("GNAT");

   for Name of Names loop
      Put_Line (Name & ":" & Natural'Image (Name'Length));
   end loop;

   Names.Delete (2);
   Put_Line ("length after delete:" & Ada.Containers.Count_Type'Image (Names.Length));
end Containers_Demo;
Ada: 3
SPARK: 5
GNAT: 4
length after delete: 2

このプログラムには new も Free もありません。valgrind で動かすと、終了時の使用量は 0 バイトで、確保したブロックはすべて解放されていました。

==10040==     in use at exit: 0 bytes in 0 blocks
==10040== All heap blocks were freed -- no leaks are possible

コンテナはジェネリックパッケージなので、要素の型を渡してインスタンス化します。ジェネリックの仕組みは「Adaのジェネリックプログラミング」で説明しています。

どの仕組みを使うかの目安

ここまでの仕組みは上から順に検討すると選びやすくなります。

Adaでメモリの扱い方を選ぶ順序まず値として宣言できるかを考え、要素を増減するならコンテナ、独自の構造が要るならアクセス型とControlled型、確保先や上限を決めたいならストレージプールを使う、という検討の順序を示す。できるできない足りる足りない決めたい値として宣言・返却できるか変数・戻り値のまま扱う列や辞書で足りるかAda.Containersアクセス型とControlled型確保先や上限を決めたいかストレージプール

図10: 値で扱えなければコンテナ、それでも足りなければアクセス型と Controlled 型を検討する。

  • 長さが実行時に決まるだけなら、値のまま宣言し、関数から返します。new は要りません。
  • 要素を増やしたり消したりするなら、Ada.Containers を使います。
  • 木やグラフのように独自の構造が必要なら、アクセス型で作り、Free を呼ぶのは Controlled 型の Finalize などの少数の場所に限ります。aliased と not null で、指せるものと null の有無を型に書きます。
  • 確保の上限や確保先を決めたいときは、Storage_Size かストレージプールを指定します。

Ada の型でメモリの使い方を表しても、Unchecked_Deallocation の誤用まではコンパイラーが止めません。その部分を少なくし、残った箇所は valgrind や確保を数えるプールで確かめます。所有権の規則まで検査したい部分は SPARK で書く方法もあります。

.NET のように GC のある環境でメモリが増え続けるときの調べ方は、「.NETでGC待ちとメモリリークを見分ける」にまとめています。

関連記事

参考リンク

  1. GNAT Reference Manual, System.Pool_Global (s-pooglo.ads). 既定のストレージプールと同じプールで、malloc と free を使い、自動での回収はしないこと。 ↩ ↩2 ↩3

  2. Ada Reference Manual (Ada 2022), 3.10 Access Types. プール固有のアクセス型と汎用のアクセス型、aliased、null 除外(not null)。 ↩ ↩2 ↩3 ↩4

  3. GNAT User’s Guide, Switches for gnatbind. -D スイッチの説明にある、セカンダリスタックの役割と実装。 ↩ ↩2

  4. GNAT Reference Manual, No_Secondary_Stack. 生成コードがセカンダリスタックを参照しないことをコンパイル時に保証する制限。 ↩ ↩2

  5. Ada Reference Manual (Ada 2022), 3.10.2 Operations of Access Types. アクセシビリティレベルと、検査に失敗したときの Program_Error。 ↩ ↩2

  6. Ada Reference Manual (Ada 2022), 13.11.2 Unchecked Storage Deallocation. Free (X) のあと X が null になること、ダングリング参照と erroneous な実行。 ↩ ↩2

  7. Annotated Ada Reference Manual (Ada 2022), 13.11.3 Default Storage Pools. 「Wording Changes from Ada 2005」の 9.d/3。Ada 2012 でガベージコレクションの記述が削除され、サブプールが用意されたこと。 ↩

  8. AdaCore, SPARK User’s Guide, Language Restrictions. アクセス型と割り当ては所有権の規則に従い、確保した領域に読み書きできるアクセス値は同時に1つだけであること(Memory Ownership Policy)。 ↩

  9. Ada Reference Manual (Ada 2022), 7.6 Assignment and Finalization. Controlled と Limited_Controlled、Initialize・Adjust・Finalize が呼ばれる条件と代入の手順。 ↩ ↩2

  10. Ada Reference Manual (Ada 2022), 13.11 Storage Management. Root_Storage_Pool と Allocate・Deallocate・Storage_Size、Storage_Size を指定した型のプールと Storage_Error。 ↩ ↩2 ↩3 ↩4

  11. GNAT User’s Guide, The GNAT Debug Pool Facility. 参照のたびに、正しく確保された領域を指しているかを確かめる GNAT.Debug_Pools。 ↩

  12. Ada Reference Manual (Ada 2022), 13.11.4 Storage Subpools. System.Storage_Pools.Subpools によるサブプール。 ↩

  13. Ada Reference Manual (Ada 2022), A.18 Containers. Ada.Containers のベクタ・リスト・マップ・集合と、Indefinite_ の付いたコンテナ。 ↩

同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。

このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。

この記事は次のサービスページにつながります。近い入口からご覧ください。

よくある質問

この記事のテーマについて、相談時によくある質問をまとめています。

Adaにはガベージコレクションがありますか?
GNATの既定のストレージプールは malloc と free で確保と解放をし、使わなくなった領域を自動では回収しません。Ada 2012 の改訂では、参照マニュアルからガベージコレクションについての記述が削除され、予測しやすい仕組みとしてサブプールが入りました。解放は Unchecked_Deallocation で行うか、Controlled 型や標準コンテナに任せます。
長さが実行時に決まる文字列を返す関数は、ヒープを使いますか?
new を書かなくても返せます。GNAT はターゲットによって、長さの決まっていない配列やレコードを返す関数にセカンダリスタックを使います。Linux x86_64 の GNAT 13.3 で 1 MB の文字列を 10,000 回返しても、ヒープの確保は約 1 MB の1回だけでした。pragma Restrictions (No_Secondary_Stack) を付けると、その使用はコンパイルエラーになります。
Unchecked_Deallocation で解放したあと、同じ領域を指す別のポインタはどうなりますか?
解放に渡した変数は null になりますが、同じ領域を指していた別の変数は元のアドレスを持ったままです。それを参照すると実行は erroneous(結果が規格で定まらない)になります。GNAT 13.3 で試すと例外は出ず、valgrind が解放済み領域の読み取りとして報告しました。
C++ の RAII にあたるものは Ada にありますか?
Ada.Finalization の Controlled 型と Limited_Controlled 型です。Finalize を上書きすると、スコープを出るとき、例外で抜けるときにも呼ばれます。ヒープの解放をそこに書けば、呼び出し側は解放を書かずに済みます。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る