欧美在线日韩-欧美在线区-欧美在线看欧美视频免费网站-欧美在线精品一区二区在线观看-www..com黄-vr专区日韩精品中文字幕

行業(yè)動(dòng)態(tài)
代碼越“整潔”,性能越“拉胯”,甚至導(dǎo)致程序變慢 15 倍!陪老婆上街不敢叫她名字,回頭率太高,網(wǎng)友:你叫你的我們聽(tīng)不見(jiàn)
2024-08-04

【CSDN 編者按】相比一個(gè)人一種風(fēng)格的代碼,幾乎人人都想編寫(xiě)一套“整潔”的代碼,然而,遵循條條框框規(guī)則寫(xiě)出來(lái)的代碼,在性能上能否可以達(dá)到理想狀態(tài)?

來(lái)自 Molly Rocket 公司的首席程序員 Casey Muratori 給出了否定的答案,與此同時(shí),他也以教科書(shū)中的示例進(jìn)行了測(cè)試,最終得出“遵循編寫(xiě)整潔代碼的這些規(guī)則,你的代碼運(yùn)行速度會(huì)放慢 15 倍”的結(jié)論。

不過(guò),對(duì)于這樣的結(jié)論也引發(fā)了巨大的爭(zhēng)議,代碼的性能和整潔度能否共存?接下來(lái),我們將通過(guò)本文一探究竟。

原文鏈接:https://www.computerenhance.com/p/clean-code-horrible-performance

聲明:本文為 CSDN 翻譯,未經(jīng)允許,禁止轉(zhuǎn)載

作者 | Casey Muratori譯者 | 彎月 責(zé)編 | 蘇宓出品 | CSDN(ID:CSDNnews)

編寫(xiě)“整潔”的代碼,這是一條反復(fù)被人提及的編程建議,尤其是初學(xué)者,聽(tīng)得太多耳朵都長(zhǎng)繭了。“整潔”的代碼背后是一長(zhǎng)串規(guī)則,告訴你應(yīng)該怎么書(shū)寫(xiě),代碼才能保持“整潔”。

實(shí)際上,這些規(guī)則中很大的一部分并不會(huì)影響代碼的運(yùn)行時(shí)間。我們無(wú)法客觀評(píng)估這些類型的規(guī)則,而且也沒(méi)必要進(jìn)行這樣的評(píng)估。然而,一些所謂的“整潔”代碼規(guī)則(其中有一部分甚至被反復(fù)強(qiáng)調(diào))是可以客觀衡量的,因?yàn)樗鼈兇_實(shí)會(huì)影響代碼的運(yùn)行時(shí)行為。

整理和歸納“整潔”的代碼規(guī)則,并提取實(shí)際影響代碼結(jié)構(gòu)的規(guī)則,我們將得到:

使用多態(tài)代替“if/else”和“switch”;

代碼不應(yīng)該知道使用對(duì)象的內(nèi)部結(jié)構(gòu);

嚴(yán)格控制函數(shù)的規(guī)模;

函數(shù)應(yīng)該只做一件事;

“DRY”(Don’t Repeat Yourself):不要重復(fù)自己。

這些規(guī)則非常具體地說(shuō)明了為了保持代碼“整潔”,我們應(yīng)該如何書(shū)寫(xiě)特定的代碼片段。然而,我的疑問(wèn)在于,如果創(chuàng)建一段遵循這些規(guī)則的代碼,它的性能如何?

為了構(gòu)建我認(rèn)為嚴(yán)格遵守“整潔之道”的代碼,我使用了“整潔”代碼相關(guān)文章中包含的現(xiàn)有示例。也就是說(shuō),這些代碼不是我編寫(xiě)的,我只是利用他們提供的示例代碼來(lái)評(píng)估“整潔”代碼倡導(dǎo)的規(guī)則。

那些年我們見(jiàn)過(guò)的“整潔”代碼

提起“整潔”代碼的示例,你經(jīng)常會(huì)看到下面這樣的代碼:

