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 performan开发者_如何学Pythonce 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.
[ThreadStatic] is no free lunch. Every access to the variable needs to go through a helper function in the CLR (JIT_GetThreadFieldAddr_Primitive/Objref) instead of being compiled inline by the jitter. It also isn't a true substitute for a local variable, recursion is going to byte. You really have to profile this yourself, guesstimating perf with that much CLR code in the loop isn't feasible.
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);
}
From results like this, ThreadStatic looks pretty fast. I'm not sure that anybody has a specific answer to if it's faster then reallocating a 50 element array though. That's the kind of thing you'll have to benchmark yourself. :)
I'm somewhat torn on if it's a "good idea" or not. So long as all the implementation details are kept inside the class it's not necessarily a bad idea (you really don't want the caller to have to worry about it), but unless benchmarks showed a performance gain from this method I would stick to simply allocating the array each time because it makes the code simpler and easier to read. As the more complicated of the two solutions, I'd need to see some benefit from the complexity before choosing this one.
精彩评论