C# 到 CIL 拳击与 ToString 成本
C# to CIL Boxing vs. ToString Cost
我正在通过 C# 阅读 CLR(第 4 版)这本书,不是作为 C# 的新手,而是作为一个了解该语言的人,试图提高我对 CLR 底层功能的掌握。
无论如何,本书在讨论 boxing/unboxing 值类型时给出了一个示例 (pg127-131),该示例以对 Console.WriteLine 的调用结束,值类型连接到一个字符串作为参数传递。
这本书解释了装箱和 unboxing/copy 操作会导致开销,我已经知道这一点,但它随后指出该示例可以通过 运行 .ToString() 在值类型上进行优化已通过。
我创建了一个示例程序并编译了它,然后使用 ILDASM 检查它生成的 IL。带有 ToString 的版本本质上是相同的,但是将 "box" 指令替换为 "call" 到 ToString(没有震惊)。
我在 100000 次运行的循环中对代码进行了基准测试,没有区别(波动较大)。我意识到在基准测试(缓存等)时其他因素也会起作用,但顺便说一下这本书解释它的方式我曾期望即使在天真的基准测试中避免 "box" 指令时也会看到显着差异..
难道调用一个函数也好不了多少? ToString中是否进行了装箱操作,使好处无效并且书错了?有人可以阐明这一点吗?
作为参考,这里有两个 ILDASM 读数:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 24 (0x18)
.maxstack 2
.locals init (int32 V_0)
IL_0000: ldc.i4.4
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: box [mscorlib]System.Int32
IL_0008: ldstr "."
IL_000d: call string [mscorlib]System.String::Concat(object,
object)
IL_0012: call void [mscorlib]System.Console::WriteLine(string)
IL_0017: ret
} // end of method Program::Main
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 25 (0x19)
.maxstack 2
.locals init (int32 V_0)
IL_0000: ldc.i4.4
IL_0001: stloc.0
IL_0002: ldloca.s V_0
IL_0004: call instance string [mscorlib]System.Int32::ToString()
IL_0009: ldstr "."
IL_000e: call string [mscorlib]System.String::Concat(string,
string)
IL_0013: call void [mscorlib]System.Console::WriteLine(string)
IL_0018: ret
} // end of method Program::Main
您忽略了一个事实,即 String.Concat
将在内部提供的 object
参数上调用 ToString
:
public static String Concat(Object arg0, Object arg1) {
Contract.Ensures(Contract.Result<String>() != null);
Contract.EndContractBlock();
if (arg0 == null)
{
arg0 = String.Empty;
}
if (arg1==null) {
arg1 = String.Empty;
}
return Concat(arg0.ToString(), arg1.ToString());
}
所以 call
指令无论如何都会在那里,但它隐藏在 Concat
方法调用中。
调用 ToString
会选择不同的 Concat
重载,而这个不会在内部调用 ToString
。
IL_000d: call string [mscorlib]System.String::Concat(object, object)
对比
IL_000e: call string [mscorlib]System.String::Concat(string, string)
CLR 可能会内联对 string.Concat(object,object)
的调用,这会导致与您的 "optimized" 版本相同的代码。请注意,C# 编译器会将大量此类优化留给 CLR,因为它有更好的工具来执行它们。
除了几个空检查(将被优化掉)之外,它只是调用 string.Concat(left.ToString(),right.ToString())
,这将被简化为 string.Concat(left,right.ToString())
,因为 CLR 会看到 ToString()
只是 returns this
.
因此两种情况下的执行代码可能相同。
我正在通过 C# 阅读 CLR(第 4 版)这本书,不是作为 C# 的新手,而是作为一个了解该语言的人,试图提高我对 CLR 底层功能的掌握。
无论如何,本书在讨论 boxing/unboxing 值类型时给出了一个示例 (pg127-131),该示例以对 Console.WriteLine 的调用结束,值类型连接到一个字符串作为参数传递。
这本书解释了装箱和 unboxing/copy 操作会导致开销,我已经知道这一点,但它随后指出该示例可以通过 运行 .ToString() 在值类型上进行优化已通过。
我创建了一个示例程序并编译了它,然后使用 ILDASM 检查它生成的 IL。带有 ToString 的版本本质上是相同的,但是将 "box" 指令替换为 "call" 到 ToString(没有震惊)。
我在 100000 次运行的循环中对代码进行了基准测试,没有区别(波动较大)。我意识到在基准测试(缓存等)时其他因素也会起作用,但顺便说一下这本书解释它的方式我曾期望即使在天真的基准测试中避免 "box" 指令时也会看到显着差异..
难道调用一个函数也好不了多少? ToString中是否进行了装箱操作,使好处无效并且书错了?有人可以阐明这一点吗?
作为参考,这里有两个 ILDASM 读数:
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 24 (0x18)
.maxstack 2
.locals init (int32 V_0)
IL_0000: ldc.i4.4
IL_0001: stloc.0
IL_0002: ldloc.0
IL_0003: box [mscorlib]System.Int32
IL_0008: ldstr "."
IL_000d: call string [mscorlib]System.String::Concat(object,
object)
IL_0012: call void [mscorlib]System.Console::WriteLine(string)
IL_0017: ret
} // end of method Program::Main
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
// Code size 25 (0x19)
.maxstack 2
.locals init (int32 V_0)
IL_0000: ldc.i4.4
IL_0001: stloc.0
IL_0002: ldloca.s V_0
IL_0004: call instance string [mscorlib]System.Int32::ToString()
IL_0009: ldstr "."
IL_000e: call string [mscorlib]System.String::Concat(string,
string)
IL_0013: call void [mscorlib]System.Console::WriteLine(string)
IL_0018: ret
} // end of method Program::Main
您忽略了一个事实,即 String.Concat
将在内部提供的 object
参数上调用 ToString
:
public static String Concat(Object arg0, Object arg1) {
Contract.Ensures(Contract.Result<String>() != null);
Contract.EndContractBlock();
if (arg0 == null)
{
arg0 = String.Empty;
}
if (arg1==null) {
arg1 = String.Empty;
}
return Concat(arg0.ToString(), arg1.ToString());
}
所以 call
指令无论如何都会在那里,但它隐藏在 Concat
方法调用中。
调用 ToString
会选择不同的 Concat
重载,而这个不会在内部调用 ToString
。
IL_000d: call string [mscorlib]System.String::Concat(object, object)
对比
IL_000e: call string [mscorlib]System.String::Concat(string, string)
CLR 可能会内联对 string.Concat(object,object)
的调用,这会导致与您的 "optimized" 版本相同的代码。请注意,C# 编译器会将大量此类优化留给 CLR,因为它有更好的工具来执行它们。
除了几个空检查(将被优化掉)之外,它只是调用 string.Concat(left.ToString(),right.ToString())
,这将被简化为 string.Concat(left,right.ToString())
,因为 CLR 会看到 ToString()
只是 returns this
.
因此两种情况下的执行代码可能相同。