Newtonsoft JsonSerializerSettings重复创建为何引发高CPU与JIT开销?
问题场景
通过实现SnakeCaseAttribute(继承自ActionFilterAttribute),让部分控制器返回snake_case格式的JSON。原代码在每次请求的OnActionExecuted方法中新建JsonSerializerSettings,导致CPU占用极高;改为静态实例后,性能提升10-15倍,JIT编译耗时从每秒约1000ms降至约10ms。
原代码(性能问题版本)
public class SnakeCaseAttribute : ActionFilterAttribute { public override void OnActionExecuted(ActionExecutedContext context) { if (context.Result is ObjectResult objectResult) { objectResult.Formatters.Add(new NewtonsoftJsonOutputFormatter( new JsonSerializerSettings // 性能瓶颈所在 { ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy { OverrideSpecifiedNames = false }, }, }, context.HttpContext.RequestServices.GetRequiredService<ArrayPool<char>>(), context.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value)); } else { base.OnActionExecuted(context); } } }
优化后代码
public class SnakeCaseAttribute : ActionFilterAttribute { // 静态实例,全局复用 private static readonly JsonSerializerSettings _snakeCaseSettings = new JsonSerializerSettings { ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy { OverrideSpecifiedNames = false }, }, }; public override void OnActionExecuted(ActionExecutedContext context) { if (context.Result is ObjectResult objectResult) { objectResult.Formatters.Add(new NewtonsoftJsonOutputFormatter( _snakeCaseSettings, // 复用静态配置 context.HttpContext.RequestServices.GetRequiredService<ArrayPool<char>>(), context.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value)); } else { base.OnActionExecuted(context); } } }
底层逻辑与性能提升原因
1. DefaultContractResolver的契约缓存机制
Newtonsoft.Json的DefaultContractResolver核心工作是为每个.NET类型生成序列化契约——包含属性名映射(比如驼峰转蛇形)、序列化规则、忽略项等元数据。为了避免重复计算,DefaultContractResolver会默认缓存这些契约。
但如果每次请求都新建DefaultContractResolver实例:
- 缓存无法复用,每处理一个请求都要重新遍历目标类型的属性、执行字符串转换(驼峰→蛇形)、生成全新的契约
- 这些操作涉及大量反射和字符串处理,是CPU消耗的主要来源
2. JsonSerializerSettings的重复初始化开销
每次新建JsonSerializerSettings时,除了实例化DefaultContractResolver,还要初始化一系列默认配置(比如日期格式、空值处理规则)、绑定内部事件等。单次开销虽小,但高并发场景下会累积成显著的CPU负载。
3. JIT编译的重复执行
CLR的JIT编译器会在方法首次执行时将IL代码编译为机器码。原代码中,每次新建JsonSerializerSettings和NewtonsoftJsonOutputFormatter,都会触发相关序列化方法的重复JIT编译;而静态实例只会在应用启动时编译一次,后续请求直接复用已编译的机器码,这就是JIT耗时大幅下降的关键。
4. 线程安全性说明
JsonSerializerSettings和DefaultContractResolver在初始化完成后是线程安全的(只要不动态修改配置),因此使用静态实例不会引发并发问题。
内容的提问来源于stack exchange,提问作者Maciej Pszczolinski

