• 正确使用String类型


    在实际程序中,String类型用得非常广泛,然而,由于.NET对String类型变量的独特管理方式,使用不当,会严重影响程序的性能。我们分几个方面来谈这个问题:


    1 了解String数据的内存分配方式


     编写一个控制台应用程序,输入以下测试代码:

        class Program
        {
            static void Main(string[] args)
            {
                String s = "a";
                s = "abcd";
            }
        }

     使用.NET Framework 2.0 SDK提供的ildasm.exe工具查看生成的MSIL指令:

    01  .method private hidebysig static void  Main(string[] args) cil managed
    02  {
    03    .entrypoint
    04    // 代码大小       14 (0xe)
    05    .maxstack  1
    06    .locals init ([0] string s)
    07    IL_0000:  nop
    08    IL_0001:  ldstr      "a"
    09    IL_0006:  stloc.0
    10    IL_0007:  ldstr      "abcd"
    11    IL_000c:  stloc.0
    12    IL_000d:  ret
    13  } // end of method Program::Main

     简要解释一下上述MSIL指令代码:
     第06句给局部变量s分配一个索引号(索引号从0开始,如函数中有多个局部变量,其索引号按在函数中出现的顺序加一)。
     在编译时编译器会将代码中的两个字串“a”和“abcd”写入到程序集的元数据(metadata)中,此时,这两个字串被称为“字串字面量(string literal)”。
     第08句使用ldstr指令为字串对象“a”分配内存,并将此对象引用压入到线程堆栈中。
     第09句使用stloc指令从线程堆栈顶弹出先前压入的对象引用,将其传给局部变量s(其索引号为0)。
     同样的过程对“abcd”重复进行一次,所以这两句简单的代码

                String s = "a";
                s = "abcd";

     将会导致CLR使用ldstr指令分配两次内存。
     根据上述分析,读者一定明白了String变量的内容是只读的,给其赋不同的值将会导致内存的重新分配。因此,为提高程序性能,编程时应尽量减少内存的分配操作。
     下面对代码中常见的字串用法进行分析,从中读者可以知道如何避免严重影响程序性能的字串操作。

    2 尽量少使用字串加法运算符

     请看以下两段代码:

      (1)          String s1 = "ab";
                         s1+="cd";
      (2)          String s1="ab"+"cd";

     这两段代码运行结果一样,但速度一样快吗?
     请看第(1)段代码生成的MSIL指令:

      .locals init ([0] string s1)
      IL_0000:  nop
      IL_0001:  ldstr      "ab"
      IL_0006:  stloc.0
      IL_0007:  ldloc.0
      IL_0008:  ldstr      "cd"
      IL_000d:  call       string [mscorlib]System.String::Concat(string,
                                                                  string)
      IL_0012:  stloc.0
      IL_0013:  ret

     再看第(2)段代码生成的指令:

     .locals init ([0] string s1)
      IL_0000:  nop
      IL_0001:  ldstr      "abcd"
      IL_0006:  stloc.0
      IL_0007:  ret

     可以很清楚地看到,第(1)段代码将导致String类的Concat()方法被调用(实现字串加法运算)。对于第(2)段代码,由于C#编译器聪明地在编译时直接将两个字串合并为一个字串字面量,所以程序运行时CLR只调用一次ldstr指令就完成了所有工作,其执行速度谁快就不言而喻了!


    3 避免使用加法运算符连接不同类型的数据
     

    请看以下代码:

     String str = "100+100=" + 200;
         Console.Writeline(str);

     生成的MSIL指令为:

      .maxstack  2
      .locals init ([0] string str)
      IL_0000:  nop
      IL_0001:  ldstr      "100+100="
      IL_0006:  ldc.i4     0xc8
      IL_000b:  box        [mscorlib]System.Int32
      IL_0010:  call       string [mscorlib]System.String::Concat(object,
                                                                  object)
      IL_0015:  stloc.0
      IL_0016:  ldloc.0
      IL_0017:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_001c:  nop
      IL_001d:  ret

     可以清晰地看到,这两句C#代码不仅导致了String类的Concat()方法被调用(IL_0010),而且还引发了装箱操作(IL_000b)!
     Concat()方法会导致CLR为新字串分配内存空间,而装箱操作不仅要分配内存,还需要创建一个匿名对象,对象创建之后还必须有一个数据复制的过程,代价不菲!
     改为以下代码:

                String str = "100+100=";
                Console.Write(str);
                Console.WriteLine(200);

     生成的MSIL指令为:

      .maxstack  1
      .locals init ([0] string str)
      IL_0000:  nop
      IL_0001:  ldstr      "100+100="
      IL_0006:  stloc.0
      IL_0007:  ldloc.0
      IL_0008:  call       void [mscorlib]System.Console::Write(string)
      IL_000d:  nop
      IL_000e:  ldc.i4     0xc8
      IL_0013:  call       void [mscorlib]System.Console::WriteLine(int32)
      IL_0018:  nop
      IL_0019:  ret

     可以看到,虽然多了一次方法调用(Console.Write)方法,但却避免了复杂的装箱操作,也避免了调用String.Concat()方法对内存的频繁分配操作,性能更好。


    4.在循环中使用StringBuilder代替String实现字串连接


     在某些场合需要动态地将多个子串连接成一个大字串,比如许多复杂的SQL命令都是通过循环语句生成的。这时,应避免使用String类的加法运算符,举个简单的实例:

                String str ="";
                for (int i = 1; i <= 10; i++)
                {
                    str += i;
                    if(i<10)
                        str += "+";
                }

     上述代码将生成一个字串:1+2+…+10。
     有了前面的知识,读者一定知道这将导致进行10次装箱操作,19次字串内存分配操作(由String.Concat()方法引发),由于生成的MSIL指令太长,此处不再列出,请读者自行用ildasm.exe工具查看上述代码生成的MSIL指令。
     改为以下代码,程序性能会好很多:

               //预先分配1K的内存空间
                StringBuilder sb = new StringBuilder(1024);
                for (int i = 1; i <= 10; i++)
                {
                    sb.Append(i);
                    if(i<10)
                        sb.Append("+");
                }
                String result = sb.ToString();

     通过使用ildasm.exe工具查看生成的MSIL代码,发现虽然上述代码生成的MSIL指令比前面多了7条,但却避免了耗时的装箱操作,而且内存分配的次数也少了很多。当循环的次数很大时,两段代码的运行性能差异很大。
     

  • 相关阅读:
    ServerSuperIO 3.5版本的体系结构,以及未来规划的几点思考
    《连载 | 物联网框架ServerSuperIO教程》- 18.集成OPC Client,及使用步骤。附:3.5 发布与更新说明。
    《连载 | 物联网框架ServerSuperIO教程》- 17.集成Golden实时数据库,高并发保存测点数据。附:3.4 发布与版本更新说明。
    《连载 | 物联网框架ServerSuperIO教程》- 16.集成OPC Server,及使用步骤。附:3.3 发布与版本更新说明。
    [祝贺] 东方国信集团的钢铁大数据和工业节能两个案例入选工信部工业互联网优秀案例
    《连载 | 物联网框架ServerSuperIO教程》- 15.数据持久化接口的使用。附:3.2发布与版本更新说明。
    物联网建设中通讯互联层的终极解决方案
    hadoop 2.7.2 + zookeeper 高可用集群部署
    开源物联网框架ServerSuperIO 3.0正式发布(C#),跨平台:Win&Win10 Iot&Ubuntu&Ubuntu Mate,一套设备驱动跨平台挂载,附:开发套件和教程。
    《连载 | 物联网框架ServerSuperIO教程》- 14.配制工具介绍,以及设备驱动、视图驱动、服务实例的挂载
  • 原文地址:https://www.cnblogs.com/yangbin1005/p/996251.html
Copyright © 2020-2023  润新知