由浅入深CIL系列:4.抛砖引玉:使用CIL来分析string类型在.NET运算中的性能和避免装箱
一、在.NET中string是一种特殊的引用类型,它一旦被赋值在堆上的地址即不可改变,之后对其进行的字符串相加等操作之后的结果都指向另外一个堆地址,而非原来的字符串地址。现在我们看以下一段C#代码以观察string在实际编码过程中的使用。
class Program
{
static void Main(string[] args)
{
//打印One Word
string str1 = "One";
str1 += " Word";
Console.WriteLine(str1);
string str2 = "One"+" Word";
Console.WriteLine(str2);
//打印One Word43
int i = 43;
Console.WriteLine(str1+i);
Console.WriteLine(str1 + i.ToString());
}
}
.method private hidebysig static void Main(string[] args) cil managed
{
//初始化
.entrypoint
// 代码大小 80 (0x50)
.maxstack 2
.locals init ([0] string str1,
[1] string str2,
[2] int32 i)
//两个字符串分为两个步骤相加
IL_0000: nop
IL_0001: ldstr "One"
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldstr " Word"
IL_000d: call string [mscorlib]System.String::Concat(string,
string)
IL_0012: stloc.0
IL_0013: ldloc.0
IL_0014: call void [mscorlib]System.Console::WriteLine(string)
//两个字符串在同一行里面进行相加 被翻译为CIL时就已经连接到一起
IL_0019: nop
IL_001a: ldstr "One Word"
IL_001f: stloc.1
IL_0020: ldloc.1
IL_0021: call void [mscorlib]System.Console::WriteLine(string)
//将Int32字符装箱再和String合并打印
IL_0026: nop
IL_0027: ldc.i4.s 43
IL_0029: stloc.2
IL_002a: ldloc.0
IL_002b: ldloc.2
IL_002c: box [mscorlib]System.Int32
IL_0031: call string [mscorlib]System.String::Concat(object,
object)
IL_0036: call void [mscorlib]System.Console::WriteLine(string)
//直接调用数字的ToString()方法
IL_003b: nop
IL_003c: ldloc.0
IL_003d: ldloca.s i
IL_003f: call instance string [mscorlib]System.Int32::ToString()
IL_0044: call string [mscorlib]System.String::Concat(string,
string)
IL_0049: call void [mscorlib]System.Console::WriteLine(string)
IL_004e: nop
IL_004f: ret
} // end of method Program::Main
三、首先我们看两种字符串的构造方式的不同而引起的效能变化。
第1种
//打印One Word
string str1 = "One";
str1 += " Word";
Console.WriteLine(str1);
//第一种、两个字符串分为两个步骤相加
IL_0000: nop
IL_0001: ldstr "One" //将One字符串存到堆上并且将其引用压栈
IL_0006: stloc.0 //从栈顶部的One字符串引用地址弹出到第一个参数0
IL_0007: ldloc.0 //将索引 0 处的局部变量加载到计算堆栈上
IL_0008: ldstr " Word" //将Word字符串存到堆上并且将其引用返回到计算机栈上
//调用系统函数将上面两个字符串相加其结果存到栈顶
IL_000d: call string [mscorlib]System.String::Concat(string,
string)
IL_0012: stloc.0 //从栈顶部的One字符串引用地址弹出到第一个参数0
IL_0013: ldloc.0 //将索引 0 处的局部变量加载到计算堆栈上
//调用系统参数打印One Word
IL_0014: call void [mscorlib]System.Console::WriteLine(string)
第2种
string str2 = "One"+" Word";
Console.WriteLine(str2);
//两个字符串在同一行里面进行相加 被翻译为CIL时就已经连接到一起
IL_0019: nop
IL_001a: ldstr "One Word" //直接已经构造成One Word
IL_001f: stloc.1 //从栈顶部的One Word字符串引用地址弹出到第一个参数1
IL_0020: ldloc.1 //将索引 1 处的局部变量加载到计算堆栈上
//调用系统参数打印One Word
IL_0021: call void [mscorlib]System.Console::WriteLine(string)
结论:通过上面两种方式构造方式的CIL我们可以很清晰的看出第二种方式的效率要高于第一种的字符串构造方式。所以我们在实际的编码过程中可以考虑尽量使用第二种编码方式。
四、大家都知道装箱操作会在堆上寻找一个控件来存储值类型的值。会耗费大量的时间。所以下面我们来看两个实例代码
第1种
int i = 43;
Console.WriteLine(str1+i);
//将Int32字符装箱再和String合并打印
IL_0026: nop
IL_0027: ldc.i4.s 43
IL_0029: stloc.2
IL_002a: ldloc.0
IL_002b: ldloc.2
IL_002c: box [mscorlib]System.Int32 //在这里有一个装箱的操作
IL_0031: call string [mscorlib]System.String::Concat(object,
object)
IL_0036: call void [mscorlib]System.Console::WriteLine(string)
第2种
int i = 43;
Console.WriteLine(str1 + i.ToString());
//直接调用数字的ToString()方法
IL_003b: nop
IL_003c: ldloc.0
IL_003d: ldloca.s i
//这里没有装箱的操作,仅仅调用了重载的ToString()方法
IL_003f: call instance string [mscorlib]System.Int32::ToString()
IL_0044: call string [mscorlib]System.String::Concat(string,
string)
IL_0049: call void [mscorlib]System.Console::WriteLine(string)
IL_004e: nop