[PATCH] GCC 16.2 Ada: 自動 Lock_Free 保護オブジェクトのクリーンアップ漏れを修正

概要

FreeBSD 15.1 amd64 上で、寿命の短い単純な保護オブジェクトを繰り返し生成すると プロセスメモリが無制限に増加する GCC/GNAT 16.2.0 Ada フロントエンドの不具合を 再現しました。

GNAT による自動 Lock_Free 実装の対象となる protected type は、まずネイティブの System.Tasking.Protected_Objects.Protection 型の _object フィールドと Initialize_Protection を含む形に展開され、その後 protected body の解析時に lock-free と判定される場合があります。クリーンアップ処理は後から設定された Uses_Lock_Free の状態を信頼するため、ネイティブの _object がすでに生成・初期化 されているにもかかわらず Finalize_Protection を省略します。

これは GCC 本体のコンパイラ不具合ですが、実行時に現れる影響は FreeBSD 上で 再現できました。そのため、upstream 側の対応を待たずに Ada 対応 GCC 16.2 port へ修正を 取り込めるよう、小さな FreeBSD ports 形式の downstream 向けパッチをここに 示します。

FreeBSD port の対象範囲

検証したコンパイラは、FreeBSD の GCC port の設定とパッチセットを基に構築した Ada 対応 GCC 16.2.0 です。

現在公式の lang/gcc16 port では Ada フロントエンドが有効になっていないため、 現在の lang/gcc16 バイナリパッケージ自体が影響を受けるとは主張しません。 古い GCC リリースを基にした FreeBSD の Ada ports は今回調査しておらず、それらに ついても何ら主張しません。

GCC 16.2 の基準環境で使用した FreeBSD GCC port のパッチは、この不具合に関係する 3つの GCC Ada ソース (exp_ch7.adb, exp_ch9.adb, sem_ch9.adb) を変更していません。

確実に再現できるコンパイラ症状

GCC/GNAT 16.2.0 では、デフォルトの自動判定の場合に次の結果になります。

case                 Initialize  Finalize  lock-free ops  Protection field
default (automatic)         yes        no   yes            present
Lock_Free => True            no        no   yes            absent
Lock_Free => False          yes       yes   no             present

このデフォルトの場合の不整合が不具合です。

最小ソースコードは次のとおりです。

procedure Protected_Finalize is
   protected type Prot is
      procedure Touch;
   private
      Value : Integer := 0;
   end Prot;

   protected body Prot is
      procedure Touch is
      begin
         Value := Value + 1;
      end Touch;
   end Prot;

   Object : Prot;
begin
   Object.Touch;
end Protected_Finalize;

次のようにコンパイルします。

gcc16 -c -O0 -fdump-tree-original protected_finalize.adb

未修正の GCC 16.2 のツリーダンプには system.tasking.protected_objects.initialize_protection は含まれますが、 system.tasking.protected_objects.finalize_protection は含まれません。

FreeBSD での実行時の影響

1バッチ100リクエストを50バッチ実行するスタンドアロンのワークロードでは、 GCC/GNAT 16.2.0 のベースラインで次の結果になりました。

case                    constructions   RSS growth   VSZ growth
plain_eight                        0       +4 KiB       0 KiB
direct_protected_one           5,000     +628 KiB    +528 KiB
protected_one                  5,000     +628 KiB    +528 KiB
protected_eight               40,000    +5012 KiB   +5016 KiB
lock_free_eight              40,000       +4 KiB       0 KiB

スレッド数とファイルディスクリプタ数は安定したままでした。FreeBSD ではネイティブの protected object の経路が最終的に pthread_mutex_init / pthread_mutex_destroy を 使用するため、生成されるべき finalization の欠落がネイティブの mutex 状態に伴う メモリ増加として直接観測されます。

根本原因

GCC 16.2 フロントエンドでは、処理順序が次のようになっています。

Expand_N_Protected_Type_Declaration
  -> Uses_Lock_Free is still False
  -> generate native Protection _object
Build_Record_Init_Proc
  -> generate Initialize_Protection
Analyze_Protected_Body
  -> automatically Set_Uses_Lock_Free
cleanup generation
  -> Is_Simple_Protected_Type tests not Uses_Lock_Free
  -> skip existing Finalize_Protection path

LLDB のトレースでも、この再現プログラムでは Make_Initialize_Protection への到達が、 後続の Set_Uses_Lock_Free より先であることを確認しました。

修正

この修正では Is_Simple_Protected_Type が、後から変化する Uses_Lock_Free フラグでは なく、実際に生成済みの展開表現に基づいて型を分類するようにします。対応するレコードが 解析済みで _object を含むことを確認した上で、既存の Find_Protection_Type を再利用し、 そのフィールドが通常の System.Tasking.Protected_Objects.Protection 型であることを 確認します。

