Empty strings

Alright, there’s already been some discussion on this here, and here, and here. Someone even made a chart of timings from someone else’s results. What’s the topic? Whether string.Empty or “” is better.

Generally speaking, the differences in speed and efficiency aren’t fantastic enough to be a big deal. I haven’t had to code something where this difference was crucial to my application’s efficiency.

The general consensus is to use the Length property to determine emptiness. I’ve finally decided to give it a whirl and run some tests. There were 5 tests suggested:

  • s == “”
  • s == string.Empty
  • s.Equals(“”)
  • s.Equals(string.Empty)
  • s.Length == 0

I’ll give you the code I used first

const int cnTries = 10;
const int cnIterations = 1000000000;
DateTime dtStart, dtEnd;
TimeSpan ts;
double fEmptyQuotes = 0, fShortQuotes = 0, fLongQuotes = 0;
double fEmptyEmpty = 0, fShortEmpty = 0, fLongEmpty = 0;
double fEmptyDotQuotes = 0, fShortDotQuotes = 0, fLongDotQuotes = 0;
double fEmptyDotEmpty = 0, fShortDotEmpty = 0, fLongDotEmpty = 0;
double fEmptyDotLength = 0, fShortDotLength = 0, fLongDotLength = 0;
int i, j;
string sEmpty = string.Empty;
string sShort = "This is a short string to test empty string comparison";
string sLong = "This is a long string to test the efficiency of comparing with empty strings, which means it has to be like, really long. And I'm starting to run out of useless things to say...";

for (j = 0; j < cnTries; ++j)
{
    //double fEmptyQuotes = 0, fShortQuotes = 0, fLongQuotes = 0;
    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sEmpty == "") ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fEmptyQuotes += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sShort == "") ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fShortQuotes += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sLong == "") ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fLongQuotes += ts.TotalMilliseconds;

    //double fEmptyEmpty = 0, fShortEmpty = 0, fLongEmpty = 0;
    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sEmpty == string.Empty) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fEmptyEmpty += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sShort == string.Empty) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fShortEmpty += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sLong == string.Empty) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fLongEmpty += ts.TotalMilliseconds;

    //double fEmptyDotQuotes = 0, fShortDotQuotes = 0, fLongDotQuotes = 0;
    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sEmpty.Equals("")) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fEmptyDotQuotes += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sShort.Equals("")) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fShortDotQuotes += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sLong.Equals("")) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fLongDotQuotes += ts.TotalMilliseconds;

    //double fEmptyDotEmpty = 0, fShortDotEmpty = 0, fLongDotEmpty = 0;
    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sEmpty.Equals(string.Empty)) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fEmptyDotEmpty += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sShort.Equals(string.Empty)) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fShortDotEmpty += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sLong.Equals(string.Empty)) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fLongDotEmpty += ts.TotalMilliseconds;

    //double fEmptyDotLength = 0, fShortDotLength = 0, fLongDotLength = 0;
    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sEmpty.Length == 0) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fEmptyDotLength += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sShort.Length == 0) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fShortDotLength += ts.TotalMilliseconds;

    dtStart = DateTime.Now;
    for (i = 0; i < cnIterations; ++i)
    {
        if (sLong.Length == 0) ;
    }
    dtEnd = DateTime.Now;
    ts = dtEnd - dtStart;
    fLongDotLength += ts.TotalMilliseconds;
}

Console.WriteLine("empty: {0}", fEmptyQuotes / (double)cnTries);
Console.WriteLine("short: {0}", fShortQuotes / (double)cnTries);
Console.WriteLine("long : {0}", fLongQuotes / (double)cnTries);
Console.WriteLine("empty: {0}", fEmptyEmpty / (double)cnTries);
Console.WriteLine("short: {0}", fShortEmpty / (double)cnTries);
Console.WriteLine("long : {0}", fLongEmpty / (double)cnTries);
Console.WriteLine("empty: {0}", fEmptyDotQuotes / (double)cnTries);
Console.WriteLine("short: {0}", fShortDotQuotes / (double)cnTries);
Console.WriteLine("long : {0}", fLongDotQuotes / (double)cnTries);
Console.WriteLine("empty: {0}", fEmptyDotEmpty / (double)cnTries);
Console.WriteLine("short: {0}", fShortDotEmpty / (double)cnTries);
Console.WriteLine("long : {0}", fLongDotEmpty / (double)cnTries);
Console.WriteLine("empty: {0}", fEmptyDotLength / (double)cnTries);
Console.WriteLine("short: {0}", fShortDotLength / (double)cnTries);
Console.WriteLine("long : {0}", fLongDotLength / (double)cnTries);