/* ======================================================================== LISTING 22 ======================================================================== */class shape_base{public: shape_base() {} virtual f32 Area() = 0;};class square : public shape_base{public: square(f32 SideInit) : Side(SideInit) {} virtual f32 Area() {return Side*Side;}private: f32 Side;};class rectangle : public shape_base{public: rectangle(f32 WidthInit, f32 HeightInit) : Width(WidthInit), Height(HeightInit) {} virtual f32 Area() {return Width*Height;}private: f32 Width, Height;};class triangle : public shape_base{public: triangle(f32 BaseInit, f32 HeightInit) : Base(BaseInit), Height(HeightInit) {} virtual f32 Area() {return 0.5f*Base*Height;}private: f32 Base, Height;};class circle : public shape_base{public: circle(f32 RadiusInit) : Radius(RadiusInit) {} virtual f32 Area() {return Pi32*Radius*Radius;}private: f32 Radius;};

這段代碼是一個(gè)形狀的基類,從中派生出了一些特定的形狀:圓形、三角形、矩形、正方形。此外,還有一個(gè)計(jì)算面積的虛函數(shù)。

就像規(guī)則要求的一樣,我們傾向于多態(tài)性,函數(shù)只做一件事,而且很小。最終,我們得到了一個(gè)“整潔”的類層次結(jié)構(gòu),每個(gè)派生類都知道如何計(jì)算自己的面積,并存儲(chǔ)了計(jì)算面積所需的數(shù)據(jù)。

如果我們想象使用這個(gè)層次結(jié)構(gòu)來(lái)做某事,比如計(jì)算一系列形狀的總面積,那么我們希望看到下面這樣的代碼:

/* ======================================================================== LISTING 23 ======================================================================== */f32 TotalAreaVTBL(u32 ShapeCount, shape_base **Shapes){ f32 Accum = 0.0f; for(u32 ShapeIndex = 0; ShapeIndex { Accum += Shapes[ShapeIndex]->Area(); } return Accum;}

你可能會(huì)發(fā)現(xiàn),此處我沒(méi)有使用任何迭代,因?yàn)椤罢麧嵈a之道”中沒(méi)有建議你必須使用迭代器。因此,我想盡可能避免有損“整潔”代碼的寫(xiě)法,我不希望添加任何有可能混淆編譯器并導(dǎo)致性能下降的抽象迭代器。

此外,你可能還會(huì)注意到,這個(gè)循環(huán)是在一個(gè)指針數(shù)組上進(jìn)行的。這是使用類層次結(jié)構(gòu)的直接結(jié)果:我們不知道每種形狀占用的內(nèi)存有多大。所以除非我們添加另一個(gè)虛函數(shù)調(diào)用來(lái)獲取每個(gè)形狀的數(shù)據(jù)大小,并使用某種步長(zhǎng)可變的跳躍過(guò)程來(lái)遍歷它們,否則我們需要指針來(lái)找出每個(gè)形狀的實(shí)際開(kāi)始位置。

因?yàn)檫@個(gè)計(jì)算數(shù)一個(gè)累加和,所以循環(huán)本身引起的依賴可能會(huì)導(dǎo)致循環(huán)速度減慢。由于計(jì)算累加可以以任意順序進(jìn)行,為了安全起見(jiàn),我還寫(xiě)了一個(gè)手動(dòng)展開(kāi)的版本:

/* ======================================================================== LISTING 24 ======================================================================== */f32 TotalAreaVTBL4(u32 ShapeCount, shape_base **Shapes){ f32 Accum0 = 0.0f; f32 Accum1 = 0.0f; f32 Accum2 = 0.0f; f32 Accum3 = 0.0f; u32 Count = ShapeCount/4; while(Count--) { Accum0 += Shapes[0]->Area(); Accum1 += Shapes[1]->Area(); Accum2 += Shapes[2]->Area(); Accum3 += Shapes[3]->Area(); Shapes += 4; } f32 Result = (Accum0 + Accum1 + Accum2 + Accum3); return Result;}

在一個(gè)簡(jiǎn)單的測(cè)試工具中運(yùn)行以上這兩個(gè)例程,可以粗略地計(jì)算出執(zhí)行該操作每個(gè)形狀所需的循環(huán)總數(shù):

廣告
膽小者勿入!五四三二一...恐怖的躲貓貓游戲現(xiàn)在開(kāi)始!
×

測(cè)試工具以兩種不同的方式統(tǒng)計(jì)代碼的時(shí)間。**種方法是只運(yùn)行一次代碼,以顯示在沒(méi)有預(yù)熱的狀態(tài)下代碼的運(yùn)行時(shí)間(在此狀態(tài)下,數(shù)據(jù)應(yīng)該在 L3 中,但 L2 和 L1 已被刷新,而且分支預(yù)測(cè)器尚未針對(duì)循環(huán)進(jìn)行預(yù)測(cè))。

第二種方法是反復(fù)運(yùn)行代碼,看看當(dāng)緩存和分支預(yù)測(cè)器以最適合循環(huán)的方式運(yùn)行時(shí)情況會(huì)怎樣。請(qǐng)注意,這些都不是嚴(yán)謹(jǐn)?shù)臏y(cè)量,因?yàn)檎缒闼?jiàn),我們已經(jīng)看到了巨大的差異,根本不需要任何嚴(yán)謹(jǐn)?shù)姆治龉ぞ摺?/span>

從結(jié)果中我們可以看出,這兩個(gè)例程之間沒(méi)有太大區(qū)別。這段“整潔”的代碼計(jì)算這個(gè)形狀的面積大約需要循環(huán)35次,如果幸運(yùn)的話,有可能減少到34次。

所以,我們嚴(yán)格遵守“代碼整潔之道”,最后需要循環(huán)35次。

違反“代碼整潔之道”的**條規(guī)則后

那么,如果我們違反**條規(guī)則,會(huì)怎么樣?如果我們不使用多態(tài)性,使用一個(gè) switch 語(yǔ)句呢?

下面,我又編寫(xiě)了一段一模一樣的代碼,只不過(guò)這一次我沒(méi)有使用類層次結(jié)構(gòu),而是使用枚舉,將所有內(nèi)容扁平化為一個(gè)結(jié)構(gòu)的形狀類型:

/* ======================================================================== LISTING 25 ======================================================================== */enum shape_type : u32{ Shape_Square, Shape_Rectangle, Shape_Triangle, Shape_Circle, Shape_Count,};struct shape_union{ shape_type Type; f32 Width; f32 Height;};f32 GetAreaSwitch(shape_union Shape){ f32 Result = 0.0f; switch(Shape.Type) { case Shape_Square: {Result = Shape.Width*Shape.Width;} break; case Shape_Rectangle: {Result = Shape.Width*Shape.Height;} break; case Shape_Triangle: {Result = 0.5f*Shape.Width*Shape.Height;} break; case Shape_Circle: {Result = Pi32*Shape.Width*Shape.Width;} break; case Shape_Count: {} break; } return Result;}

這是代碼整潔之道出現(xiàn)以前,很常見(jiàn)的“老派”寫(xiě)法。

請(qǐng)注意,由于我們沒(méi)有為每個(gè)形狀提供特定的數(shù)據(jù)類型,所以如果某個(gè)類型缺乏其中一個(gè)值(比如“高度”),計(jì)算就不使用了。

現(xiàn)在,這個(gè)結(jié)構(gòu)的用戶獲取面積不再需要調(diào)用虛函數(shù),而是需要使用帶有 switch 語(yǔ)句的函數(shù),這違反了“代碼整潔之道”。即便如此,你會(huì)注意到代碼更加簡(jiǎn)潔了,但功能基本相同。switch 語(yǔ)句的每一個(gè) case 的都對(duì)應(yīng)于類層次結(jié)構(gòu)中的一個(gè)虛函數(shù)。

對(duì)于求和循環(huán)本身,你可以看到這段代碼與上述“整潔”版幾乎相同:

/* ======================================================================== LISTING 26 ======================================================================== */f32 TotalAreaSwitch(u32 ShapeCount, shape_union *Shapes){ f32 Accum = 0.0f; for(u32 ShapeIndex = 0; ShapeIndex < ShapeCount; ++ShapeIndex) { Accum += GetAreaSwitch(Shapes[ShapeIndex]); } return Accum;}f32 TotalAreaSwitch4(u32 ShapeCount, shape_union *Shapes){ f32 Accum0 = 0.0f; f32 Accum1 = 0.0f; f32 Accum2 = 0.0f; f32 Accum3 = 0.0f; ShapeCount /= 4; while(ShapeCount--) { Accum0 += GetAreaSwitch(Shapes[0]); Accum1 += GetAreaSwitch(Shapes[1]); Accum2 += GetAreaSwitch(Shapes[2]); Accum3 += GetAreaSwitch(Shapes[3]); Shapes += 4; } f32 Result = (Accum0 + Accum1 + Accum2 + Accum3); return Result;}

