Micro-Optimization and Meatballs

January 15, 2005

In my previous entry on the real cost of performance, there were some complaints that my code's slow and it sucks. If I had a nickel every time someone told me that, I could have retired years ago. Let's take a look at the specific complaint that the s <> "" comparison is inefficient, using low-level windows API timing in the Stopwatch class:

Const iterations As Integer = 1000000
Dim s As String = "sample string"
Dim sw As New Stopwatch
Dim n As Integer

n = 0
sw.Start()
For i As Integer = 1 To iterations
    If s.Length = 0 Then
        n += 1
    End If
Next
sw.Stop()
Console.WriteLine(sw.ElapsedMs)

n = 0
sw.Start()
For i As Integer = 1 To iterations
    If s = String.Empty Then
        n += 1
    End If
Next
sw.Stop()
Console.WriteLine(sw.ElapsedMs)

n = 0
sw.Start()
For i As Integer = 1 To iterations
    If s = "" Then
        n += 1
    End If
Next
sw.Stop()
Console.WriteLine(sw.ElapsedMs)

n = 0
sw.Start()
For i As Integer = 1 To iterations
    If s.Equals(String.Empty) Then
        n += 1
    End If
Next
sw.Stop()
Console.WriteLine(sw.ElapsedMs)

Here are the results:

Athlon FX-53
2.4 GHz
Pentium-M
1.2 GHz
s.Length = 02.6 ms10 ms
s = String.Empty20 ms46 ms
s =""20 ms43 ms
s.Equals(String.Empty)13 ms26 ms

So, yes, String.Length is five (or more) times faster. And yes, using String.Equals is twice as fast. However, neither of those will work when the string is Nothing, and we're still talking about a difference of 30 milliseconds, on the slowest computer I own, over a MILLION string comparisons! This brings to mind a Bill Murray quote from Meatballs: It just doesn't matter! It just doesn't matter!

Meatballs (1979)

Arguments about which method results in code that is easier to read and easier to maintain will be gladly entertained. Arguments about speed will not. Stop micro-optimizing and start macro-optimizing: per Lippert, code that makes sense is code which can be analyzed and maintained, and that makes it performant.

If you'd like to time this yourself, here's a stopwatch class which uses the high resolution API counters. Good luck-- you're gonna need it. The resolution, I mean.

Posted by Jeff Atwood
11 Comments

Yeah, I can't help thinking that the compiler should be smart enough to optimize

s = ""

Into what you posted, Christian.

That's the other problem with micro-optimizations. You gotta ask youself, why am I doing the compiler's work? Something's wrong here.

Jeff Atwood on January 15, 2005 3:51 AM

Something to be aware of, In C# the following statement will evaluate to false:

string s = null;
if (s == "")
{
System.Console.WriteLine("String is null");
}
else
{
System.Console.WriteLine("String is NOT null");
}

whereas in VB the following code evaluates to true:

Dim s As String = Nothing
If s = "" Then
System.Console.WriteLine("String is null!")
Else
System.Console.WriteLine("String is NOT null!")
End If

C# has determined that "empty string" is not the same as null. This, to me, seems like the correct behavior.

In C++ an empty string is a pointer to an allocated block of memory of size 1 byte (or 2 if Unicode). When comparing an empty string with null in C++ the pointer addresses are compared, not sure if C# is doing the same without cracking open the IL. So the following statement will always evaluate the condition to false.

char *p = "\0";
if (p == null)
{
}

I agree that micro-optimization can be a real headache when performance experts (or so called) break code. However, in this instance I would say that comparing a string to empty string when the string is null is bad programming, for the reason stated above. Also, it's not clear in the code if the intention is to check for null or to ignore it and really look for empty string. I much prefer the following:

C#

if (s == null || s.Length == 0)
...

VB

If s is Nothing OrElse s.Length = 0 then
'OrElse to sidestep shortcut.
...