Basically I used an empty string, a short string and a long string to check against empty strings. I ran these 3 cases against the 5 tests 1 billion times for each test case. Then I ran the entire gamut of tests 10 times to get an average. The short string contained 54 characters. The long string contained 177 characters.

Running 1 billion times per test case will give a sufficiently long enough time period. Running 10 times and getting an average will give a sufficiently stable result (DateTime.Now isn’t exactly an, uh, exact stopwatch criteria.).

Here are the results
Checking with [s == ""] test.

  • Empty string, 10315.6250 milliseconds
  • Short string, 8307.8125 milliseconds
  • Long string, 8564.0625 milliseconds

Checking with [s == string.Empty] test.

  • Empty string, 3573.4375 milliseconds
  • Short string, 8307.8125 milliseconds
  • Long string, 8603.1250 milliseconds

Checking with [s.Equals("")] test.

  • Empty string, 9517.1875 milliseconds
  • Short string, 7537.5000 milliseconds
  • Long string, 7576.5625 milliseconds

Checking with [s.Equals(string.Empty)] test.

  • Empty string, 9540.6250 milliseconds
  • Short string, 7515.6250 milliseconds
  • Long string, 7607.8125 milliseconds

Checking with [s.Length == 0] test.

  • Empty string, 443.7500 milliseconds
  • Short string, 443.7500 milliseconds
  • Long string, 445.3125 milliseconds

The check with the Length property wins hands down.

Of course, if I stopped here, this post would be very boring. The reason cited for slowness of checks (or manipulations) of strings that has any double quote in it, is that an actual object of type string is created. The creation of the object added overhead.

I wouldn’t want to delve into the IL code and dismantle everything just so I could spot the exact portion that explains why the Length property is faster, or whether the compiler creates a constant representing an empty string.

Instead, I’ll analyse the results I got instead. If you look at the results, the two non-empty strings run with close timings. This tells me something; The length of the string doesn’t matter as long as it’s non-empty.

I also realised that, with the exception of the 5th test, all test results of using the empty string to compare with an empty string differ with test results of using non-empty strings to compare with empty strings. For example, in the 1st test, the empty string case ran with 10315.6250 milliseconds while the 2nd test gave 3573.4375 milliseconds. Yet in both tests, the non-empty string cases ran with similar timings (hovering around 8400 milliseconds)!

So my second realisation: Empty strings are indeed treated as different objects as non-empty strings. This might seem obvious, but I thought it bears highlighting.

My third realisation is that the Equals function is faster than the double equal == operation. In the grand scheme of things, it probably doesn’t matter. It’s just another indicator that strings aren’t native types, so just because it supports the == operator doesn’t mean it’s comparable to other native types such as integers.

Note that there aren’t any significant differences between tests 3 and 4, meaning tests using Equals("") and Equals(string.Empty) are practically equivalent. As to why test results from 1 and 2 aren’t relatively similar to test results from 3 and 4, I don’t know. My guess would be the Equals function transformed the "" to string.Empty better than the == operator.

My current coding practice is to use s.Equals("stringtocompare") for checking equality with non-empty strings, and s.Length == 0 for checking if s is an empty string. From my test results, my choice seems to be the most efficient.

As for the use of the string.Empty constant, I actually have another reason, and that is maintainability. Let’s look at the following 2 cases,

string s = "";

and

string s = string.Empty;

Even though it’s more verbose, the latter case is clearer that an empty string is assigned. I’ve debugged code where the error was because the original coder typed " " instead of "". Why subject your eyes to undue labour to checking if it’s a string with a space or an empty string?

I’m getting on in years, and my eyes aren’t what they used to be. So, have pity on me and your fellow programmers. Just use string.Empty ok?

Have a happy New Year!

  1. Sarma Josyula

    Thank you very much for the grunt work and the analysis. I am a big fan of efficient code because over time even a few milliseconds difference adds up. Plus more efficiency in code implies better scalability for the application. Even if H/W is cheap it does not mean we should not do our little bit.

    Thanks again.
    Sarma

  2. Vincent Tan

    Efficient code comes from a combination of good program algorithms and good hardware. I might have little control over the hardware bought, but I make sure my code is up to par.

    Thanks for visiting!

Comments are closed.