A question came up in an online discussion about the relative performance of using a bare primitive type vs wrapping that type into a struct. Wrapping the type is a common technique used to avoid runtime errors, enlisting the compiler to enforce the correct semantics at compile time instead. But, sometimes people worry about the performance cost of doing this.

My experience is that type safety usually results in code that scales better. For example, it might enable better runtime optimization, or it might reduce code duplication, or it might permit better algorithms.

In this case, we’re explicitly going to look at the micro-optimization of wrapping a primitive type into a struct. Is there actually a performance problem to address?

I didn’t think there was.

But, facts always trump opinions (or guesses), so I decided to write a benchmark to find out.

I created a simple project using BenchMarkDotNet and made sure to include benchmarking of allocations as well as execution time. If you’re interested, I published the source as a gist.

For the primitive type case, the method Foo takes a string and returns its hash:

private int Foo(string s)
{
    return s.GetHashCode();
}

I originally used .Length the but the optimizer is just a little too clever and it managed to optimize away the method completely.

For comparison, I created a simple wrapper struct, and an overload of Foo:

private int Foo(WrapString s)
{
    return s.Value.GetHashCode();
}

public readonly struct WrapString
{
    public readonly string Value;

    public WrapString(string value)
    {
        Value = value;
    }
}

Then, because I was curious, I defined a generic struct for comparison:

private int Foo(Wrap<string> s)
{
    return s.Value.GetHashCode();
}

public readonly struct Wrap<T>
{
    public readonly T Value;

    public Wrap(T value)
    {
        Value = value;
    }
}

Here are the results of running the Benchmark:

| Method             | Mean     | Error     | StdDev    | Median   | Allocated |
|--------------------|----------|-----------|-----------|----------|-----------|
| Primitive          | 6.092 ns | 0.2577 ns | 0.5437 ns | 5.875 ns | 0 B       |
| UsingStruct        | 5.916 ns | 0.1123 ns | 0.1050 ns | 5.904 ns | 0 B       |
| UsingGenericStruct | 5.846 ns | 0.0706 ns | 0.0660 ns | 5.829 ns | 0 B       |

I did all of this on my laptop, and while I was “hands off” while the benchmark was running, this isn’t a machine that’s been configured to eliminate as much background activity as possible. The only tuning I did was to configure power management for best performance.

It’s initially interesting to see that the struct versions appear to run just a hair faster, but when you look at the error and standard deviation values, it becomes pretty clear that there’s no meaningful difference in performance.

Crucially, they all run with zero memory allocation, so there’s absolutely no impact on the heap.

Comments

blog comments powered by Disqus