唯一的不同之處在于,我們調(diào)用常規(guī)函數(shù)來(lái)獲取面積。

但是,我們已經(jīng)看到了相較于類層次結(jié)構(gòu),使用扁平結(jié)構(gòu)的直接好處:形狀可以存儲(chǔ)在數(shù)組中,不需要指針。不需要間接訪問(wèn),因?yàn)樗行螤钫加玫膬?nèi)存大小都一樣。

另外,我們還獲得了額外的好處,現(xiàn)在編譯器可以確切地看到我們?cè)谶@個(gè)循環(huán)中做了什么,因?yàn)樗恍璨榭?GetAreaSwitch 函數(shù)。它不必假設(shè)只有等到運(yùn)行時(shí)我們才能看得見(jiàn)某些虛擬面積函數(shù)具體在做什么。

那么,編譯器能利用這些好處為我們做什么呢?下面,我們來(lái)完整地運(yùn)行一遍四個(gè)形狀的面積計(jì)算,得到的結(jié)果如下:

觀察結(jié)果,我們可以看出,改用“老派””的寫(xiě)法后,代碼的性能立即提高了 1.5 倍。我們什么都沒(méi)干,只是刪除了使用 C++ 多態(tài)性的代碼,就收獲了1.5倍的性能提升。

違反代碼整潔之道的**條規(guī)則(也是核心原則之一),計(jì)算每個(gè)面積的循環(huán)數(shù)量就從35次減少到了24次,這意味著,遵循代碼整潔之道會(huì)導(dǎo)致代碼的速度降低1.5倍。拿手機(jī)打個(gè)比方,就相當(dāng)于把 iPhone 14 Pro Max 換成了 iPhone 11 Pro Max。過(guò)去三四年間硬件的發(fā)展瞬間化無(wú),僅僅是因?yàn)橛腥苏f(shuō)要使用多態(tài)性,不要使用 switch 語(yǔ)句。

然而,這只是一個(gè)開(kāi)頭。

違反“代碼整潔之道”的更多條規(guī)則后

如果我們違反更多規(guī)則,結(jié)果會(huì)怎么樣?如果我們打破第二條規(guī)則,“沒(méi)有內(nèi)部知識(shí)”,結(jié)果會(huì)如何?如果我們的函數(shù)可以利用自身實(shí)際操作的知識(shí)來(lái)提高效率呢?

回顧一下計(jì)算面積的 switch 語(yǔ)句,你會(huì)發(fā)現(xiàn)所有面積的計(jì)算方式都很相似:

case Shape_Square: {Result = Shape.Width*Shape.Width;} break; case Shape_Rectangle: {Result = Shape.Width*Shape.Height;} break; case Shape_Triangle: {Result = 0.5f*Shape.Width*Shape.Height;} break; case Shape_Circle: {Result = Pi32*Shape.Width*Shape.Width;} break;

所有形狀的面積計(jì)算都是做乘法,長(zhǎng)乘以寬、寬乘以高,或者乘以 π 的系數(shù)等等。只不過(guò),三角形的面積需要乘以1/2,而圓的面積需要乘以 π。

這是我認(rèn)為此處使用 switch 語(yǔ)句非常合適的原因之一,盡管這與代碼整潔之道背道而馳。透過(guò) switch 語(yǔ)句,我們可以很清楚地看到這種模式。當(dāng)你按照操作而不是類型組織代碼時(shí),觀察和提取通用模式就很簡(jiǎn)單。相比之下,觀察類版本,你可能永遠(yuǎn)也發(fā)現(xiàn)不了這種模式,因?yàn)轭惏姹静粌H有很多樣板代碼,而且你需要將每個(gè)類放在一個(gè)單獨(dú)的文件中,無(wú)法并排比較。

所以,從架構(gòu)的角度來(lái)看,我一般都不贊成類層次結(jié)構(gòu),但這不是重點(diǎn)。我想說(shuō)的是,我們可以通過(guò)上述發(fā)現(xiàn)的模式大大簡(jiǎn)化 switch 語(yǔ)句。

