Dot-Net

使用 ThreadStatic 替換昂貴的本地人——好主意嗎?

  • April 30, 2013

更新:正如我所料,社區對這個問題的合理建議是“衡量並觀察”。chibacity 發布了一個答案,其中包含一些非常好的測試,這些測試為我做了這個;同時,我自己寫了一個測試;我看到的性能差異實際上是如此巨大,以至於我不得不寫一篇關於它的部落格文章。

但是,我也應該承認Hans 的解釋,即該ThreadStatic屬性確實不是免費的,實際上依賴於 CLR 輔助方法來發揮其​​魔力。這使得它是否是適用於任何情況的適當優化遠非顯而易見。

對我來說,好消息是,就而言,它似乎取得了很大的進步。


我有一個方法(在許多其他事情中)為一些局部變數實例化一些中等大小的數組(~50 個元素)。

經過一些分析後,我發現這種方法是一種性能瓶頸。並不是該方法需要很長時間才能呼叫;相反,它只是被呼叫了很多次,非常快(在一個會話中數十萬到數百萬次,這將是幾個小時)。因此,即使對其性能進行相對較小的改進也應該是值得的。

我突然想到,也許不是在每次呼叫時分配一個新數組,我可以使用標記的欄位[ThreadStatic];每當呼叫該方法時,它將檢查該欄位是否在目前執行緒上初始化,如果沒有,則對其進行初始化。從那時起,同一執行緒上的所有呼叫都將有一個數組準備就緒。

(該方法初始化數組本身中的每個元素,因此數組中的“陳舊”元素應該不是問題。)

我的問題很簡單:這看起來是個好主意嗎?ThreadStatic以這種方式使用屬性是否存在我應該知道的陷阱(即,作為一種性能優化來減輕為局部變數實例化新對象的成本)?是不是一個ThreadStatic領域本身的表現可能不是很好;例如,是否有很多額外的“東西”在後台發生,有自己的一套成本,使這個功能成為可能?

對我來說,我什至嘗試優化像 50 元素數組這樣便宜(?)的東西也是錯誤的——如果是這樣,一定要讓我知道——但一般問題仍然存在。

我已經進行了一個簡單的基準測試,並且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);
}

$$ ThreadStatic $$沒有免費的午餐。對變數的每次訪問都需要通過 CLR (JIT_GetThreadFieldAddr_Primitive/Objref) 中的輔助函式,而不是通過抖動內聯編譯。它也不是局部變數的真正替代品,遞歸是字節。您確實必須自己對此進行分析,在循環中使用那麼多 CLR 程式碼來猜測性能是不可行的。

引用自:https://stackoverflow.com/questions/4864974