15

更新:正如我所料,社区对这个问题的合理建议是“衡量并观察”。chibacity 发布了一个答案,其中包含一些非常好的测试,这些测试为我做了这个;同时,我自己写了一个测试;我看到的性能差异实际上是如此巨大,以至于我不得不写一篇关于它的博客文章。

但是,我也应该承认Hans 的解释,即该ThreadStatic属性确实不是免费的,实际上依赖于 CLR 辅助方法来发挥其魔力。这使得它是否是适用于任何情况的适当优化远非显而易见。

对我来说,好消息是,就而言,它似乎取得了很大的进步。


我有一个方法(在许多其他事情中)为一些局部变量实例化一些中等大小的数组(~50 个元素)。

经过一些分析后,我发现这种方法是一种性能瓶颈。并不是该方法需要很长时间才能调用;相反,它只是被调用了很多次,非常快(在一个会话中数十万到数百万次,这将是几个小时)。因此,即使对其性能进行相对较小的改进也应该是值得的。

我突然想到,也许不是在每次调用时分配一个新数组,我可以使用标记的字段[ThreadStatic];每当调用该方法时,它将检查该字段是否在当前线程上初始化,如果没有,则对其进行初始化。从那时起,同一线程上的所有调用都将有一个数组准备就绪。

(该方法初始化数组本身中的每个元素,因此数组中的“陈旧”元素应该不是问题。)

我的问题很简单:这看起来是个好主意吗?ThreadStatic以这种方式使用属性是否存在我应该知道的陷阱(即,作为一种性能优化来减轻为局部变量实例化新对象的成本)?是不是一个ThreadStatic领域本身的表现可能不是很好;例如,是否有很多额外的“东西”在后台发生,有自己的一套成本,使这个功能成为可能?

对我来说,我什至尝试优化像 50 元素数组这样便宜(?)的东西也是错误的——如果是这样,一定要让我知道——但一般问题仍然存在。

4

3 回答 3

9

[ThreadStatic]没有免费的午餐。对变量的每次访问都需要通过 CLR (JIT_GetThreadFieldAddr_Primitive/Objref) 中的辅助函数,而不是通过抖动内联编译。它也不是局部变量的真正替代品,递归是字节。您确实必须自己对此进行分析,在循环中使用那么多 CLR 代码来猜测性能是不可行的。

于 2011-02-01T17:22:09.133 回答
5

我已经进行了一个简单的基准测试,并且ThreadStatic对于问题中描述的简单参数表现更好。

与许多具有大量迭代的算法一样,我怀疑这是一个直接的 GC 开销案例,因为它会为分配新数组的版本杀死它:

更新

使用包括添加数组迭代以模拟最小数组引用使用ThreadStatic的测试,加上数组引用使用以及先前在本地复制引用的测试:

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

代码:

[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);
}
于 2011-02-01T17:33:14.190 回答
2

从这样的结果来看 ThreadStatic 看起来相当快。我不确定是否有人对它是否比重新分配 50 个元素的数组更快有具体的答案。这就是你必须对自己进行基准测试的事情。:)

我有点担心这是不是一个“好主意”。只要所有实现细节都保留在类中,这不一定是一个坏主意(你真的不希望调用者担心它),但除非基准测试显示这种方法可以提高性能,否则我会坚持简单每次都分配数组,因为它使代码更简单,更易于阅读。作为这两种解决方案中更复杂的一种,在选择这个解决方案之前,我需要从复杂性中看到一些好处。

于 2011-02-01T17:00:34.457 回答