0
0
Lập trình
Thaycacac
Thaycacac thaycacac

Tối ưu hóa hiệu suất với Constant Folding trong .NET 10

Đăng vào 7 tháng trước

• 5 phút đọc

Tối ưu hóa hiệu suất với Constant Folding trong .NET 10

Giới thiệu

Khi lần đầu tiên tôi xem mã lắp ráp được tạo ra bởi JIT, tôi đã tự hỏi: Tại sao bộ xử lý lại cộng sáu và hai trong một vòng lặp liên tục? Hóa ra là do trình biên dịch không nhận ra các hằng số trong mã của tôi. Sự bỏ sót này đã dẫn đến việc bỏ lỡ bộ nhớ cache, tiêu tốn pin và cuối cùng là một cuộc điều tra tốn kém. Từ đó, tôi có một quy tắc: luôn kiểm tra những gì trình biên dịch của bạn có thể tối ưu hóa, và hãy ăn mừng khi nó làm được nhiều hơn. .NET 10 mang lại nhiều điểm đáng mừng cho các nhà phát triển.

Constant Folding là gì?

Định nghĩa cơ bản

Constant folding là một kỹ thuật mà trình biên dịch sử dụng để tối ưu hóa mã nguồn bằng cách thực hiện các phép toán trên hằng số tại thời điểm biên dịch thay vì tại thời điểm thực thi. Điều này giúp giảm số lượng phép toán cần thực hiện trong thời gian chạy.

Ví dụ, khi bạn viết:

csharp Copy
int Add(int i) => i + 2 * 3;

Trình biên dịch C# sẽ chuyển đổi mã IL sao cho phép nhân xảy ra tại thời điểm biên dịch, không phải tại thời điểm chạy. Phương thức được tạo ra là i + 6.

Các giai đoạn của quá trình biên dịch

  1. Giai đoạn biên dịch C# – Chỉ các hằng số mà mã nguồn có thể nhìn thấy.
  2. Giai đoạn JIT – Có thể tối ưu hóa các giá trị static readonly, Environment.ProcessorCount, và những gì có thể được inlining.
  3. Giai đoạn liên kết / AOT – Có thể biết nhiều hơn, đặc biệt nếu mã không sử dụng được loại bỏ.

Mỗi bước cho phép runtime loại bỏ nhiều hướng dẫn hơn, mở khóa loại bỏ mã chết và giải phóng các thanh ghi cho logic kinh doanh thực sự của bạn.

Ví dụ thực tế về Constant Folding

Tối ưu hóa toán học cổ điển

Hãy xem cặp phương thức dưới đây:

csharp Copy
int M1(int i) => i + M2(2 * 3);
int M2(int j) => j * Environment.ProcessorCount;

Trình biên dịch C# sẽ gộp 2 * 3 thành 6. Tuy nhiên, lợi ích lớn hơn đến từ việc inlining. JIT trong .NET 9 có thể inlining M2, nhận thấy rằng Environment.ProcessorCount là một hằng số trên máy này (giả sử là 16), và tạo ra:

assembly Copy
lea eax, [rsi + 60h] ; i + 96 (0x60 = 96)
ret

Chỉ cần bốn byte mã máy, không có phép nhân, không có tải bộ nhớ.

Tối ưu hóa kiểm tra null

Kiểm tra null rất rẻ - cho đến khi bạn có hai cái liên tiếp trong mỗi cuộc gọi string.AsSpan():

csharp Copy
s ??= "";
return s.AsSpan();

.NET 9 phát sinh hai hướng dẫn test rsi, rsi. .NET 10 loại bỏ cái thứ hai, giảm kích thước phương thức từ 41 xuống 25 byte. Đối với một cuộc gọi đơn lẻ, bạn sẽ không bao giờ nhận thấy điều đó; nhưng trong một trình phân tích cú pháp UTF-8 chạy hàng triệu lần mỗi giây, các nhánh và áp lực T-front-end sẽ biến mất.

Tối ưu hóa biểu thức điều kiện

Xem xét:

