优化代码以便更快地执行

时间:2009-08-17 21:40:11

标签: c# string optimization

如何优化以下代码以便更快地执行?

static void Main(string[] args) 
{
    String a = "Hello ";
    String b = " World! ";
    for (int i=0; i<20000; i++) 
    {
        a = a + b;
    }
    Console.WriteLine(a);
} 

9 个答案:

答案 0 :(得分:33)

来自StringBuilder文档:

  

效果注意事项

     

Concat和AppendFormat方法都将新数据连接到现有的String或StringBuilder对象。 String对象并置操作始终从现有字符串和新数据创建新对象。 StringBuilder对象维护一个缓冲区以容纳新数据的串联。如果房间可用,新数据将附加到缓冲区的末尾;否则,分配一个新的,更大的缓冲区,将原始缓冲区中的数据复制到新缓冲区,然后将新数据附加到新缓冲区。

     

String或StringBuilder对象的串联操作的性能取决于内存分配发生的频率。字符串连接操作始终分配内存,而StringBuilder连接操作仅在StringBuilder对象缓冲区太小而无法容纳新数据时分配内存。因此,如果连接固定数量的String对象,则String类更适合并置操作。在这种情况下,编译器甚至可以将单个连接操作组合成单个操作。 如果连接任意数量的字符串,则StringBuilder对象最好用于连接操作;例如,如果循环连接随机数量的用户输入字符串。

static void Main(string[] args) {
    String a = "Hello ";
    String b = " World! ";
    StringBuilder result = new StringBuilder(a.Length + b.Length * 20000);
    result.Append(a);
    for (int i=0; i<20000; i++) {
        result.Append(b);
    }
    Console.WriteLine(result.ToString());
} 

答案 1 :(得分:3)

由于它的输出是预先确定的,如果你刚刚对循环构建的文字值进行硬编码,它会运行得更快。

答案 2 :(得分:3)

在循环中执行输出(快5倍,结果相同):

static void Main(string[] args) 
{
    Console.Write("Hello ");
    for (int i=0; i<20000; i++)
       Console.Write(" World! ");
    Console.Write(Environment.NewLine);
}

或者在正手上分配内存并填充它(快4倍,结果相同):

static void Main(string[] args) 
{
   String a = "Hello "; 
   String b = " World! ";

   int it = 20000;
   char[] result = new char[a.Length + it*b.Length];

   a.ToCharArray().CopyTo(result, 0);

   for (int i = 0; i < it; i++) 
      b.ToCharArray().CopyTo(result, a.Length + i * b.Length);

   Console.WriteLine(result);    
}

答案 3 :(得分:1)

可能是IO占主导地位(将输出写入控制台或文件将是最慢的部分),因此可能无法从高度优化中受益。简单地消除明显的悲观就足够了。

作为一般规则,请勿创建临时对象。循环的每次迭代都会创建一个临时字符串,处理a中的整个前一个字符串以及b中字符串的值,因此最多需要b长度的20000倍每次循环操作。即便如此,复制只需要30亿个字节,因此应该在不到一秒的时间内在现代机器上完成(假设运行时对目标硬件使用正确的操作)。将160,008个字符转储到控制台可能需要更长的时间。

一种技术是使用构建器或缓冲区来创建更少的临时对象,而是使用StringBuilder在内存中创建一个长字符串,然后将其复制到字符串,然后输出该字符串。