請(qǐng)記住:這不是我選擇的示例,這可是整潔代碼倡導(dǎo)者用于說(shuō)明的示例。所以,我并沒(méi)有刻意選擇一個(gè)恰巧能夠抽出一個(gè)模式的例子,因此這種現(xiàn)象應(yīng)該比較普遍,因?yàn)榇蠖鄶?shù)相似類型都有類似的算法結(jié)構(gòu),就像這個(gè)例子一樣。

為了利用這種模式,首先我們可以引入一個(gè)簡(jiǎn)單的表,說(shuō)明每種類型的面積計(jì)算需要使用哪個(gè)系數(shù)。其次,對(duì)于圓和正方形之類只需要一個(gè)參數(shù)(圓的參數(shù)為半徑,正方形的參數(shù)為邊長(zhǎng))的形狀,我們可以認(rèn)為它們的長(zhǎng)和寬恰巧相同,這樣我們就可以創(chuàng)建一個(gè)非常簡(jiǎn)單的計(jì)算面積的函數(shù):

/* ======================================================================== LISTING 27 ======================================================================== */f32 const CTable[Shape_Count] = {1.0f, 1.0f, 0.5f, Pi32};f32 GetAreaUnion(shape_union Shape){ f32 Result = CTable[Shape.Type]*Shape.Width*Shape.Height; return Result;}

這個(gè)版本的兩個(gè)求和循環(huán)完全相同,無(wú)需修改,我們只需要將 GetAreaSwitch 換成 GetAreaUnion,其他代碼保持不變。

下面,我們來(lái)看看使用這個(gè)新版本的效果:

廣告
從秘書(shū)起步,十年內(nèi)無(wú)人超越,以一己之力力挽狂瀾成就一段傳奇
×

我們可以看到,從基于類型的思維模式切換到基于函數(shù)的思維模式,我們獲得了巨大的速度提升。從 switch 語(yǔ)句(相較于整潔代碼版本性能已經(jīng)提升了 1.5 倍)換成表驅(qū)動(dòng)的版本,速度全面提升了 10 倍。

我們只是添加了一個(gè)表查找和一行代碼,僅此而已!現(xiàn)在不僅代碼的運(yùn)行速度大幅提升,而且語(yǔ)義的復(fù)雜性也顯著降低。標(biāo)記更少、操作更少、代碼更少。

將數(shù)據(jù)模型與所需的操作融合到一起后,計(jì)算每個(gè)面積的循環(huán)數(shù)量減少到了 3.0~3.5 次。與遵循代碼整潔之道前兩條規(guī)則的代碼相比,這個(gè)版本的速度提高了 10 倍。

10 倍的性能提升非常巨大,我甚至無(wú)法拿 iPhone 做類比,即便是 iPhone 6(現(xiàn)代基準(zhǔn)測(cè)試中最古老的手機(jī))也只比最新的iPhone 14 Pro Max 慢 3 倍左右。

如果是線程桌面性能,10 倍的速度提升就相當(dāng)于如今的 CPU 退回到2010年。代碼整潔之道的前兩條規(guī)則抹殺了 12 年的硬件發(fā)展。

然而,這個(gè)測(cè)試只是一個(gè)非常簡(jiǎn)單的操作。我們還沒(méi)有探討“函數(shù)應(yīng)該只做一件事”以及“盡可能保持小”。如果我們調(diào)整一下問(wèn)題,全面遵循這些規(guī)則,結(jié)果會(huì)怎么樣?

下面這段代碼的層次結(jié)構(gòu)完全相同,但這次我添加了一個(gè)虛函數(shù),用于獲取每個(gè)形狀的角的個(gè)數(shù):

/* ======================================================================== LISTING 32 ======================================================================== */class shape_base{public: shape_base() {} virtual f32 Area() = 0; virtual u32 CornerCount() = 0;};class square : public shape_base{public: square(f32 SideInit) : Side(SideInit) {} virtual f32 Area() {return Side*Side;} virtual u32 CornerCount() {return 4;}private: f32 Side;};class rectangle : public shape_base{public: rectangle(f32 WidthInit, f32 HeightInit) : Width(WidthInit), Height(HeightInit) {} virtual f32 Area() {return Width*Height;} virtual u32 CornerCount() {return 4;}private: f32 Width, Height;};class triangle : public shape_base{public: triangle(f32 BaseInit, f32 HeightInit) : Base(BaseInit), Height(HeightInit) {} virtual f32 Area() {return 0.5f*Base*Height;} virtual u32 CornerCount() {return 3;}private: f32 Base, Height;};class circle : public shape_base{public: circle(f32 RadiusInit) : Radius(RadiusInit) {} virtual f32 Area() {return Pi32*Radius*Radius;} virtual u32 CornerCount() {return 0;}private: f32 Radius;};