csharp Copy
string tmp = condition ? GetOne() : GetTwo();
return tmp is not null;

Bởi vì cả hai hàm đều trả về chuỗi cứng mã hóa, tính nullness là đảm bảo. .NET 9 vẫn tạo ra biến và kiểm tra nó. .NET 10 trực tiếp chuyển đến mov eax, 1; ret - chỉ sáu byte. Engine đã biết câu trả lời trước khi mã của bạn chạy.

Tối ưu hóa so sánh SIMD

Logic vector là một sức mạnh hiệu suất - cho đến khi JIT từ chối tính toán trước các so sánh rõ ràng. Hai PR (#117099#117572) đã dạy JIT của .NET 10 gộp nhiều mối quan hệ vector hơn:

csharp Copy
Vector128<int> mask = vec == Vector128<int>.Zero; // kết quả hằng số? Gộp nó lại!

Điều này giúp giảm áp lực lên các thanh ghi, giải phóng băng thông cache và - nghịch lý - làm cho auto-vectorization dễ dàng hơn vì IR đơn giản hơn.

Tại sao bạn nên quan tâm

  1. Tăng throughput trên đường nóng – Ít hướng dẫn hơn có nghĩa là IPC tốt hơn và tiêu thụ điện năng thấp hơn.
  2. Mở khóa các tối ưu hóa khác – Khi các kiểm tra ranh giới bị loại bỏ, JIT có thể mở rộng vòng lặp mà trước đó nó đã bỏ qua.
  3. Refactor an toàn hơn – Di chuyển mã vào các phương thức trợ giúp mà không lo rằng một hằng số sẽ “thoát” và trở thành gánh nặng tại thời điểm chạy.
  4. API sạch hơn – Bạn có thể viết các điều khoản bảo vệ diễn đạt (value ?? throw) biết rằng JIT thường xóa chúng.

Hướng dẫn viết mã “thân thiện với gộp”

  • Ưu tiên static readonly hơn là singleton lười biếng khi giá trị là bất biến; JIT coi chúng như hằng số.
  • Giữ cho các hàm trợ giúp nhỏ và không trạng thái - khả năng inlining tốt hơn tương đương với khả năng gộp tốt hơn.
  • Đánh dấu các bao bọc tầm thường bằng [MethodImpl(MethodImplOptions.AggressiveInlining)] chỉ khi các phép đo chứng minh điều đó có ích.
  • Sử dụng pattern matching (is null, is not null) một cách tự do; các JIT hiện đại nhận thấy chúng.
  • Dựa vào Vector128<T>.Count, IntPtr.Size, và các “hằng số cấu hình” tương tự; .NET sẽ gộp chúng.

Giai thoại trong phát triển

Pipeline ghi log của chúng tôi xây dựng các chuỗi kết hợp trong một vòng lặp chặt chẽ. Sau khi chuyển sang .NET 10 RC1, mức sử dụng CPU trên dịch vụ thu thập giảm 8% mà không thay đổi mã nào. Nguyên nhân gốc rễ? Gộp kiểm tra null trong string.Create và các điều chỉnh SIMD trong IndexOfAny. Nhóm phát triển đã tự hào, nhưng người hùng thực sự là engine gộp hằng số của JIT.

Kết luận

Constant folding không phải là điều mới, nhưng phạm vi của nó tiếp tục mở rộng. .NET 10 gộp nhiều phép toán hơn, nhiều kiểm tra null hơn và nhiều phép toán vector hơn bất kỳ phiên bản nào trước đó - trong khi tạo ra mã máy nhỏ hơn và chặt chẽ hơn.

Lần tới khi profiler của bạn cho thấy sự gia tăng bất ngờ trong một phương thức tưởng chừng như không quan trọng, hãy xem xét mã lắp ráp. Nếu bạn phát hiện một phép nhân với mười sáu hoặc một kiểm tra null dư thừa, hãy nâng cấp runtime - hoặc nộp một vấn đề và xem đội ngũ .NET gộp hằng số của bạn trong bản xem trước tiếp theo.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào