[PATCH] GCC 16.2 Ada: sửa lỗi dọn dẹp đối tượng protected khi Lock_Free được chọn tự động

Tóm tắt

Tôi đã tái hiện một lỗi ở front-end Ada của GCC/GNAT 16.2.0 trên FreeBSD 15.1 amd64. Lỗi này khiến bộ nhớ tiến trình tăng không giới hạn khi các đối tượng protected đơn giản, có vòng đời ngắn, được tạo lặp đi lặp lại.

Một kiểu protected đủ điều kiện cho cơ chế Lock_Free tự động của GNAT có thể được mở rộng thành biểu diễn chứa trường _object native kiểu System.Tasking.Protected_Objects.ProtectionInitialize_Protection, rồi chỉ sau đó mới được đánh dấu là lock-free trong quá trình phân tích protected body. Việc sinh mã dọn dẹp sau đó dựa vào trạng thái Uses_Lock_Free được xác định muộn hơn và bỏ qua Finalize_Protection, dù _object native đã được tạo và khởi tạo.

Đây là lỗi ở trình biên dịch GCC upstream, nhưng hệ quả runtime có thể quan sát được đã được tái hiện trên FreeBSD. Một bản vá downstream nhỏ theo kiểu FreeBSD ports được cung cấp ở đây để một port GCC 16.2 có bật Ada có thể tích hợp bản sửa mà không cần chờ hành động từ upstream.

Phạm vi port FreeBSD

Trình biên dịch được kiểm thử là một bản dựng GCC 16.2.0 có bật Ada, dựa trên cấu hình và bộ bản vá của port GCC trên FreeBSD.

Port lang/gcc16 chính thức hiện tại không bật front-end Ada, vì vậy tôi không khẳng định rằng chính gói nhị phân lang/gcc16 hiện tại bị ảnh hưởng. Các port Ada của FreeBSD dựa trên những bản GCC cũ hơn không được khảo sát trong báo cáo này và tôi không đưa ra khẳng định nào về chúng.

Các bản vá port GCC của FreeBSD dùng cho baseline GCC 16.2 không sửa ba file nguồn GCC Ada liên quan đến lỗi này (exp_ch7.adb, exp_ch9.adb, sem_ch9.adb).

Triệu chứng của trình biên dịch có thể tái hiện nhất quán

Với GCC/GNAT 16.2.0, trường hợp mặc định được chọn tự động tạo ra kết quả sau:

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

Trường hợp mặc định không nhất quán chính là lỗi.

Mã nguồn tối thiểu:

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;

Biên dịch bằng:

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

Tree dump của GCC 16.2 chưa sửa có system.tasking.protected_objects.initialize_protection nhưng không có system.tasking.protected_objects.finalize_protection.

Hệ quả runtime trên FreeBSD

Với workload độc lập gồm 50 batch, mỗi batch 100 request, baseline GCC/GNAT 16.2.0 cho kết quả:

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

Số thread và file descriptor vẫn ổn định. Trên FreeBSD, luồng xử lý của đối tượng protected native cuối cùng sử dụng pthread_mutex_init / pthread_mutex_destroy, vì vậy việc thiếu phần finalization do trình biên dịch sinh ra biểu hiện trực tiếp qua mức tăng bộ nhớ dành cho trạng thái mutex native.

Nguyên nhân gốc

Thứ tự xử lý của front-end GCC 16.2 là:

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

Trace LLDB xác nhận rằng, với chương trình tái hiện này, Make_Initialize_Protection được thực thi trước Set_Uses_Lock_Free diễn ra sau đó.

Bản sửa

Bản sửa làm cho Is_Simple_Protected_Type phân loại kiểu dựa trên biểu diễn đã thực sự được mở rộng thay vì cờ Uses_Lock_Free có thể thay đổi về sau. Nó kiểm tra record tương ứng đã được phân tích và có chứa _object, rồi tái sử dụng Find_Protection_Type để xác nhận trường đó có kiểu System.Tasking.Protected_Objects.Protection thông thường.

Điều này giữ nguyên hành vi của Lock_Free => True được chỉ định rõ ràng vì biểu diễn đó không có trường _object, đồng thời giữ các kiểu protected có entry trên đường xử lý hiện có của chúng.

Bản vá theo kiểu FreeBSD ports được đưa vào bên dưới:

patch-gcc_ada_exp__ch7.adb

Có thể áp dụng từ thư mục gốc của mã nguồn GCC 16.2 bằng patch -p0.

Xác minh bản vá

Phần thay đổi chức năng chính xác trong bản vá FreeBSD đã được biên dịch và kiểm thử trong một cây compiler GCC/GNAT 16.2 được cô lập.

Kết quả sau khi vá:

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

Trong cùng workload, phép chạy runtime độc lập trên FreeBSD không cho thấy mức tăng tỷ lệ với số lần tạo đối tượng ở bất kỳ trường hợp nào:

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

Một tập kiểm thử hồi quy GCC Ada DejaGNU được chọn đã hoàn tất với 20 test pass đúng như dự kiến, 0 failure ngoài dự kiến và 0 test unresolved. Các phép kiểm tra bổ sung cho thấy finalization tương ứng đã được khôi phục cho protected subtype trong trường hợp chọn tự động, array và heap object; tree dump của một kiểu protected có entry giống hệt từng byte trước và sau bản vá.

Hành động đề nghị FreeBSD

Vui lòng cân nhắc đưa bản vá mã nguồn được cung cấp vào một port GCC 16.2 có bật Ada, chẳng hạn một port GNAT 16 trong tương lai hoặc tương đương, cho đến khi vấn đề được sửa trong mã nguồn trình biên dịch mà port đó sử dụng.

Tôi có thể cung cấp chương trình tái hiện runtime độc lập, số đo thô, tree dump của trình biên dịch, trace thứ tự bằng LLDB và các bản tóm tắt DejaGNU nếu chúng hữu ích cho việc đánh giá. Toàn bộ bằng chứng được lưu giữ cục bộ; bản vá mã nguồn nhỏ và báo cáo này được thiết kế để đủ cho đợt đánh giá ban đầu của FreeBSD.

Phương án Bugzilla nếu maintainer yêu cầu

Kênh thông thường của FreeBSD để gửi bản vá cho một port hiện có là Bugzilla. Nếu các maintainer Ada muốn theo dõi vấn đề tại đó sau khi chọn port bị ảnh hưởng hoặc port mục tiêu, hãy dùng:

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

Đính kèm patch-gcc_ada_exp__ch7.adb ở dạng không nén. Không thay bằng lang/gcc16 chỉ để điền trường này: bản dựng chính thức hiện tại của nó không bật Ada. Hãy dùng đúng port mục tiêu do maintainer FreeBSD lựa chọn.

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;

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