長(zhǎng)方形有4個(gè)角,三角形有3個(gè),圓為0。接下來(lái),我們來(lái)修改問(wèn)題的定義,原來(lái)的問(wèn)題是計(jì)算一系列形狀的面積之和,我們改為計(jì)算角加權(quán)的面積總和:總面積之和乘以角的數(shù)量。當(dāng)然,這只是一個(gè)例子,實(shí)際工作中不會(huì)遇到。

下面,我們來(lái)更新“整潔”的求和循環(huán),我們需要添加必要的數(shù)學(xué)運(yùn)算,還需要多調(diào)用一次虛函數(shù):

f32 CornerAreaVTBL(u32 ShapeCount, shape_base **Shapes){ f32 Accum = 0.0f; for(u32 ShapeIndex = 0; ShapeIndex < ShapeCount; ++ShapeIndex) { Accum += (1.0f / (1.0f + (f32)Shapes[ShapeIndex]->CornerCount())) * Shapes[ShapeIndex]->Area(); } return Accum;}f32 CornerAreaVTBL4(u32 ShapeCount, shape_base **Shapes){ f32 Accum0 = 0.0f; f32 Accum1 = 0.0f; f32 Accum2 = 0.0f; f32 Accum3 = 0.0f; u32 Count = ShapeCount/4; while(Count--) { Accum0 += (1.0f / (1.0f + (f32)Shapes[0]->CornerCount())) * Shapes[0]->Area(); Accum1 += (1.0f / (1.0f + (f32)Shapes[1]->CornerCount())) * Shapes[1]->Area(); Accum2 += (1.0f / (1.0f + (f32)Shapes[2]->CornerCount())) * Shapes[2]->Area(); Accum3 += (1.0f / (1.0f + (f32)Shapes[3]->CornerCount())) * Shapes[3]->Area(); Shapes += 4; } f32 Result = (Accum0 + Accum1 + Accum2 + Accum3); return Result;}

其實(shí),我應(yīng)該單獨(dú)寫(xiě)一個(gè)函數(shù),添加另一層間接。為了保證對(duì)“整潔”代碼采取疑罪從無(wú)的原則,我明確保留了這些代碼。

switch 語(yǔ)句的版本也需要相同的修改。首先,我們?cè)偬砑右粋€(gè) switch 語(yǔ)句來(lái)處理角的數(shù)量,case 語(yǔ)句與層次結(jié)構(gòu)版本完全相同:

/* ======================================================================== LISTING 34 ======================================================================== */u32 GetCornerCountSwitch(shape_type Type){ u32 Result = 0; switch(Type) { case Shape_Square: {Result = 4;} break; case Shape_Rectangle: {Result = 4;} break; case Shape_Triangle: {Result = 3;} break; case Shape_Circle: {Result = 0;} break; case Shape_Count: {} break; } return Result;}

接下來(lái),我們按照相同的方式計(jì)算面積:

/* ======================================================================== LISTING 35 ======================================================================== */f32 CornerAreaSwitch(u32 ShapeCount, shape_union *Shapes){ f32 Accum = 0.0f; for(u32 ShapeIndex = 0; ShapeIndex < ShapeCount; ++ShapeIndex) { Accum += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[ShapeIndex].Type))) * GetAreaSwitch(Shapes[ShapeIndex]); } return Accum;}f32 CornerAreaSwitch4(u32 ShapeCount, shape_union *Shapes){ f32 Accum0 = 0.0f; f32 Accum1 = 0.0f; f32 Accum2 = 0.0f; f32 Accum3 = 0.0f; ShapeCount /= 4; while(ShapeCount--) { Accum0 += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[0].Type))) * GetAreaSwitch(Shapes[0]); Accum1 += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[1].Type))) * GetAreaSwitch(Shapes[1]); Accum2 += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[2].Type))) * GetAreaSwitch(Shapes[2]); Accum3 += (1.0f / (1.0f + (f32)GetCornerCountSwitch(Shapes[3].Type))) * GetAreaSwitch(Shapes[3]); Shapes += 4; } f32 Result = (Accum0 + Accum1 + Accum2 + Accum3); return Result;}