Rob Garrett on January 15, 2005 8:29 AM

The best of both worlds...

Public Class NullString
Private Sub New()
End Sub
Public Shared Function Equals (ByVal tested As String) As Boolean
If tested Is Nothing OrElse 0 = tested.Length Then
Return True
End If
Return False
End Function
End Class


....

If NullString.Equals(myString) Then
'Do something
End If

Christian Romney on January 15, 2005 11:14 AM

Indeed, anyone who runs around adding incorrect micro-optimizations needs to be tasked with trimming my lawn with nail clippers until they're broken of the habit. :-)

FYI, the next version of the framework will have a static String.IsNullOrEmpty(s) method. (I haven't run it through a profiler to see what its comparative cost is yet.)

Eric Lippert on January 15, 2005 11:40 AM

However, in this instance I would say that comparing a string to empty string when the string is null is bad programming, for the reason stated above.

Shrug. That's how it always worked in VB.

I see where you're coming from, but the null/empty distinction isn't a very meaningful one. We do this all the time for database fields-- wrapper functions that substitute Null with default values of 0 (integer), "" (string), and False (bool).

Probably the best compromise is the String.IsNullOrEmpty() whidbey function Mr. Lippert pointed out; that makes it pretty clear what is happening.

Jeff Atwood on January 16, 2005 2:07 AM

I'm not familiar enough with Whidbey yet, but another question comes to mind immediately: I seem to remember reading about nullable value types like int if you use a syntax like int? myValue = null; (atrocious). What are we going to do then? Integer.IsNullOrZero()? Double.IsNullOrZero() ad nauseum? I suppose we could use generics, but this is one ugly pattern.

Christian Romney on January 17, 2005 10:29 AM

You might consider the Marshal.PrelinkAll method for your Stopwatch class to be sure to move the cost of resolving extern methods outside of the timing.

Doug McClean on January 19, 2005 3:57 AM

Great tip, I didn't know that was possible!

Jeff Atwood on January 20, 2005 9:25 AM

In vb there isn't a distinction between null and empty, but oh let me assure you in c# there is and it's a huge one... for example, you can't say s.Length == 0 if s is null or you get a NullReferenceException. Null means 'exists nowhere in memory' not the same as 'empty, but at this place in memory waiting to hold a value' The methods or properties of a reference type (as strings are in c#) go with the instances unless they are static methods, (length is most definitely instance specific, because it has no meaning in the static context (ie we don't care what the length of String is we care what the length of this instance of string is)) So if you go to 'nowehere in memory' and from there try to execute a method on it, even something like .get_Length(), which is the same internally as saying Length, then you're going to blow it up.

remember, null = nowhere, non-existant, while empty = somewhere not holding data, but waiting to with enough space for it.

Dave on March 31, 2006 10:50 AM

***Off topic***
Hey Jeff, your comment boxes (background: black font:darkgray) are a 'Reading Horror'! I have to select your text just so I can read it, like in the olden days when people put invisible text in their webpages as easter eggs!

***On topic***
When your writting something in a language like VB,Python or Ruby; these little performance tricks are near meaningless. They are also meaningless when making a common GUI program or a rarely used automation script or even a little webapp. Totally agree with you here.
However when your working on a high performance system like a database server or a game, then performance makes a big difference because all of the sudden your world becomes measured by the millisecond, no longer by the second that most End-Users need.
30ms for 1.000.000operations can be a bottleneck.
In the end its all about Appropriate programming, like a good carpenter use the right tools for the right job. And sometimes optimization is the right tool (however if you need that kind performance of that scale don't use VB or a scripting language :)

Robert on April 1, 2008 8:04 AM

You might consider the Marshal.PrelinkAll method for your Stopwatch class to be sure to move the cost of resolving extern methods outside of the timing.
http://pralians.ru

Sobr on May 14, 2009 1:40 PM

The comments to this entry are closed.