创建LogEventInfo时反射的性能影响及优化实现方案咨询
优化NLog LogEventInfo生成的性能方案
针对百万级调用场景,反射带来的性能损耗确实不可忽视,以下是几种可行的优化方案,按性能从高到低排序:
1. 手动属性映射(性能最优)
如果ILogItem的具体实现类数量不多,直接编写手动赋值的代码,完全避免反射开销。示例:
// 针对具体实现类的扩展方法 public static LogEventInfo ToLogEventInfo(this OrderLogItem data, string message, Exception ex = null) { var eventInfo = new LogEventInfo(); if (ex != null) eventInfo.Exception = ex; data.EventMessage = message; data.LogTime = DateTime.UtcNow; // 替换原转换逻辑,直接获取UTC时间更高效 data.LogId = Guid.NewGuid(); // 手动映射所有属性 eventInfo.Properties[nameof(data.LogId)] = data.LogId; eventInfo.Properties[nameof(data.LogTime)] = data.LogTime; eventInfo.Properties[nameof(data.EventMessage)] = data.EventMessage; // 其他属性依次添加... return eventInfo; }
优势:运行时无任何反射开销,性能达到极致;缺点:新增ILogItem实现类时需要同步维护映射代码,灵活性较差。
2. 缓存属性访问委托(平衡性能与灵活性)
通过表达式树编译属性访问的委托,首次调用时编译并缓存,后续复用编译后的委托,性能接近手动映射,同时保留灵活性。
首先创建缓存类:
public static class LogEventInfoMapperCache { private static readonly Dictionary<Type, Action<ILogItem, LogEventInfo>> _mapperCache = new(); private static readonly object _lockObj = new(); public static Action<ILogItem, LogEventInfo> GetMapper(Type logItemType) { if (_mapperCache.TryGetValue(logItemType, out var mapper)) return mapper; lock (_lockObj) { if (_mapperCache.TryGetValue(logItemType, out mapper)) return mapper; mapper = CreatePropertyMapper(logItemType); _mapperCache.Add(logItemType, mapper); return mapper; } } private static Action<ILogItem, LogEventInfo> CreatePropertyMapper(Type logItemType) { var dataParam = Expression.Parameter(typeof(ILogItem), "data"); var eventParam = Expression.Parameter(typeof(LogEventInfo), "eventInfo"); // 将ILogItem转换为具体类型 var castData = Expression.TypeAs(dataParam, logItemType); var assignmentExpressions = new List<Expression>(); var properties = logItemType.GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var prop in properties) { // 生成 eventInfo.Properties[prop.Name] = data.PropName 的表达式 var propNameConst = Expression.Constant(prop.Name); var propValue = Expression.Property(castData, prop); var propertiesIndex = Expression.Property(eventParam, nameof(LogEventInfo.Properties)); var assignment = Expression.Assign( Expression.Property(propertiesIndex, "Item", propNameConst), propValue ); assignmentExpressions.Add(assignment); } var block = Expression.Block(assignmentExpressions); return Expression.Lambda<Action<ILogItem, LogEventInfo>>(block, dataParam, eventParam).Compile(); } }
然后修改原扩展方法:
public static LogEventInfo ToLogEventInfo(this ILogItem data, string message, Exception ex = null) { var eventInfo = new LogEventInfo(); if (ex != null) eventInfo.Exception = ex; if (data != null) { data.EventMessage = message; data.LogTime = DateTime.UtcNow; // 优化UTC时间获取 data.LogId = Guid.NewGuid(); // 使用缓存的委托完成属性映射 var mapper = LogEventInfoMapperCache.GetMapper(data.GetType()); mapper(data, eventInfo); } else { if (!string.IsNullOrEmpty(message)) eventInfo.Message = message; } return eventInfo; }
优势:仅首次调用某类型时存在编译开销,后续调用性能接近手动映射;支持任意ILogItem实现类,无需手动维护映射代码。
3. 源生成器(C# 9+ 推荐)
利用C#源生成器在编译时自动生成ILogItem实现类的映射代码,既保留手动映射的性能,又无需手动维护代码。
核心思路:
- 创建实现
ISourceGenerator的类,扫描所有实现ILogItem的类型 - 为每个类型生成对应的
ToLogEventInfo扩展方法,代码逻辑与手动映射一致 - 编译时自动将生成的代码纳入项目,运行时无反射开销
优势:性能与手动映射一致,且新增ILogItem实现类时自动生成映射代码,零维护成本;缺点:需要熟悉C#源生成器的编写,对项目环境有版本要求(C# 9+)。
额外小优化
原代码中TimeZoneInfo.ConvertTime(DateTime.Now, TimeZoneInfo.Utc)可以直接替换为DateTime.UtcNow,两者结果一致,但后者无需转换操作,性能更优。
内容的提问来源于stack exchange,提问作者wadefanyaoxia
相关产品推荐
相关产品推荐

