.NET Profiler中同一函数多次触发JIT编译的解决方法
问题描述
我使用CORPROFILER API开发了自定义C++版.NET Profiler,通过插桩调用C#辅助程序集,并设置了如下事件掩码:
ProfilerInfo->SetEventMask( COR_PRF_DISABLE_TRANSPARENCY_CHECKS_UNDER_FULL_TRUST | COR_PRF_MONITOR_EXCEPTIONS | COR_PRF_USE_PROFILE_IMAGES | COR_PRF_MONITOR_JIT_COMPILATION | COR_PRF_MONITOR_MODULE_LOADS);
目前发现部分函数会反复触发JITCompilationStarted回调,导致Profiler崩溃,日志显示同一函数多次出现(如下),请问如何避免同一函数被重复插桩以及多次触发JIT编译?
日志信息
Wed May 15 12:58:07 2024 - [INFO1] System.Void Builder:EnsureCapacity(System.Int32) 2540 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void DisposableData:Release() 2540 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void Builder:EnsureCapacity(System.Int32) 6656 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void DisposableData:Release() 6656 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void Builder:EnsureCapacity(System.Int32) 2540 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void DisposableData:Release() 2540 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void Builder:EnsureCapacity(System.Int32) 6656 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void DisposableData:Release() 6656 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void Builder:EnsureCapacity(System.Int32) 2540 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void DisposableData:Release() 2540 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void Builder:EnsureCapacity(System.Int32) 6656 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void DisposableData:Release() 6656 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void Builder:EnsureCapacity(System.Int32) 2540 PID: 6020
Wed May 15 12:58:07 2024 - [INFO1] System.Void DisposableData:Release() 2540 PID: 6020
核心原因分析
同一函数反复触发JITCompilationStarted通常源于以下几点:
- 插桩修改IL后未标记函数已处理,导致CLR重新编译时再次触发回调,形成循环
- 多线程环境下未做线程安全的已插桩函数追踪,并发回调重复处理同一函数
- 忽略
isSafeToBlock参数,在不安全阻塞场景执行插桩,触发CLR重新调度JIT COR_PRF_USE_PROFILE_IMAGES启用后,镜像处理逻辑非幂等,导致模块重复加载触发编译
具体解决方案
1. 实现线程安全的已插桩函数缓存
创建全局线程安全的哈希容器记录已完成插桩的FunctionID,每次回调先校验是否已处理:
#include <unordered_map> #include <windows.h> std::unordered_map<FunctionID, bool> g_instrumentedFunctions; SRWLOCK g_instrumentedLock = SRWLOCK_INIT; void __stdcall JITCompilationStarted(FunctionID functionId, BOOL isSafeToBlock) { // 共享锁检查是否已插桩 AcquireSRWLockShared(&g_instrumentedLock); bool alreadyInstrumented = (g_instrumentedFunctions.find(functionId) != g_instrumentedFunctions.end()); ReleaseSRWLockShared(&g_instrumentedLock); if (alreadyInstrumented) return; // 执行插桩逻辑(确保逻辑幂等,不会触发重复编译) // ... // 独占锁标记已插桩 AcquireSRWLockExclusive(&g_instrumentedLock); g_instrumentedFunctions[functionId] = true; ReleaseSRWLockExclusive(&g_instrumentedLock); }
2. 严格处理isSafeToBlock参数
当isSafeToBlock为FALSE时,禁止执行任何可能阻塞的操作(如加载辅助集、复杂IL修改),直接跳过或延迟处理:
void __stdcall JITCompilationStarted(FunctionID functionId, BOOL isSafeToBlock) { if (!isSafeToBlock) { // 不安全阻塞,跳过本次回调,避免触发CLR重新调度 return; } // 执行安全的插桩逻辑 // ... }
3. 过滤无需插桩的高频函数
对Builder:EnsureCapacity这类基础高频函数直接过滤,减少不必要的插桩和编译触发:
void __stdcall JITCompilationStarted(FunctionID functionId, BOOL isSafeToBlock) { WCHAR functionName[1024]; ULONG nameLength = 1024; HRESULT hr = g_profilerInfo->GetFunctionInfo(functionId, nullptr, nullptr, functionName, &nameLength, nullptr, nullptr); if (SUCCEEDED(hr)) { if (wcscmp(functionName, L"Builder:EnsureCapacity") == 0 || wcscmp(functionName, L"DisposableData:Release") == 0) { return; } } // 执行插桩逻辑 // ... }
4. 确保IL修改逻辑幂等
使用ICorProfilerInfo::SetILFunctionBody修改IL时,只执行一次,避免重复修改导致CLR重新编译。修改前可通过GetILFunctionBody检查是否已被修改。
5. 校验COR_PRF_USE_PROFILE_IMAGES的处理逻辑
启用该标志后,模块加载时的镜像修改逻辑必须是幂等的,确保同一模块不会被重复修改。可通过ModuleID维护已处理模块的缓存,避免重复处理。
内容的提问来源于stack exchange,提问作者Durai