與直接求面積總和的版本相同,類層次結(jié)構(gòu)與 switch 語(yǔ)句的實(shí)現(xiàn)代碼幾乎相同。唯一的區(qū)別是,調(diào)用虛函數(shù)還是使用 switch 語(yǔ)句。

下面再來(lái)看看表驅(qū)動(dòng)的寫(xiě)法,你可以看到將操作和數(shù)據(jù)融合在一起的效果有多棒。與所有其他版本不同,在這個(gè)版本中,唯一需要修改的只有表中的值。我們并不需要獲取形狀的次要信息,我們可以將角的個(gè)數(shù)和面積系數(shù)直接放入表中,而代碼保持不變:

/* ======================================================================== LISTING 36 ======================================================================== */f32 const CTable[Shape_Count] = {1.0f / (1.0f + 4.0f), 1.0f / (1.0f + 4.0f), 0.5f / (1.0f + 3.0f), Pi32};f32 GetCornerAreaUnion(shape_union Shape){ f32 Result = CTable[Shape.Type]*Shape.Width*Shape.Height; return Result;}

運(yùn)行上述所有“角加權(quán)面積”函數(shù),我們可以看到它們的性能受第二個(gè)形狀屬性的影響程度:

如你所見(jiàn),“整潔”代碼的性能更糟糕。在直接計(jì)算面積時(shí),switch 語(yǔ)句版本的速度快了 1.5 倍,而如今快了將近2倍,而查找表版本快了近 15 倍。

這說(shuō)明“整潔”的代碼存在更深層次的問(wèn)題:?jiǎn)栴}越復(fù)雜,代碼整潔之道對(duì)性能的損害就越大。當(dāng)你嘗試將代碼整潔之道擴(kuò)展到具有許多屬性的對(duì)象時(shí),代碼的性能普遍會(huì)遭受損失。

使用代碼整潔之道的次數(shù)越多,編譯器就越不清楚你在干什么。一切都在單獨(dú)的翻譯單元中,在虛函數(shù)調(diào)用的后面。無(wú)論編譯器多么聰明,都無(wú)法優(yōu)化這種代碼。

更糟糕的是,你無(wú)法使用此類代碼處理復(fù)雜的邏輯。如上所述,如果你的代碼庫(kù)圍繞函數(shù)而建,那么一些簡(jiǎn)單的功能(例如將值提取到表中和刪除 switch 語(yǔ)句)很容易實(shí)現(xiàn)。但是,如果圍繞類型而建,那么實(shí)際就會(huì)困難得多,若非大量重寫(xiě),甚至可能無(wú)法實(shí)現(xiàn)。

我們只是添加了一個(gè)屬性,速度差異就從 10 倍增至 15 倍。這就像 2023 年的硬件退步到了 2008 年。

然而,也許你已經(jīng)注意到了,我甚至沒(méi)有提到優(yōu)化。除了保證不產(chǎn)生循環(huán)帶來(lái)的依賴之外,出于測(cè)試的目的,我沒(méi)有做任何優(yōu)化!

如果我使用一個(gè)略微優(yōu)化過(guò)的 AVX 版本運(yùn)行這些例程,得到的結(jié)果如下:

速度差異在 20~25 倍之間,當(dāng)然,沒(méi)有任何 AVX 優(yōu)化的代碼使用了類似于代碼整潔之道的原則。

以上,我們只提到了4個(gè)原則,還有第五個(gè)呢?

老實(shí)說(shuō),“不要重復(fù)自己”似乎很好。如上所述,我們沒(méi)有重復(fù)自己。也許你會(huì)說(shuō),四個(gè)計(jì)算累加和的展開(kāi)版本有重復(fù)的嫌疑,但這只是為了演示目的。實(shí)際上,我們不必同時(shí)保留這兩個(gè)例程。

如果“不要重復(fù)自己”有更嚴(yán)格的要求,比如不要構(gòu)建兩個(gè)不同的表來(lái)編碼相同系數(shù)的版本,那么我就有不同意見(jiàn)了,因?yàn)橛袝r(shí)我們必須這樣做才能獲得合理的性能。但是,一般來(lái)說(shuō),“不要重復(fù)自己”只是意味著不要重復(fù)編寫(xiě)完全相同的代碼,所以聽(tīng)起來(lái)像是一個(gè)合理的建議。

