Ngày 21 tháng 10 năm 2025
Tôi đã từng đề cập đến Silhouette trước đây, thư viện mà tôi đang xây dựng để giúp việc viết .NET profilers bằng C# trở nên khả thi. Nó bao phủ hầu hết các tính năng được cung cấp bởi API profiler, và tôi thậm chí đã sử dụng nó cho các dự án chuyên nghiệp. Tuy nhiên, vẫn còn một điểm mù mà tôi không chắc làm thế nào để giải quyết.
API profiling trong .NET cho phép đăng ký các function hooks, sẽ được gọi mỗi khi một hàm được vào hoặc ra. Tính năng này hiếm khi được sử dụng vì tác động hiệu suất của nó, nhưng nó có thể hữu ích cho các tình huống rất cụ thể, chẳng hạn để viết một tracing profiler.
Mục lục
Sampling vs Tracing Profilers
Sampling profilers là loại profiler phổ biến nhất. Chúng hoạt động bằng cách định kỳ ngắt ứng dụng được profile để chụp ảnh nhanh các callstacks của nó. Điều này thường có overhead thấp và cho cái nhìn tổng quan tốt về nơi thời gian được sử dụng trong ứng dụng, rất tuyệt cho phân tích hiệu suất chung. Tuy nhiên, nó có thể bỏ lỡ các phương thức nhỏ và không thể ước tính số lần một hàm được gọi.
Tracing profilers, mặt khác, ghi lại mọi lần vào và ra của hàm. Điều này cung cấp cái nhìn toàn diện về việc thực thi ứng dụng, hữu ích chẳng hạn để đánh giá độ phức tạp thuật toán hoặc phân tích các mẫu gọi. Nhưng chúng giới thiệu một overhead cố định cho mỗi lần gọi hàm, dẫn đến việc đánh giá quá cao thời gian dành cho các hàm lá nhỏ, khiến nó không phù hợp cho phân tích hiệu suất.
Đây giống như nguyên lý bất định Heisenberg áp dụng cho profilers: bạn có thể đo chính xác thời gian thực thi của một phương thức hoặc số lần nó được gọi, nhưng không thể làm cả hai cùng lúc.
Jetbrains dotTrace là một ví dụ về profiler hỗ trợ cả hai chế độ, cho phép chuyển đổi theo nhu cầu của bạn.
Function Hooks trong .NET Profilers
Để hỗ trợ tracing profilers và các trường hợp sử dụng tương tự, .NET profiler API hiển thị phương thức SetEnterLeaveFunctionHooks (và các biến thể của nó SetEnterLeaveFunctionHooks2, SetEnterLeaveFunctionHooks3, và SetEnterLeaveFunctionHooks3WithInfo). Những phương thức này cho phép bạn đăng ký ba callbacks, sẽ được gọi mỗi khi một hàm được vào, ra, hoặc trong một tailcall. Để giảm thiểu overhead của những callbacks này, JIT không lưu các thanh ghi CPU trước khi gọi chúng như nó sẽ làm cho một lời gọi hàm bình thường. Điều này có nghĩa là các callbacks cần được viết theo một cách cụ thể, bằng assembly hoặc sử dụng các quy ước gọi đặc biệt nếu ngôn ngữ và trình biên dịch hỗ trợ chúng.
Tại thời điểm này, bạn có thể bắt đầu thấy vấn đề cho Silhouette: .NET hoàn toàn trừu tượng hóa khái niệm thanh ghi (thực tế, gần như hoàn toàn), vì vậy không có cách nào để viết code cấp thấp như vậy trong C#. Bất kỳ giải pháp nào cũng sẽ liên quan đến một stub được viết bằng assembly lưu các thanh ghi và chuyển tiếp lời gọi đến mã managed.
Tạo Static Library
Bước đầu tiên là tạo static lib sẽ chứa các assembly stubs. Có lẽ tại một số thời điểm tôi sẽ cung cấp chúng trực tiếp với Silhouette, nhưng hiện tại chúng cần được cung cấp từ bên ngoài. May mắn thay, Microsoft đã hoàn thành hầu hết công việc cho x64 trong kho lưu trữ clr-samples của họ, vì vậy chúng ta chỉ cần tải xuống tệp asmhelpers.asm và đặt nó trong một dự án C++ “Static Library” mới.
Sau đó, trong một tệp C++, chúng ta có thể khai báo các hàm EnterStub, LeaveStub và TailcallStub sẽ được gọi tự động bởi các assembly hooks. Chúng ta cũng khai báo một hàm RegisterCallbacks sẽ được gọi từ C# để đăng ký các managed callbacks. Chúng tôi sẽ gọi chúng trong việc triển khai của các hàm EnterStub, LeaveStub và TailcallStub.
Khi biên dịch, điều này sẽ tạo ra một tệp .lib, bây giờ chúng ta sẽ xem cách tích hợp nó vào profiler Silhouette.
Viết Profiler
Trong tệp dự án csproj, chúng tôi phải thêm những dòng này để liên kết tĩnh tệp .lib mà chúng tôi vừa tạo:
<ItemGroup>
<DirectPInvoke Include="__Internal" />
<NativeLibrary Include="$(MSBuildProjectDirectory)\..\x64\Release\FunctionEnterLeaveCallbacks.lib" />
</ItemGroup>
Chúng ta có thể sau đó khai báo hàm RegisterCallbacks của chúng tôi trong C#:
[DllImport("__Internal")]
private static extern void RegisterCallbacks(ref IntPtr enter, ref IntPtr leave, ref IntPtr tailCall);
Cuối cùng, chúng ta có thể triển khai các phương thức FunctionEnter và FunctionLeave để làm điều gì đó hữu ích. Trong ví dụ này, chúng ta sẽ chỉ theo dõi hàm đang được thực thi và hiển thị tên của chúng với thụt lề thích hợp. Để làm cho đầu ra dễ đọc, chúng tôi giới hạn nó vào thread chính.
Sau khi biên dịch mọi thứ, chúng ta có thể kiểm tra bằng cách gắn profiler vào một ứng dụng console đơn giản.
Ở đây, chúng ta chỉ đang kiểm tra tên của các phương thức. Kiểm tra các đối số phức tạp hơn nhiều. Nếu bạn quan tâm đến chủ đề này, bạn có thể kiểm tra bài viết đó, nơi Christophe Nasarre chỉ ra cách làm điều đó trong C++.
Toàn bộ mã nguồn của bài viết này có sẵn trên GitHub.



