.netmultithreadingperformanceoptimizationthreadstatic

Using ThreadStatic to replace expensive locals -- good idea?


Update: as I should have expected, the community's sound advice in response to this question was to "measure it and see." chibacity posted an answer with some really nice tests that did this for me; meanwhile, I wrote a test of my own; and the performance difference I saw was actually so huge that I felt compelled to write a blog post about it.

However, I should also acknowledge Hans's explanation that the ThreadStatic attribute is indeed not free and in fact relies on a CLR helper method to work its magic. This makes it far from obvious whether it would be an appropriate optimization to apply in any arbitrary case.

The good news for me is that, in my case, it seems to have made a big improvement.


I have a method which (among many other things) instantiates some medium-size arrays (~50 elements) for a few local variables.

After some profiling I've identified this method as something of a performance bottleneck. It isn't that the method takes an extremely long time to call; rather, it is simply called many times, very quickly (hundreds of thousands to millions of times in a session, which will be several hours). So even relatively small improvements to its performance should be worthwhile.

It occurred to me that maybe instead of allocating a new array on each call, I could use fields marked [ThreadStatic]; whenever the method is called, it will check if the field is initialized on the current thread, and if not, initialize it. From that point on all calls on the same thread will have an array all ready to go at that point.

(The method initializes every element in the array itself, so having "stale" elements in the array should not be an issue.)

My question is simply this: does this seem like a good idea? Are there pitfalls to using the ThreadStatic attribute in this way (i.e., as a performance optimization to mitigate the cost of instantiating new objects for local variables) that I should know about? Is the performance of a ThreadStatic field itself perhaps not great; e.g., is there a lot of extra "stuff" happening in the background, with its own set of costs, to make this feature possible?

It's also quite plausible to me that I'm wrong to even try to optimize something as cheap (?) as a 50-element array—and if that's so, definitely let me know—but the general question still holds.


Solution

  • I have carried out a simple benchmark and ThreadStatic performs better for the simple parameters described in the question.

    As with many algorithms which have a high number of iterations, I suspect it is a straightforward case of GC overhead killing it for the version which allocates new arrays:

    Update

    With tests that include an added iteration of the array to model minimal array reference use, plus ThreadStatic array reference usage in addition to previous test where reference was copied local:

    Iterations : 10,000,000
    
    Local ArrayRef          (- array iteration) : 330.17ms
    Local ArrayRef          (- array iteration) : 327.03ms
    Local ArrayRef          (- array iteration) : 1382.86ms
    Local ArrayRef          (- array iteration) : 1425.45ms
    Local ArrayRef          (- array iteration) : 1434.22ms
    TS    CopyArrayRefLocal (- array iteration) : 107.64ms
    TS    CopyArrayRefLocal (- array iteration) : 92.17ms
    TS    CopyArrayRefLocal (- array iteration) : 92.42ms
    TS    CopyArrayRefLocal (- array iteration) : 92.07ms
    TS    CopyArrayRefLocal (- array iteration) : 92.10ms
    Local ArrayRef          (+ array iteration) : 1740.51ms
    Local ArrayRef          (+ array iteration) : 1647.26ms
    Local ArrayRef          (+ array iteration) : 1639.80ms
    Local ArrayRef          (+ array iteration) : 1639.10ms
    Local ArrayRef          (+ array iteration) : 1646.56ms
    TS    CopyArrayRefLocal (+ array iteration) : 368.03ms
    TS    CopyArrayRefLocal (+ array iteration) : 367.19ms
    TS    CopyArrayRefLocal (+ array iteration) : 367.22ms
    TS    CopyArrayRefLocal (+ array iteration) : 368.20ms
    TS    CopyArrayRefLocal (+ array iteration) : 367.37ms
    TS    TSArrayRef        (+ array iteration) : 360.45ms
    TS    TSArrayRef        (+ array iteration) : 359.97ms
    TS    TSArrayRef        (+ array iteration) : 360.48ms
    TS    TSArrayRef        (+ array iteration) : 360.03ms
    TS    TSArrayRef        (+ array iteration) : 359.99ms
    

    Code:

    [ThreadStatic]
    private static int[] _array;
    
    [Test]
    public object measure_thread_static_performance()
    {
        const int TestIterations = 5;
        const int Iterations = (10 * 1000 * 1000);
        const int ArraySize = 50;
    
        Action<string, Action> time = (name, test) =>
        {
            for (int i = 0; i < TestIterations; i++)
            {
                TimeSpan elapsed = TimeTest(test, Iterations);
                Console.WriteLine("{0} : {1:F2}ms", name, elapsed.TotalMilliseconds);
            }
        };
    
        int[] array = null;
        int j = 0;
    
        Action test1 = () =>
        {
            array = new int[ArraySize];
        };
    
        Action test2 = () =>
        {
            array = _array ?? (_array = new int[ArraySize]);
        };
    
        Action test3 = () =>
        {
            array = new int[ArraySize];
    
            for (int i = 0; i < ArraySize; i++)
            {
                j = array[i];
            }
        };
    
        Action test4 = () =>
        {
            array = _array ?? (_array = new int[ArraySize]);
    
            for (int i = 0; i < ArraySize; i++)
            {
                j = array[i];
            }
        };
    
        Action test5 = () =>
        {
            array = _array ?? (_array = new int[ArraySize]);
    
            for (int i = 0; i < ArraySize; i++)
            {
                j = _array[i];
            }
        };
    
        Console.WriteLine("Iterations : {0:0,0}\r\n", Iterations);
        time("Local ArrayRef          (- array iteration)", test1);
        time("TS    CopyArrayRefLocal (- array iteration)", test2);
        time("Local ArrayRef          (+ array iteration)", test3);
        time("TS    CopyArrayRefLocal (+ array iteration)", test4);
        time("TS    TSArrayRef        (+ array iteration)", test5);
    
        Console.WriteLine(j);
    
        return array;
    }
    
    [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", MessageId = "System.GC.Collect")]
    private static TimeSpan TimeTest(Action action, int iterations)
    {
        Action gc = () =>
        {
            GC.Collect();
            GC.WaitForFullGCComplete();
        };
    
        Action empty = () => { };
    
        Stopwatch stopwatch1 = Stopwatch.StartNew();
    
        for (int j = 0; j < iterations; j++)
        {
            empty();
        }
    
        TimeSpan loopElapsed = stopwatch1.Elapsed;
    
        gc();
        action(); //JIT
        action(); //Optimize
    
        Stopwatch stopwatch2 = Stopwatch.StartNew();
    
        for (int j = 0; j < iterations; j++) action();
    
        gc();
    
        TimeSpan testElapsed = stopwatch2.Elapsed;
    
        return (testElapsed - loopElapsed);
    }