但是,通过在循环中使用Console.Write来直接编写输出而不是创建任何临时字符串或缓冲区,您可以进一步实现相同的功能。这将删除两个复制操作(将字符串b复制到缓冲区,然后将缓冲区复制到字符串对象,然后将字符串的数据复制到输出缓冲区;最后的复制操作是Console.Write内部的操作,因此无法避免在C#)中,但需要更多的操作系统调用,因此可能会或可能不会更快。

另一个常见的优化是展开循环。因此,没有一个循环有一行写一个“世界!”并循环20,000次,你可以有(比方说)五行写一个“世界!”每个循环它们4,000次。如果增加和测试循环变量的成本高于你在循环中所做的成本,那通常只值得自己做,但它可以导致其他优化。

部分展开循环后,您可以将循环中的代码组合起来,并通过一次调用Console.Write来编写五个或十个“世界!”,这样可以节省一些时间,因为您只需要一个第五是系统调用次数。


写入控制台,在cmd窗口中,它似乎受到控制台窗口速度的限制:

(100次运行的时间以秒为单位)

     724.5312500 - concat
      53.2187500 - direct
      30.3906250 - direct writing b x10
      30.3750000 - direct writing b x100
      30.3750000 - builder
      30.3750000 - builder writing b x100

写入文件,不同技术的时间不同:

     205.0000000 - concat
       9.7031250 - direct
       1.0781250 - direct writing b x10
       0.5000000 - builder
       0.4843750 - direct writing b x100
       0.4531250 - builder writing b x100

由此可以得出两个结论:

如果您在cmd.exe窗口中写入控制台,则大多数改进都无关紧要。您必须将系统整体分析,并且(除非您尝试减少CPU的能耗),除了系统其余部分的能力之外,优先考虑一个组件是没有意义的。

虽然显然做了更多工作 - 更多地复制数据并调用相同数量的函数,但StringBuilder方法更快。这意味着,与非托管语言中的等效语句相比,每次调用Console.Write都会产生相当高的开销。

写入文件,在Win XP上使用gcc C99:

    0.375 - direct ( fputs ( b, stdout ) 20000 times )
    0.171 - direct unrolled ( fputs ( b x 100, stdout ) 200 times )
    0.171 - copy to b to a buffer 20000 times then puts once

C中系统调用的较低成本允许它实现IO绑定,而不是受.net运行时边界的限制。因此,在优化.net时,托管/非托管边界变得很重要。

答案 4 :(得分:1)

static void Main(string[] args) 
{
    const String a = "Hello " +
        /* insert string literal here that contains " World! " 20000 times. */ ;
    Console.WriteLine(a);
}

我无法相信他们在学校里教这样的废话。没有一个真实世界的例子说明为什么你会这样做,更不用说优化它了。所有这些教导的是如何微观优化一个没有用的程序,这对学生作为程序员/开发人员的健康起到了反作用。

答案 5 :(得分:1)

MemoryStream比使用StringBuilder稍快:

    static void Main(string[] args)
    {
        String a = "Hello ";
        String b = " World! ";

        System.IO.MemoryStream ms = new System.IO.MemoryStream(20000 * b.Length + a.Length);
        System.IO.StreamWriter sw = new System.IO.StreamWriter(ms);

        sw.Write(a);
        for (int i = 0; i < 20000; i++)
        {
            sw.Write(b);
        }

        ms.Seek(0,System.IO.SeekOrigin.Begin);
        System.IO.StreamReader sr = new System.IO.StreamReader(ms);
        Console.WriteLine(sr.ReadToEnd());
    }

答案 6 :(得分:0)

我想知道,这会更快吗?

static void Main(string[] args) {
    String a = "Hello ";
    String b = " World! ";
    int worldCount = 20000;
    StringBuilder worldList = new StringBuilder(b.Length * worldCount);
    worldList.append(b);
    StringBuilder result = new StringBuilder(a.Length + b.Length * worldCount);
    result.Append(a);

    while (worldCount > 0) {

       if ((worldCount & 0x1) > 0) {  // Fewer appends, more ToStrings.
          result.Append(worldList);   // would the ToString here kill performance?
       }
       worldCount >>= 1;
       if (worldCount > 0) {
          worldList.Append(worldList);
       }
    }

    Console.WriteLine(result.ToString());
}

答案 7 :(得分:0)

取决于String对象的内容我猜。如果内部所有的都是以空字符结尾的字符串,那么您可以通过在某处存储字符串的长度来进行优化。另外,如果您只是输出到stdout,那么将输出调用移动到循环中会更有意义(更少的内存开销),并且它也应该更快。

答案 8 :(得分:0)

以下是一些时间结果。每次测试从20000次迭代开始。除非另有说明,否则每个测试都包括时序输出。组的每个数字表示迭代次数比前一次大10倍。如果少于4个数字,则测试时间太长,所以我将其杀死。 “Parallize it”意味着我在4个线程上均匀地分割了连接数,并在所有结束时附加了结果(很可能在这里节省了一点时间并将它们放入队列并在完成时附加它们,但没有想到到目前为止)。所有时间都以毫秒为单位。

656 6658 66999 370717 输出hello loop输出世界。没有连接。

658 6641 65807 554546 用stringbuilder构建然后输出

664 6571 65676 314140 用stringbuilder构建,初始大小没有输出

2761 367042 OP,仅限字符串(连接时杀死测试;没有打印到屏幕上)

167 43227 parallize it OP无输出

27 40 323 1758 parallize it stringbuilder no output