在過去,我習慣將要交給ThreadPool執行的程式邏輯另外寫成void NamedMethod(object arg) { … }裡,再配合ThreadPool.QueueUserWorkItem(new WaitCallback(NamedMethod), arg);。

近來讀到幾篇文章,發現高手們都很順手地用了Lambda演算式,習慣寫成ThreadPool.QueueUserWorkItem(arg => { … }, arg),將邏輯直接包在匿名方法中。不但程式碼變得更簡潔,二來程式邏輯出現位置等同執行時機,程式碼更加直覺化,可讀性更佳。

在更早一篇談Closure的文章曾提到,匿名方法的背後,Compiler會偷偷將其轉為對應的具名方法,並改寫部分程式(參考),理論上與具名方法做法相近,效能應不會差太多,但我還是挺想透過實驗驗證一下。另一方面,我想也藉此一併展示傳統QueueUserWorkItem寫法與Lambda極簡風的比較!

using System;
using System.Diagnostics;
using System.Threading;
 
namespace MultiCore
{
    class TestAnonymousMethod
    {
        static int JOB_COUNT = 0;
        //使用ManualResetEvent同步所有工作的完成時機
        static ManualResetEvent syncEvent = 
            new ManualResetEvent(false);
        static void Main(string[] args)
        {
            //做1000萬次
            int TIMES = 1000 * 10000;
            Stopwatch sw = new Stopwatch();
            //反覆做五次以求驗證結果一致性
            for (int round = 0; round < 5; round++)
            {
                sw.Reset();
                //先填入待辦工作項目
                JOB_COUNT = TIMES;
                sw.Start();
                for (int i = 0; i < TIMES; i++)
                    //排入1000萬件工作,傳統寫法
                    ThreadPool.QueueUserWorkItem(
                        new WaitCallback(NamedMethod), i);
                //等待待辦工作件數為零,同步事件被觸發
                syncEvent.WaitOne();
                sw.Stop();
                Console.WriteLine("Named Method = {0:N0}ms", 
                    sw.ElapsedMilliseconds);
 
                JOB_COUNT = TIMES;
                syncEvent.Reset();
                sw.Reset();
                sw.Start();
                for (int i = 0; i < TIMES; i++)
                    //使用Lambda演算式將計算邏輯放在匿名方法中
                    ThreadPool.QueueUserWorkItem(o =>
                    {
                        //隨便找點事就,就算Log10吧!
                        double d = Math.Log10(Convert.ToDouble(o));
                        //待辦工作項目減1
                        Interlocked.Decrement(ref JOB_COUNT);
                        //若沒有待辦事項,表示全部工作完成,觸發同步
                        if (JOB_COUNT == 0) syncEvent.Set();
                    }, i);
                syncEvent.WaitOne();
                sw.Stop();
                Console.WriteLine("Anonymous Method = {0:N0}ms", 
                    sw.ElapsedMilliseconds);
            }
            Console.Read();
        }
 
        static void NamedMethod(object arg)
        {
            double d = Math.Log10(Convert.ToDouble(arg));
            Interlocked.Decrement(ref JOB_COUNT);
            if (JOB_COUNT == 0) syncEvent.Set();
        }
    }
}

為了謹慎起見,我加入迴圈一口氣跑五次測試,試圖用多組數據來確保結果一致性。結論有點意思,在五次測試數據中,Lambda的寫法速度通通比傳統寫法還來得快。(【2009-01-02更新】若將NamedMethod方式迴圈裡的new WaitCallback移至迴圈外,則二者速度相當)

Named Method = 11,650ms
Anonymous Method = 9,151ms
Named Method = 9,724ms
Anonymous Method = 8,993ms
Named Method = 10,242ms
Anonymous Method = 9,079ms
Named Method = 11,611ms
Anonymous Method = 8,955ms
Named Method = 9,583ms
Anonymous Method = 9,007ms

透過這個實驗,我的小小心得是: 並未發現Lambda Expression有減損效能的事證,未來可大膽安心服用。


Comments

# by laneser

我的小小疑問是, 照理說, 理論上與具名方法做法相近, 或者說, 應該是一模一樣, 所耗費的事情 (產生具名方法的 code) 又在 Compiler time 做掉了, 速度應該也是一樣才對, 可是從數據來看卻有比較快? 該不會是 new WaitCallback() 的速度小於 new anonymous callback instance ? 我怎麼看大概只有這個地方會引發比較 lambda 比較快問題... 有空或許可以研究看看.

# by Jeffrey

to laneser, 我認為new WailCallback是會有影響的,而且可透過改寫 WaitCallback wb = new WaitCallback(NamedMethod); for (int i = 0; i < TIMES; i++) ThreadPool.QueueUserWorkItem(wb, i); 的方式改善,經實測二者的差距的確會變小,甚至互有消長。不過因文中用的是我習慣的傳統寫法,這點就沒有Tune。

Post a comment