明示的な Lock_Free => True の表現には _object フィールドがないため、その動作は 変わりません。entry を持つ protected type も従来の経路のままです。

FreeBSD ports 形式のパッチは次のとおりです。

patch-gcc_ada_exp__ch7.adb

GCC 16.2 のソースルートから patch -p0 で適用できます。

パッチの検証

FreeBSD パッチに含まれる実際のコンパイラ変更と同一の hunk を、隔離した GCC/GNAT 16.2 コンパイラツリーでビルドしてテストしました。

パッチ適用後の結果は次のとおりです。

default (automatic)   Initialize=yes  Finalize=yes  lock-free=yes
Lock_Free => True     Initialize=no   Finalize=no   lock-free=yes
Lock_Free => False    Initialize=yes  Finalize=yes  lock-free=no

スタンドアロンの FreeBSD ランタイムを同じワークロードで実行したところ、 どのケースでも構築回数に比例するメモリ増加は見られませんでした。

plain_eight                RSS +4 KiB, VSZ 0 KiB
direct_protected_one       RSS +4 KiB, VSZ 0 KiB
protected_one              RSS +4 KiB, VSZ 0 KiB
protected_eight            RSS +4 KiB, VSZ 0 KiB
lock_free_eight            RSS +4 KiB, VSZ 0 KiB

選択した GCC Ada DejaGNU 回帰テストセットは、期待どおり20件成功、予期しない失敗0件、 unresolved test 0件で完了しました。追加検証では、自動判定される protected subtype、 配列、ヒープオブジェクトについて対応する finalization が復元されることを確認し、entry を 持つ protected type のツリーダンプはパッチ適用前後でバイト単位に同一でした。

FreeBSD への依頼

この問題が対象 port で使用されるコンパイラソース側で修正されるまで、Ada 対応 GCC 16.2 port(たとえば将来の GNAT 16 port または同等のもの)に、このソースパッチを 取り込むことをご検討ください。

レビューに必要であれば、スタンドアロンのランタイム再現プログラム、生の測定値、 コンパイラのツリーダンプ、LLDB の処理順序トレース、DejaGNU のサマリーを提供できます。 完全な検証資料はローカルに保管しており、ここでは初期の FreeBSD レビューに十分なものとして、 小さなソースパッチとこのレポートを提示しています。

Maintainer から求められた場合の Bugzilla 代替手順

FreeBSD で既存 port にパッチを適用する通常の経路は Bugzilla です。Ada maintainer が 影響対象または適用対象の port を選んだ後、Bugzilla での追跡を希望する場合は次を 使用します。

Product:   Ports & Packages
Component: Individual Port(s)
Summary:   [PATCH] lang/<maintainer-selected-port>: fix GCC 16.2 Ada protected cleanup

patch-gcc_ada_exp__ch7.adb は圧縮せずに添付してください。フィールドを埋めるためだけに lang/gcc16 を指定しないでください。現在の公式ビルドでは Ada が有効になっていません。 FreeBSD maintainer が選んだ正確な対象 port を使用してください。

patch-gcc_ada_exp__ch7.adb:

--- gcc/ada/exp_ch7.adb.orig
+++ gcc/ada/exp_ch7.adb
@@ -5432,12 +5432,34 @@
    ------------------------------

    function Is_Simple_Protected_Type (T : Entity_Id) return Boolean is
+      Comp    : Entity_Id;
+      Rec_Typ : Entity_Id;
+
    begin
-      return
-        Is_Protected_Type (T)
-          and then not Uses_Lock_Free (T)
-          and then not Has_Entries (T)
-          and then Is_RTE (Find_Protection_Type (T), RE_Protection);
+      if not Is_Protected_Type (T) or else Has_Entries (T) then
+         return False;
+      end if;
+
+      Rec_Typ := Corresponding_Record_Type (T);
+
+      if No (Rec_Typ) or else not Analyzed (Rec_Typ) then
+         return False;
+      end if;
+
+      --  Base the decision on the representation that was actually built.
+      --  Automatic lock-free selection may happen after the corresponding
+      --  record and its initialization procedure have already been created.
+
+      Comp := First_Component (Rec_Typ);
+      while Present (Comp) loop
+         if Chars (Comp) = Name_uObject then
+            return Is_RTE (Find_Protection_Type (T), RE_Protection);
+         end if;
+
+         Next_Component (Comp);
+      end loop;
+
+      return False;
    end Is_Simple_Protected_Type;

    -------------------------------