Lambda表達(dá)式:要性能還是要清晰的代碼?
在上文的***個(gè)示例中,我們演示了如何使用Lambda表達(dá)式配合.NET 3.5中定義的擴(kuò)展方法來方便地處理集合中的元素(篩選,轉(zhuǎn)化等等)。不過有朋友可能會(huì)提出,那個(gè)“普通寫法”并非是性能***的實(shí)現(xiàn)方法。方便起見,也為了突出“性能”方面的問題,我們把原來的要求簡化一下:將序列中的偶數(shù)平方輸出為一個(gè)列表。按照那種“普通寫法”可能就是:
- static List< int> EvenSquare(IEnumerable< int> source)
- {
- var evenList = new List< int>();
- foreach (var i in source)
- {
- if (i % 2 == 0) evenList.Add(i);
- }
- var squareList = new List< int>();
- foreach (var i in evenList) squareList.Add(i * i);
- return squareList;
- }
從理論上來說,這樣的寫法的確比以下的做法在性能要差一些:
- static List< int> EvenSquareFast(IEnumerable< int> source)
- {
- List< int> result = new List< int>();
- foreach (var i in source)
- {
- if (i % 2 == 0) result.Add(i * i);
- }
- return result;
- }
在第二種寫法直接在一次遍歷中進(jìn)行篩選,并且直接轉(zhuǎn)化。而***種寫法會(huì)則根據(jù)“功能描述”將做法分為兩步,先篩選后轉(zhuǎn)化,并使用一個(gè)臨時(shí)列表進(jìn)行保存。在向臨時(shí)列表中添加元素的時(shí)候,List< int>可能會(huì)在容量不夠的時(shí)候加倍并復(fù)制元素,這便造成了性能損失。雖然我們通過“分析”可以得出結(jié)論,不過實(shí)際結(jié)果還是使用CodeTimer來測(cè)試一番比較妥當(dāng):
- List< int> source = new List< int>();
- for (var i = 0; i < 10000; i++) source.Add(i);
- // 預(yù)熱
- EvenSquare(source);
- EvenSquareFast(source);
- CodeTimer.Initialize();
- CodeTimer.Time("Normal", 10000, () => EvenSquare(source));
- CodeTimer.Time("Fast", 10000, () => EvenSquareFast(source));
我們準(zhǔn)備了一個(gè)長度為10000的列表,并使用EvenSquare和EvenSquareFast各執(zhí)行一萬次,結(jié)果如下:
- Normal
- Time Elapsed: 3,506ms
- CPU Cycles: 6,713,448,335
- Gen 0: 624
- Gen 1: 1
- Gen 2: 0
- Fast
- Time Elapsed: 2,283ms
- CPU Cycles: 4,390,611,247
- Gen 0: 312
- Gen 1: 0
- Gen 2: 0
Lambda表達(dá)式的執(zhí)行:性能比對(duì)與結(jié)論
結(jié)果同我們料想中的一致,EvenSquareFast無論從性能還是GC上都領(lǐng)先于EvenSquare方法。不過,在實(shí)際情況下,我們?cè)撨x擇哪種做法呢?如果是我的話,我會(huì)傾向于選擇EvenSquare,理由是“清晰”二字。
EvenSquare雖然使用了額外的臨時(shí)容器來保存中間結(jié)果(因此造成了性能和GC上的損失),但是它的邏輯和我們需要的功能較為匹配,我們可以很容易地看清代碼所表達(dá)的含義。至于其中造成的性能損失在實(shí)際項(xiàng)目中可以說是微乎其微的。因?yàn)閷?shí)際上我們的大部分性能是消耗在每個(gè)步驟的功能上,例如每次Int32.Parse所消耗的時(shí)間便是一個(gè)簡單乘法的幾十甚至幾百倍。因此,雖然我們的測(cè)試體現(xiàn)了超過50%的性能差距,不過由于這只是“純遍歷”所消耗的時(shí)間,因此如果算上每個(gè)步驟的耗時(shí),性能差距可能就會(huì)變成10%,5%甚至更低。
當(dāng)然,如果是如上述代碼那樣簡單的邏輯,則使用EvenSquareFast這樣的實(shí)現(xiàn)方式也沒有任何問題。事實(shí)上,我們也不必強(qiáng)求將所有步驟完全合并(即僅僅使用1次循環(huán))或完全分開。我們可以在可讀性與性能之間尋求一種平衡,例如將5個(gè)步驟使用兩次循環(huán)來完能是更合適的方式。
說到“分解循環(huán)”,其實(shí)這類似于Martin Fowler在他的重構(gòu)網(wǎng)站所上列出的重構(gòu)方式之一:“Split Loop”。雖然Split Loop和我們的場(chǎng)景略有不同,但是它也是為了代碼的可讀性而避免將多種邏輯放在一個(gè)循環(huán)內(nèi)。將循環(huán)拆開之后,還可以配合“Extract Method”或“Replace Temp with Query”等方式實(shí)現(xiàn)進(jìn)一步的重構(gòu)。自然,它也提到拆分后的性能影響:
You often see loops that are doing two different things at once, because they can do that with one pass through a loop. Indeed most programmers would feel very uncomfortable with this refactoring as it forces you to execute the loop twice - which is double the work.
But like so many optimizations, doing two different things in one loop is less clear than doing them separately. It also causes problems for further refactoring as it introduces temps that get in the way of further refactorings. So while refactoring, don't be afraid to get rid of the loop. When you optimize, if the loop is slow that will show up and it would be right to slam the loops back together at that point. You may be surprised at how often the loop isn't a bottleneck, or how the later refactorings open up another, more powerful, optimization.
這段文字提到,當(dāng)拆分之后,您可能會(huì)發(fā)現(xiàn)更好的優(yōu)化方式。高德納爺爺也認(rèn)為“過早優(yōu)化是萬惡之源”。這些說法都在“鼓勵(lì)”我們將程序?qū)懙母逦皇恰翱雌饋怼备行省?/P>
以上便分析了使用C# Lambda表達(dá)式編碼時(shí)需要優(yōu)先考慮代碼清晰度的理由。
【編輯推薦】