C#中Func<>变量为null时为何仍发生内存分配?
问题复现
使用BenchmarkDotNet内存诊断工具分析代码时发现,以下C#扩展方法即使传入parameters = null调用,仍会产生12B的内存分配:
public static void Usage(this ILogger logger, LogLevel logLevel, string area, string operation, Dictionary<string, string> parameters) { Func<Dictionary<string, string>> function = null; if (parameters != null) { function = new Func<Dictionary<string, string>>(() => { return parameters; }); } logger.Usage(logLevel, area, operation, function); }
如果移除对function变量的赋值逻辑,内存分配量会降至0。
查看生成的IL代码可见两处newobj指令:
IL_0000: newobj instance void MIT.Logging.Infrastructure.LoggerExtensions/'<>c__DisplayClass0_0'::.ctor()
IL_001f: newobj instance void class [mscorlib]System.Func1<class [mscorlib]System.Collections.Generic.Dictionary2<string, string>>::.ctor(object, native int)
两处指令的执行逻辑初步判断:
- 第二处
Func委托的构造调用位于parameters非空的条件分支内,传入null时不会执行,符合预期 - 第一处位于方法入口的显示类(
<>c__DisplayClass0_0)实例化调用不在条件分支内,是意外内存分配的来源
本次使用的基准测试代码如下:
[Benchmark] public void Log_WithInfra_ExtensionMethodDirect_NoParameters() { LoggerExtensions.Usage(_logger, LogLevel.Information, LogAreas.MainApplication.AreaName, LogAreas.MainApplication.Operations.Operation0, null); }
基准测试结果:
- 无参调用场景下Mean耗时8.441ns
- Gen 0回收值为0.0023
- 总分配内存为12B
该问题不会对实际业务使用造成明显影响,但闭包显示类的实例化逻辑不符合常规分支执行预期。
根本原因
这是C#编译器闭包生成逻辑的默认行为:
当方法内的lambda表达式捕获了方法参数/局部变量时,编译器会生成专门的闭包显示类(即代码里看到的<>c__DisplayClass0_0)来存储被捕获的变量。编译器做闭包作用域分析时是按方法整体维度判断的,不会做分支级别的执行路径裁剪——它无法静态保证运行时一定不会进入parameters != null的分支,因此会把闭包显示类的实例化逻辑放在方法入口位置,不管分支是否命中都会执行,这就是传入null时仍有12B分配的来源。
12B的分配大小也和闭包显示类的结构完全匹配:32位CLR下,每个引用类型实例包含4B对象头、4B方法表指针,加上类中存储的parameters引用字段4B,总大小刚好12B。
修复方式
要消除null场景下的不必要分配,只需要提前做null判断分支返回,把闭包逻辑完全隔离到非空分支内即可,修改后代码如下:
public static void Usage(this ILogger logger, LogLevel logLevel, string area, string operation, Dictionary<string, string> parameters) { if (parameters == null) { logger.Usage(logLevel, area, operation, null); return; } Func<Dictionary<string, string>> function = () => parameters; logger.Usage(logLevel, area, operation, function); }
修改后编译器会将闭包显示类的实例化逻辑放到parameters != null的分支内部,传入null时走提前返回路径,不会触发任何闭包相关分配,内存占用即可降到0。
内容的提问来源于stack exchange,提问作者Gilad

