1.确保尽量少的装箱
2.避免分配额外的内存空间
第一个方面:
示例代码片
代码片
:
private void Test1()
{
string str1 = "test" + 1;
string str2 = "test" + 2.ToString();
}
IL代码片
:
.method private hidebysig instance void Test1() cil managed
{
// 代码大小 39 (0x27)
.maxstack 2
.locals init (string V_0,
string V_1,
int32 V_2)
IL_0000: nop
//第一行代码对应的LL代码如下:
IL_0001: ldstr "test"
IL_0006: ldc.i4.1
IL_0007: box [System.Runtime]System.Int32
IL_000c: call string [System.Runtime]System.String::Concat(object,
object)
IL_0011: stloc.0
//第二行代码对应的I代码如下:
IL_0012: ldstr "test"
IL_0017: ldc.i4.2
IL_0018: stloc.2
IL_0019: ldloca.s V_2
IL_001b: call instance string [System.Runtime]System.Int32::ToString()
IL_0020: call string [System.Runtime]System.String::Concat(string,
string)
IL_0025: stloc.1
IL_0026: ret
} // end of method Program::Test1
通过比较
IL代码
,发现第一行代码string str1 = "test" + 1;
发生装箱操作,而第二行代码却没有装箱行为,所以在使用其他值类型到字符转换拼接时,避免直接使用“+”
和值类型拼接,应该使用值类型提供的ToString方法拼接。
在编写代码中,应当尽可能避免编写不必要的装箱代码。
注意:装箱之所以会带来性能损耗,因为它需要完成下面三个步骤:
1.首先,会为值类型在托管堆中分配内存。除子值类型所分配的内存外,内存总量还要加上类型对象指针和同步块索引所占用的内存。
2.将值类型的值复制到新分配的内存中。
3.返回已经成为引用类型的对象的地址。
第二个方面:
避免分配额外的内在空间。对
CLR
来说,string 对象(字符串对象)是个很特殊的对象,它一旦被赋值就不可改变。在运行时调用 System.String 类中的任何方法或进行任何运算(如“=”、“+”等)操作,都会在内存中创建一个新的字符串对象,这也意味要为该对象分配新的内存空间。
1、下面的代码就会带来运行时的额外开销。
代码片
:
private void Test2()
{
string str1 = "123";
str1 = "Test2" + str1 + "456";
//以上两行代码会生成3个字符串对象,并执行了一次 string.Contact 方法
}
IL代码片
:
.method private hidebysig instance void Test2() cil managed
{
// 代码大小 25 (0x19)
.maxstack 3
.locals init (string V_0)
IL_0000: nop
IL_0001: ldstr "123"
IL_0006: stloc.0
IL_0007: ldstr "Test2"
IL_000c: ldloc.0
IL_000d: ldstr "456"
IL_0012: call string [System.Runtime]System.String::Concat(string,
string,
string)
IL_0017: stloc.0
IL_0018: ret
} // end of method Program::Test2
2、下面的代码不会在运行时拼接字符串,而是会在编译时直接生成字符串
private void Test3()
{
string str1 = "123" + "456" + "Test3";
//等效于 string str1 = "123456Test3";
}
private void Test4()
{
const string a = "Test4";
string str1 = "123" + a;
//因为a是一个常量,所以 等效于 string str1 = "123" + "Test4";
//最终等效于 string str1 = "123Test4";
}
IL代码片
:
.method private hidebysig instance void Test3() cil managed
{
// 代码大小 8 (0x8)
.maxstack 1
.locals init (string V_0)
IL_0000: nop
IL_0001: ldstr "123456Test3"
IL_0006: stloc.0
IL_0007: ret
} // end of method Program::Test3
.method private hidebysig instance void Test4() cil managed
{
// 代码大小 8 (0x8)
.maxstack 1
.locals init (string V_0)
IL_0000: nop
IL_0001: ldstr "123Test4"
IL_0006: stloc.0
IL_0007: ret
} // end of method Program::Test4
由于使用 System.String类会在某些场合带来明显的性能损耗,所以微软另外提供了一个类型 StringBuilder来弥补 String 的不足。StringBuilder并不会重新创建一个 string对象,它的效率源于预先以非托管的方式分配内存。
如果 StringBuilder 没有先定义长度,则默认分配的长度为16当 StringBuilder 宇符长度小于等于16时, StringBuilder 不会重新分配内存;当StringBuilder 字符长度大于16小于32时, StringBuilder 又会重新分配内存,使之成为16的倍数。
在上面的代码中,如果预先判断字符串的长度将大于16,则可以为其设定一个更加合适的长度(如32),String Builder重新分配内存时是按照上次的容量加倍进行分配的。当然,我们需要注意,StringBuilder指定的长度要合适太小了,需要频繁分配内存:太大了,浪费空间。
微软还提供了另外一个方法来简化这种操作,即使用 string.Format方法。 stringFormat方法在内部使用 String Builder进行字符串的格式化,如下面的代码所示: