Tuesday, October 04, 2005 3:24 PM
bart
Performance basics - use StringBuilder
Almost got gray hair when reading some code yesterday. The piece of C# code I was reading, was doing string concatenation to build a pretty large XML fragment (where the size was proportional to the number of rows in a database table, so not known at compile-time). If you don't know the number of concatenations at compile time, please use the System.Text.StringBuilder class. Strings in .NET are immutable, so any change you make to a string will lead to the birth of a new string in memory, leading to additional pressure on the garbage collector as well.
See it yourself with the next demo:
#define TEST1
#define TEST2
#define TEST3
using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Reflection;
class Demo
{
[DllImport("kernel32.dll")]
internal static extern int QueryPerformanceCounter(out Int64 lpPerformanceCount);
[DllImport("kernel32.dll")]
internal static extern int QueryPerformanceFrequency(out Int64 lpPerformanceCount);
public static void Main(string[] args)
{
if (args.Length != 1)
goto __error;
try
{
MAX = int.Parse(args[0]);
if (MAX <= 0)
goto __error;
}
catch
{
goto __error;
}
#if TEST1
Decimal t1 = Test(new BuildString(DemoWithStringBuilder));
Console.WriteLine(t1);
#endif
#if TEST2
Decimal t2 = Test(new BuildString(DemoWithStringBuilderAndFormat));
Console.WriteLine(t2);
#endif
#if TEST3
Decimal t3 = Test(new BuildString(DemoWithConcatenation));
Console.WriteLine(t3);
#endif
return;
__error:
Console.WriteLine("Usage: {0}.exe ", Assembly.GetExecutingAssembly().GetName().Name);
}
static decimal GetSecondsElapsed(long start, long stop)
{
long queryFrequency;
QueryPerformanceFrequency(out queryFrequency);
decimal result = Convert.ToDecimal(stop - start) / Convert.ToDecimal(queryFrequency);
return Math.Round(result, 6);
}
private delegate string BuildString();
private static int MAX = 1000000;
private static string DemoWithConcatenation()
{
string s = "";
for (int i = 0; i < MAX; i++)
s = s + i + "\n";
return s;
}
private static string DemoWithStringBuilder()
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < MAX; i++)
sb.Append(i + "\n");
return sb.ToString();
}
private static string DemoWithStringBuilderAndFormat()
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < MAX; i++)
sb.AppendFormat("{0}\n", i);
return sb.ToString();
}
private static decimal Test(BuildString build)
{
long start, stop;
QueryPerformanceCounter(out start);
build();
QueryPerformanceCounter(out stop);
return GetSecondsElapsed(start, stop);
}
}
Start, Run, notepad, OK. Copy/paste the code above and run csc. Now execute the following:
>main 10000
0.007609
0.009746
0.768374
Wow, a factor 100 difference :o. If you have some free time left, try with a parameter of 100000 as well and get something to drink in the meantime. I hope you see the difference. Also check out the GC behavior, as follows:
- In Administrative tools, go to the Performance MMC.
- Remove all counters that are currently in the list.
- Add the # Gen 0 Collections, # Gen 1 Collections and # Gen 2 Collections counters from .NET CLR Memory performance object to the view.
- First modify the code to disable tests 2 and 3:
#define TEST1
//#define TEST2
//#define TEST3
- Compile and run.
- Go to the Performance MMC and take a look at the number of collections.
- Now modify the code to enable only test 3:
//#define TEST1
//#define TEST2
#define TEST3
- Compile and run again.
- Go to the Performance MMC and taak a look at the number of collections. Compare this number to the number you saw earlier.
The difference over here for generation 0 is 2 (with StringBuilder) to 936 (with concatenation). Please keep your (perf-savvy) code reviewers from getting gray hair and use the StringBuilder class :-).
Del.icio.us |
Digg It |
Technorati |
Blinklist |
Furl |
reddit |
DotNetKicks