更新履歴(初版のみ・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 のメモリ管理を次の順に説明します。
- 値をスタックに置く。長さが実行時に決まる値も、
newを書かずに関数から返せる。 - ヒープが要るときはアクセス型を使い、何を指せるか、どこまで寿命があるかを型で決める。
- 解放は Unchecked_Deallocation で行う。名前のとおり検査はないので、直接呼ぶ場所を減らす。
- 解放を Controlled 型の Finalize に任せる。C++ の RAII と同じ考え方。
- 確保先をストレージプールで決める。上限を付けたり、確保の回数を数えたりできる。
例はすべて Ubuntu 24.04 の GNAT 13.3.0 でコンパイルして実行し、出力をそのまま載せています。メモリの誤用の確認には valgrind 3.22.0 を使いました。言語の規則は Ada 2022 の参照マニュアル(RM)で確かめています。Ada そのものの特徴は「Ada言語の魅力」で紹介しています。
Adaで値が置かれる場所
Ada では、宣言した変数は通常スタックに置かれます。長さが実行時に決まる配列も、new を書かずに変数として宣言できます。ヒープ(Ada の用語ではストレージプール)を使うのは、new を書いたときです2。
flowchart TB
accTitle: Adaで値が置かれる場所
accDescr: 宣言した変数はスタックに、長さの決まらない戻り値はGNATではセカンダリスタックに、newで作った値はアクセス型のストレージプールに置かれることを示す。
D["宣言した変数"] --> S["スタック"]
F["長さの決まらない戻り値"] --> SS["セカンダリスタック(GNAT)"]
N["newで作った値"] --> P["ストレージプール"]
P --> M["既定のプールは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 で作った値と違い、プログラムの側で解放を書く必要はありません。
flowchart TB
accTitle: セカンダリスタックの使われ方
accDescr: 関数が長さの決まらない値を返すとセカンダリスタックに積まれ、呼び出し側のブロックで使われ、ブロックを出ると戻されることを示す。
C["Repeatを呼ぶ"] --> R["結果をセカンダリスタックに積む"]
R --> U["declareブロックでLineとして使う"]
U --> E["ブロックを出る"]
E --> B["積んだ領域を戻す"]
図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
この規則のおかげで、どの変数がポインタで指されうるかを宣言から読み取れます。
flowchart TB
accTitle: アクセス型の種類と指せるもの
accDescr: プール固有のアクセス型はnewで作った値だけを指し、汎用のアクセス型はnewで作った値に加えてaliasedを付けた変数も指せることを示す。
PS["プール固有のアクセス型(access)"] --> NEW["newで作った値"]
GA["汎用のアクセス型(access all)"] --> NEW
GA --> AL["aliasedを付けた変数"]
PL["aliasedのない変数"] -.-> X["どのアクセス型も指せない"]
図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 では保存しようとした行で止まります。
flowchart TB
accTitle: アクセシビリティ検査が行われる時点
accDescr: 寿命の短い変数を指す値を寿命の長いアクセス型に入れようとすると、コンパイル時に分かればコンパイルエラーになり、匿名アクセス型の引数のように実行時まで分からなければProgram_Errorになることを示す。
A["寿命の短い変数を指す値を保存する"] --> Q{"コンパイル時に寿命が分かるか"}
Q -->|"分かる"| CE["コンパイルエラー"]
Q -->|"分からない(匿名アクセス型の引数)"| RT["実行時に検査する"]
RT --> PE["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 を呼んでいることも分かります。
flowchart TB
accTitle: Unchecked_Deallocationのあとに残るポインタ
accDescr: Free(A)でAはnullになるが、同じ領域を指していたBは元のアドレスを持ったままで、Bを参照するとerroneousになり、valgrindは解放済み領域の読み取りとして報告することを示す。
S["AとBが同じ値を指す"] --> F["Free(A)"]
F --> AN["Aはnullになる"]
F --> BD["Bは元のアドレスのまま"]
BD --> R["B.allを読む"]
R --> E["erroneous(結果は定まらない)"]
E -.-> V["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 で証明する」で扱っています。
flowchart TB
accTitle: 同じ誤りをAdaとSPARKで扱ったときの違い
accDescr: 2つのポインタで同じ領域を指して片方を解放する誤りが、Adaとして実行すると検出されずに解放済み領域を読み、SPARKとして検査するとGNATproveが所有権の移動を理由にエラーにすることを示す。
P["2つのポインタで同じ領域を指して解放する"] --> ADA["Adaとして実行する"]
P --> SP["SPARKとして検査する"]
ADA --> R["検出されずに解放済み領域を読む"]
SP --> E["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が呼ばれています。
flowchart TB
accTitle: Controlled型の手続きが呼ばれる順番
accDescr: 初期値なしの宣言でInitialize、代入では上書きされる側のFinalizeとコピー先のAdjust、スコープを出るときと例外で抜けるときに宣言と逆順のFinalizeが呼ばれることを示す。
D["初期値なしで宣言する"] --> I["Initialize"]
I --> AS["B := Aを実行する"]
AS --> F1["Bの古い値をFinalize"]
F1 --> AD["コピーした値をAdjust"]
AD --> L["スコープを出る・例外で抜ける"]
L --> F2["宣言と逆の順に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。メモリの上限を決めておきたい組込み機器や、長時間動く処理で、型ごとの使用量に枠を設けたいときに使えます。
flowchart TB
accTitle: Storage_Sizeを指定したアクセス型の確保
accDescr: Storage_Sizeを指定したアクセス型でnewを実行すると、プールに空きがあれば確保し、使い切っていればStorage_Errorになり、アクセス型を宣言したスコープを出るとプールの領域が回収されることを示す。
N["Int_Ptrでnewを実行する"] --> Q{"プールに空きがあるか"}
Q -->|"ある"| A["確保する"]
Q -->|"ない"| E["Storage_Error"]
A --> S["アクセス型を宣言したスコープを出る"]
S --> R["プールの領域を回収する"]
図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 バイトでした。要素数より大きい分には、配列の添字の範囲を持つための領域などが含まれると考えられますが、内訳は処理系が決めるもので、この記事では確かめていません。
flowchart TB
accTitle: 数えるプールを通る確保と解放
accDescr: Buffer型のnewとFreeがアクセス型に指定したCounting_PoolのAllocateとDeallocateを呼び、Counting_Poolは回数を数えてからGNATの既定のプールに確保と解放を任せることを示す。
N["Buffer内のnew"] --> A["Counting_PoolのAllocate"]
FR["FinalizeのFree"] --> D["Counting_PoolのDeallocate"]
A --> C["回数と使用量を数える"]
D --> C
C --> G["GNATの既定のプール"]
G --> M["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のジェネリックプログラミング」で説明しています。
どの仕組みを使うかの目安
ここまでの仕組みは上から順に検討すると選びやすくなります。
flowchart TB
accTitle: Adaでメモリの扱い方を選ぶ順序
accDescr: まず値として宣言できるかを考え、要素を増減するならコンテナ、独自の構造が要るならアクセス型とControlled型、確保先や上限を決めたいならストレージプールを使う、という検討の順序を示す。
Q1{"値として宣言・返却できるか"} -->|"できる"| V["変数・戻り値のまま扱う"]
Q1 -->|"できない"| Q2{"列や辞書で足りるか"}
Q2 -->|"足りる"| C["Ada.Containers"]
Q2 -->|"足りない"| AC["アクセス型とControlled型"]
AC --> Q3{"確保先や上限を決めたいか"}
Q3 -->|"決めたい"| SP["ストレージプール"]
図10: 値で扱えなければコンテナ、それでも足りなければアクセス型と Controlled 型を検討する。
- 長さが実行時に決まるだけなら、値のまま宣言し、関数から返します。
newは要りません。 - 要素を増やしたり消したりするなら、
Ada.Containersを使います。 - 木やグラフのように独自の構造が必要なら、アクセス型で作り、
Freeを呼ぶのは Controlled 型のFinalizeなどの少数の場所に限ります。aliasedとnot nullで、指せるものとnullの有無を型に書きます。 - 確保の上限や確保先を決めたいときは、
Storage_Sizeかストレージプールを指定します。
Ada の型でメモリの使い方を表しても、Unchecked_Deallocation の誤用まではコンパイラーが止めません。その部分を少なくし、残った箇所は valgrind や確保を数えるプールで確かめます。所有権の規則まで検査したい部分は SPARK で書く方法もあります。
.NET のように GC のある環境でメモリが増え続けるときの調べ方は、「.NETでGC待ちとメモリリークを見分ける」にまとめています。
関連記事
- Ada言語の魅力 ── 型で設計を語り、数十年動き続けるソフトウェアを支える言語
- Adaのジェネリックプログラミング ── 型で契約を書き、再利用をゼロコストで実現する
- AIが書いたコードを SPARK で証明する
- Ada と C / C# をつなぐ ── DLL を作って呼び出す
- Ada と Rust の安全性の考え方の違い
- マルチスレッドの実務ベストプラクティス C++編 ── RAIIとjthreadで事故を構造から消す
参考リンク
-
GNAT Reference Manual, System.Pool_Global (s-pooglo.ads). 既定のストレージプールと同じプールで、malloc と free を使い、自動での回収はしないこと。 ↩ ↩2 ↩3
-
Ada Reference Manual (Ada 2022), 3.10 Access Types. プール固有のアクセス型と汎用のアクセス型、
aliased、null 除外(not null)。 ↩ ↩2 ↩3 ↩4 -
GNAT User’s Guide, Switches for gnatbind.
-Dスイッチの説明にある、セカンダリスタックの役割と実装。 ↩ ↩2 -
GNAT Reference Manual, No_Secondary_Stack. 生成コードがセカンダリスタックを参照しないことをコンパイル時に保証する制限。 ↩ ↩2
-
Ada Reference Manual (Ada 2022), 3.10.2 Operations of Access Types. アクセシビリティレベルと、検査に失敗したときの
Program_Error。 ↩ ↩2 -
Ada Reference Manual (Ada 2022), 13.11.2 Unchecked Storage Deallocation.
Free (X)のあとXが null になること、ダングリング参照と erroneous な実行。 ↩ ↩2 -
Annotated Ada Reference Manual (Ada 2022), 13.11.3 Default Storage Pools. 「Wording Changes from Ada 2005」の 9.d/3。Ada 2012 でガベージコレクションの記述が削除され、サブプールが用意されたこと。 ↩
-
AdaCore, SPARK User’s Guide, Language Restrictions. アクセス型と割り当ては所有権の規則に従い、確保した領域に読み書きできるアクセス値は同時に1つだけであること(Memory Ownership Policy)。 ↩
-
Ada Reference Manual (Ada 2022), 7.6 Assignment and Finalization.
ControlledとLimited_Controlled、Initialize・Adjust・Finalizeが呼ばれる条件と代入の手順。 ↩ ↩2 -
Ada Reference Manual (Ada 2022), 13.11 Storage Management.
Root_Storage_PoolとAllocate・Deallocate・Storage_Size、Storage_Sizeを指定した型のプールとStorage_Error。 ↩ ↩2 ↩3 ↩4 -
GNAT User’s Guide, The GNAT Debug Pool Facility. 参照のたびに、正しく確保された領域を指しているかを確かめる
GNAT.Debug_Pools。 ↩ -
Ada Reference Manual (Ada 2022), 13.11.4 Storage Subpools.
System.Storage_Pools.Subpoolsによるサブプール。 ↩ -
Ada Reference Manual (Ada 2022), A.18 Containers.
Ada.Containersのベクタ・リスト・マップ・集合と、Indefinite_の付いたコンテナ。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
Alire で Ada のプロジェクトを始める ── プロジェクト作成、依存の追加、ビルドプロファイル、GNATproveまで
AdaのパッケージマネージャーAlireで、プロジェクトの作成、GNATの自動取得、依存クレートの追加、ビルドプロファイルの切り替え、GNATproveによる証明までを、alr 2.1で実行した出力とともに説明します。
Ada 2022 の新しい機能 ── GNAT で動くものと、まだ書けない並列構文
Ada 2022で加わった宣言式、差分集成体、全型の'Image、'Reduce、Big_Integers、Jorvikプロファイルを、GNAT 13.3と15.3で動かした結果とともに説明します。並列ブロックと並列ループはGNATでは受け付けられませんでした。
Ada と C / C# をつなぐ ── DLL を作って呼び出す
Adaで共有ライブラリを作り、CとC#から呼ぶ手順を実際に動かして説明します。Interfaces.CとConvention => C、gprbuildの設定、初期化、文字列とメモリの持ち主、例外の扱い、AdaからWindowsのDLLを呼ぶ方法まで扱います。
Adaのジェネリックプログラミング ── 型で契約を書き、再利用をゼロコストで実現する
Adaのジェネリックプログラミングを、総称サブプログラム、総称パッケージ、仮サブプログラム、型カテゴリ、実務上の設計指針まで体系的に解説します。型安全な再利用とゼロコスト抽象化の考え方を整理します。
Ada言語の魅力 ── 型で設計を語り、数十年動き続けるソフトウェアを支える言語
Ada言語の魅力を紹介します。強い型付け、範囲制約、パッケージによる仕様と実装の分離、契約による設計、言語組み込みのタスク、SPARKによる形式検証、GNATとAlireでの開発環境まで、高信頼ソフトウェアを支える設計思想を整理します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
技術相談・設計レビュー
AdaやCで書かれた制御系・組込み系のコードで、どの値をヒープに置き、誰がいつ解放するかを設計し直したい場合に、技術相談・設計レビューとして扱えます。
不具合調査・原因解析
長く動かすとメモリが増える、解放したはずの領域を読んで値が壊れる、といった症状の原因を調べるのは、不具合調査・原因解析として進めやすい題材です。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- 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 を上書きすると、スコープを出るとき、例外で抜けるときにも呼ばれます。ヒープの解放をそこに書けば、呼び出し側は解放を書かずに済みます。