[PATCH] GCC 16.2 Ada: 자동 Lock_Free 보호 객체의 정리 누락 수정
요약
FreeBSD 15.1 amd64에서 GCC/GNAT 16.2.0 Ada 프런트엔드 결함을 재현했습니다. 이 결함은 수명이 짧은 단순 보호 객체를 반복해서 생성할 때 프로세스 메모리가 제한 없이 증가하게 합니다.
GNAT의 자동 Lock_Free 구현 대상이 될 수 있는 보호 타입은 네이티브
System.Tasking.Protected_Objects.Protection _object 필드와
Initialize_Protection이 있는 형태로 확장된 뒤, 보호 본문 분석 단계에서
나중에 lock-free로 표시될 수 있습니다. 이후 정리 코드 생성은 이 나중의
Uses_Lock_Free 상태를 신뢰하여, 네이티브 _object가 이미 생성되고
초기화되었는데도 Finalize_Protection을 생략합니다.
이 문제는 업스트림 GCC 컴파일러 결함이지만, 런타임에서 나타나는 결과는 FreeBSD에서 재현되었습니다. 업스트림 조치를 기다리지 않고 Ada를 활성화한 GCC 16.2 포트에서 수정할 수 있도록 작은 FreeBSD ports 형식의 다운스트림 패치를 함께 제공합니다.
FreeBSD 포트 범위
시험한 컴파일러는 FreeBSD GCC 포트의 구성/패치 세트를 바탕으로 만든 Ada 활성화 GCC 16.2.0 빌드입니다.
현재 공식 lang/gcc16 포트는 Ada 프런트엔드를 활성화하지 않으므로, 현재
lang/gcc16 바이너리 패키지 자체가 영향을 받는다고 주장하는 것이 아닙니다.
이 보고서에서는 더 오래된 GCC 릴리스를 기반으로 한 FreeBSD Ada 포트를
조사하지 않았으며, 해당 포트들에 대해서는 어떠한 주장도 하지 않습니다.
GCC 16.2 기준선에 사용한 FreeBSD GCC 포트 패치는 이 결함과 관련된 세 개의
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의 tree dump에는
system.tasking.protected_objects.initialize_protection은 있지만
system.tasking.protected_objects.finalize_protection은 없습니다.
FreeBSD 런타임 결과
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에서
네이티브 보호 객체 경로는 최종적으로 pthread_mutex_init /
pthread_mutex_destroy를 사용하므로, 생성되어야 할 finalization의 누락이
네이티브 뮤텍스 상태용 메모리 증가로 직접 나타납니다.
근본 원인
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 추적을 통해 재현 코드에서 나중의 Set_Uses_Lock_Free보다 먼저
Make_Initialize_Protection에 도달한다는 것도 확인했습니다.
수정
이 수정은 Is_Simple_Protected_Type이 나중에 바뀔 수 있는
Uses_Lock_Free 플래그가 아니라 실제로 생성된 확장 표현을 기준으로 타입을
분류하게 합니다. 해당 레코드가 분석되었고 _object를 포함하는지 확인한 뒤,
Find_Protection_Type을 재사용하여 그 필드가 일반적인
System.Tasking.Protected_Objects.Protection 타입인지 확인합니다.
명시적인 Lock_Free => True 표현에는 _object 필드가 없으므로 기존 동작이
그대로 유지되며, entry가 있는 보호 타입도 기존 경로를 그대로 사용합니다.
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 테스트 0건으로 완료되었습니다. 추가 검증에서는 자동 보호 subtype, 배열, 힙 객체에 대해 짝을 이루는 finalization이 복구되었고, entry가 있는 보호 타입의 tree dump는 패치 전후가 바이트 단위로 동일했습니다.
FreeBSD에 요청하는 조치
이 문제가 해당 포트가 사용하는 컴파일러 소스에서 수정될 때까지, Ada가 활성화된 GCC 16.2 포트(예를 들어 향후 GNAT 16 포트 또는 동등한 포트)에 제공한 소스 패치를 포함하는 방안을 검토해 주시기 바랍니다.
검토에 도움이 된다면 독립 런타임 재현 코드, 원시 측정값, 컴파일러 tree dump, LLDB 처리 순서 추적, DejaGNU 요약을 제공할 수 있습니다. 전체 증거 자료는 로컬에 보관하고 있으며, 작은 소스 패치와 이 보고서만으로도 초기 FreeBSD 검토에는 충분하도록 작성했습니다.
Maintainer가 요청할 경우의 Bugzilla 대안
기존 FreeBSD 포트를 수정하는 일반적인 경로는 Bugzilla입니다. Ada maintainer가 영향을 받거나 대상으로 삼을 포트를 선택한 뒤 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가 선택한 정확한 대상 포트를 사용해야
합니다.
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;
-------------------------------