使用 ThreadStatic 替換昂貴的本地人——好主意嗎?
更新:正如我所料,社區對這個問題的合理建議是“衡量並觀察”。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 程式碼來猜測性能是不可行的。