最重要的是,我們不必違反它來(lái)編寫(xiě)能夠獲得合理性能的代碼。

“遵循整潔代碼的規(guī)則,你的代碼運(yùn)行速度會(huì)降低15倍。”

因此,對(duì)于整潔代碼之道實(shí)際影響代碼結(jié)構(gòu)的五條建議,我可能只會(huì)考慮一條,其余四條就算了。因?yàn)檎缒闼?jiàn),遵循這些建議會(huì)嚴(yán)重影響軟件的性能。

有人認(rèn)為,遵循代碼整潔之道編寫(xiě)的代碼更易于維護(hù)。然而,即便這是真的,我們也不得不考慮:“代價(jià)是什么?”

我們不可能只是為了減輕程序員的負(fù)擔(dān),而放棄性能,導(dǎo)致硬件性能后退十年或更長(zhǎng)時(shí)間。我們的工作是編寫(xiě)在給定的硬件上運(yùn)行良好的程序。如果這些規(guī)則會(huì)導(dǎo)致軟件的性能變差,那就是不可接受的。

最后,我們應(yīng)該嘗試提出經(jīng)驗(yàn)法則,幫助保持代碼井井有條、易于維護(hù)和易于閱讀。這些目標(biāo)本身沒(méi)什么問(wèn)題,然而因此提出的這些規(guī)則有待思考。下次,再談?wù)撨@些規(guī)則時(shí),我希望加上一條備注:“遵循這些規(guī)則,你的代碼運(yùn)行速度會(huì)變得慢15倍。”

“整潔”的代碼和性能可否兼得?

事實(shí)上,理想與現(xiàn)實(shí)往往存在一定的差距,如工整的字跡一樣,人人都希望能看到整潔代碼,但并非人人都能夠做到,然而不做不到不意味規(guī)則有問(wèn)題。對(duì)此,一些網(wǎng)友也開(kāi)啟了熱議模式:

網(wǎng)友 1:

我認(rèn)為作者將通用建議應(yīng)用到了一個(gè)特殊情況。

違反了整潔代碼之道的**條規(guī)則(也是核心原則之一),計(jì)算每個(gè)面積的循環(huán)數(shù)量就從35次減少到了24次。

大多數(shù)現(xiàn)代軟件的99.9%時(shí)間都花在等待用戶輸入上,僅花費(fèi)0.1%的時(shí)間實(shí)際計(jì)算。如果你正在編寫(xiě)3A游戲或高性能計(jì)算軟件,那么當(dāng)然可以瘋狂優(yōu)化,獲得這些改進(jìn)。

但我們大多數(shù)人都不是這樣做的。大多數(shù)開(kāi)發(fā)人員只不過(guò)是添加計(jì)劃中的下一個(gè)功能。整潔的代碼可以讓功能更快問(wèn)世,而不是為了讓 CPU 做更少的工作。

網(wǎng)友 2:

一個(gè)經(jīng)常被引用的經(jīng)驗(yàn)法則:先跑起來(lái),再讓它變得好看,再提高速度——要按照這個(gè)順序。也就是說(shuō),如果你的代碼一開(kāi)始就很整潔,性能瓶頸就更容易找到和解決。

我有時(shí)希望我的項(xiàng)目中有性能問(wèn)題,但實(shí)際上,性能問(wèn)題經(jīng)常出現(xiàn)在更高的層次,或者架構(gòu)的層面上。比如一個(gè)API網(wǎng)關(guān)在一個(gè)循環(huán)中針對(duì)SAP服務(wù)器發(fā)出了太多的查詢。這會(huì)執(zhí)行數(shù)十億行代碼,但性能的根源是為什么操作員會(huì)一次性點(diǎn)擊許多個(gè)鏈接。

除了學(xué)校作業(yè),我從來(lái)沒(méi)有遇到過(guò)性能問(wèn)題。但我遇到過(guò)很多情況,我不得不處理寫(xiě)得不好(“不整潔”)的代碼,并耗費(fèi)大量腦細(xì)胞來(lái)理解這些代碼。

為此,你是否覺(jué)得”整潔的代碼“與性能是相互沖突的呢?


1063568276