程式碼越“整潔”,效能越“拉胯”?

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERPCRMAI 大模型等等功能:
  • Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本 

作者 | Casey Muratori、譯者 | 彎月
責編 | 蘇宓
編寫“整潔”的程式碼, 這是一條反覆被人提及的程式設計建議,尤其是初學者,聽得太多耳朵都長繭了。“整潔”的程式碼背後是一長串規則,告訴你應該怎麼書寫,程式碼才能保持“整潔”。
實際上,這些規則中很大的一部分並不會影響程式碼的執行時間。我們無法客觀評估這些型別的規則,而且也沒必要進行這樣的評估。然而,一些所謂的“整潔”程式碼規則(其中有一部分甚至被反覆強調)是可以客觀衡量的,因為它們確實會影響程式碼的執行時行為。
整理和歸納“整潔”的程式碼規則,並提取實際影響程式碼結構的規則,我們將得到:
  • 使用多型代替“if/else”和“switch”;
  • 程式碼不應該知道使用物件的內部結構;
  • 嚴格控制函式的規模;
  • 函式應該只做一件事;
  • “DRY”(Don’t Repeat Yourself):不要重複自己。
這些規則非常具體地說明了為了保持程式碼“整潔”,我們應該如何書寫特定的程式碼片段。然而,我的疑問在於,如果建立一段遵循這些規則的程式碼,它的效能如何?
為了構建我認為嚴格遵守“整潔之道”的程式碼,我使用了“整潔”程式碼相關文章中包含的現有示例。也就是說,這些程式碼不是我編寫的,我只是利用他們提供的示例程式碼來評估“整潔”程式碼倡導的規則。

那些年我們見過的“整潔”程式碼

提起“整潔”程式碼的示例,你經常會看到下面這樣的程式碼:

/* ========================================================================

   LISTING 22

   ======================================================================== */

classshape_base

{

public

:

    shape_base() {}

virtual f32 Area()

0

;

};

classsquare

 : 

publicshape_base

{

public

:

    square(f32 SideInit) : Side(SideInit) {}

virtual f32 Area()

{

return

 Side*Side;}

private

:

    f32 Side;

};

classrectangle

 : 

publicshape_base

{

public

:

    rectangle(f32 WidthInit, f32 HeightInit) : Width(WidthInit), Height(HeightInit) {}

virtual f32 Area()

{

return

 Width*Height;}

private

:

    f32 Width, Height;

};

classtriangle

 : 

publicshape_base

{

public

:

    triangle(f32 BaseInit, f32 HeightInit) : Base(BaseInit), Height(HeightInit) {}

virtual f32 Area()

{

return0.5f

*Base*Height;}

private

:

    f32 Base, Height;

};

classcircle

 : 

publicshape_base

{

public

:

    circle(f32 RadiusInit) : Radius(RadiusInit) {}

virtual f32 Area()

{

return

 Pi32*Radius*Radius;}

private

:

    f32 Radius;

};

這段程式碼是一個形狀的基類,從中派生出了一些特定的形狀:圓形、三角形、矩形、正方形。此外,還有一個計算面積的虛擬函式。
就像規則要求的一樣,我們傾向於多型性,函式只做一件事,而且很小。最終,我們得到了一個“整潔”的類層次結構,每個派生類都知道如何計算自己的面積,並存儲了計算面積所需的資料。
如果我們想象使用這個層次結構來做某事,比如計算一系列形狀的總面積,那麼我們希望看到下面這樣的程式碼:

/* ========================================================================

   LISTING 23

   ======================================================================== */

f32 

TotalAreaVTBL(u32 ShapeCount, shape_base **Shapes)

{

    f32 Accum = 

0.0f

;

for

(u32 ShapeIndex = 

0

; ShapeIndex < ShapeCount; ++ShapeIndex)

    {

        Accum += Shapes[ShapeIndex]->Area();

    }

return

 Accum;

}

你可能會發現,此處我沒有使用任何迭代,因為“整潔程式碼之道”中沒有建議你必須使用迭代器。 因此,我想盡可能避免有損“整潔”程式碼的寫法,我不希望新增任何有可能混淆編譯器並導致效能下降的抽象迭代器。
此外,你可能還會注意到,這個迴圈是在一個指標陣列上進行的。這是使用類層次結構的直接結果:我們不知道每種形狀佔用的記憶體有多大。所以除非我們新增另一個虛擬函式呼叫來獲取每個形狀的資料大小,並使用某種步長可變的跳躍過程來遍歷它們,否則我們需要指標來找出每個形狀的實際開始位置。
因為這個計算數一個累加和,所以迴圈本身引起的依賴可能會導致迴圈速度減慢。由於計算累加可以以任意順序進行,為了安全起見,我還寫了一個手動展開的版本:

/* ========================================================================

   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;

}

在一個簡單的測試工具中執行以上這兩個例程,可以粗略地計算出執行該操作每個形狀所需的迴圈總數:
測試工具以兩種不同的方式統計程式碼的時間。第一種方法是隻執行一次程式碼 ,以顯示在沒有預熱的狀態下程式碼的執行時間(在此狀態下,資料應該在 L3 中,但 L2 和 L1 已被重新整理,而且分支預測器尚未針對迴圈進行預測)。
第二種方法是反覆執行程式碼 ,看看當快取和分支預測器以最適合迴圈的方式執行時情況會怎樣。請注意,這些都不是嚴謹的測量,因為正如你所見,我們已經看到了巨大的差異,根本不需要任何嚴謹的分析工具。
從結果中我們可以看出,這兩個例程之間沒有太大區別。這段“整潔”的程式碼計算這個形狀的面積大約需要迴圈35次,如果幸運的話,有可能減少到34次。
所以,我們嚴格遵守“程式碼整潔之道”,最後需要迴圈35次。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

違反“程式碼整潔之道”的第一條規則後

那麼,如果我們違反第一條規則,會怎麼樣?如果我們不使用多型性,使用一個 switch 語句呢?
下面,我又編寫了一段一模一樣的程式碼,只不過這一次我沒有使用類層次結構,而是使用列舉,將所有內容扁平化為一個結構的形狀型別:

/* ========================================================================

   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;

}

這是程式碼整潔之道出現以前,很常見的“老派”寫法。
請注意,由於我們沒有為每個形狀提供特定的資料型別,所以如果某個型別缺乏其中一個值(比如“高度”),計算就不使用了。
現在,這個結構的使用者獲取面積不再需要呼叫虛擬函式,而是需要使用帶有 switch 語句的函式,這違反了“程式碼整潔之道”。即便如此,你會注意到程式碼更加簡潔了,但功能基本相同。switch 語句的每一個 case 的都對應於類層次結構中的一個虛擬函式。
對於求和迴圈本身,你可以看到這段程式碼與上述“整潔”版幾乎相同:

/* ========================================================================

   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;

}

唯一的不同之處在於,我們呼叫常規函式來獲取面積。
但是,我們已經看到了相較於類層次結構,使用扁平結構的直接好處:形狀可以儲存在陣列中,不需要指標。不需要間接訪問,因為所有形狀佔用的記憶體大小都一樣。
另外,我們還獲得了額外的好處,現在編譯器可以確切地看到我們在這個迴圈中做了什麼,因為它只需檢視 GetAreaSwitch 函式。它不必假設只有等到執行時我們才能看得見某些虛擬面積函式具體在做什麼。
那麼,編譯器能利用這些好處為我們做什麼呢?下面,我們來完整地執行一遍四個形狀的面積計算,得到的結果如下:
觀察結果,我們可以看出,改用“老派””的寫法後,程式碼的效能立即提高了 1.5 倍。 我們什麼都沒幹,只是刪除了使用 C++ 多型性的程式碼,就收穫了1.5倍的效能提升。
違反程式碼整潔之道的第一條規則(也是核心原則之一),計算每個面積的迴圈數量就從35次減少到了24次,這意味著,遵循程式碼整潔之道會導致程式碼的速度降低1.5倍。拿手機打個比方,就相當於把 iPhone 14 Pro Max 換成了 iPhone 11 Pro Max。過去三四年間硬體的發展瞬間化無,僅僅是因為有人說要使用多型性,不要使用 switch 語句。
然而,這只是一個開頭。
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/yudao-cloud
  • 影片教程:https://doc.iocoder.cn/video/

違反“程式碼整潔之道”的更多條規則後

如果我們違反更多規則,結果會怎麼樣?如果我們打破第二條規則,“沒有內部知識”,結果會如何?如果我們的函式可以利用自身實際操作的知識來提高效率呢?
回顧一下計算面積的 switch 語句,你會發現所有面積的計算方式都很相似:
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

;

所有形狀的面積計算都是做乘法,長乘以寬、寬乘以高,或者乘以 π 的係數等等。只不過,三角形的面積需要乘以1/2,而圓的面積需要乘以 π。
這是我認為此處使用 switch 語句非常合適的原因之一,儘管這與程式碼整潔之道背道而馳。透過 switch 語句,我們可以很清楚地看到這種模式。當你按照操作而不是型別組織程式碼時,觀察和提取通用模式就很簡單。相比之下,觀察類版本,你可能永遠也發現不了這種模式,因為類版本不僅有很多樣板程式碼,而且你需要將每個類放在一個單獨的檔案中,無法並排比較。
所以,從架構的角度來看,我一般都不贊成類層次結構,但這不是重點。我想說的是,我們可以透過上述發現的模式大大簡化 switch 語句。
請記住:這不是我選擇的示例,這可是整潔程式碼倡導者用於說明的示例。所以,我並沒有刻意選擇一個恰巧能夠抽出一個模式的例子,因此這種現象應該比較普遍,因為大多數相似型別都有類似的演算法結構,就像這個例子一樣。
為了利用這種模式,首先我們可以引入一個簡單的表,說明每種型別的面積計算需要使用哪個係數。其次,對於圓和正方形之類只需要一個引數(圓的引數為半徑,正方形的引數為邊長)的形狀,我們可以認為它們的長和寬恰巧相同,這樣我們就可以建立一個非常簡單的計算面積的函式:

/* ========================================================================

   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;

}

這個版本的兩個求和迴圈完全相同,無需修改,我們只需要將 GetAreaSwitch 換成 GetAreaUnion,其他程式碼保持不變。
下面,我們來看看使用這個新版本的效果:
我們可以看到,從基於型別的思維模式切換到基於函式的思維模式,我們獲得了巨大的速度提升。從 switch 語句(相較於整潔程式碼版本效能已經提升了 1.5 倍)換成表驅動的版本,速度全面提升了 10 倍。
我們只是添加了一個表查詢和一行程式碼,僅此而已!現在不僅程式碼的執行速度大幅提升,而且語義的複雜性也顯著降低。標記更少、操作更少、程式碼更少。
將資料模型與所需的操作融合到一起後,計算每個面積的迴圈數量減少到了 3.0~3.5 次。與遵循程式碼整潔之道前兩條規則的程式碼相比,這個版本的速度提高了 10 倍。
10 倍的效能提升非常巨大,我甚至無法拿 iPhone 做類比,即便是 iPhone 6(現代基準測試中最古老的手機)也只比最新的iPhone 14 Pro Max 慢 3 倍左右。
如果是執行緒桌面效能,10 倍的速度提升就相當於如今的 CPU 退回到2010年。程式碼整潔之道的前兩條規則抹殺了 12 年的硬體發展。
然而,這個測試只是一個非常簡單的操作。我們還沒有探討“函式應該只做一件事”以及“儘可能保持小”。如果我們調整一下問題,全面遵循這些規則,結果會怎麼樣?
下面這段程式碼的層次結構完全相同,但這次我添加了一個虛擬函式,用於獲取每個形狀的角的個數:

/* ========================================================================

   LISTING 32

   ======================================================================== */

classshape_base

{

public

:

    shape_base() {}

virtual f32 Area()

0

;

virtual u32 CornerCount()

0

;

};

classsquare

 : 

publicshape_base

{

public

:

    square(f32 SideInit) : Side(SideInit) {}

virtual f32 Area()

{

return

 Side*Side;}

virtual u32 CornerCount()

{

return4

;}

private

:

    f32 Side;

};

classrectangle

 : 

publicshape_base

{

public

:

    rectangle(f32 WidthInit, f32 HeightInit) : Width(WidthInit), Height(HeightInit) {}

virtual f32 Area()

{

return

 Width*Height;}

virtual u32 CornerCount()

{

return4

;}

private

:

    f32 Width, Height;

};

classtriangle

 : 

publicshape_base

{

public

:

    triangle(f32 BaseInit, f32 HeightInit) : Base(BaseInit), Height(HeightInit) {}

virtual f32 Area()

{

return0.5f

*Base*Height;}

virtual u32 CornerCount()

{

return3

;}

private

:

    f32 Base, Height;

};

classcircle

 : 

publicshape_base

{

public

:

    circle(f32 RadiusInit) : Radius(RadiusInit) {}

virtual f32 Area()

{

return

 Pi32*Radius*Radius;}

virtual u32 CornerCount()

{

return0

;}

private

:

    f32 Radius;

};

長方形有4個角,三角形有3個,圓為0。接下來,我們來修改問題的定義,原來的問題是計算一系列形狀的面積之和,我們改為計算角加權的面積總和:總面積之和乘以角的數量。當然,這只是一個例子,實際工作中不會遇到。
下面,我們來更新“整潔”的求和迴圈,我們需要新增必要的數學運算,還需要多呼叫一次虛擬函式:

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;

}

其實,我應該單獨寫一個函式,新增另一層間接。為了保證對“整潔”程式碼採取疑罪從無的原則,我明確保留了這些程式碼。
switch 語句的版本也需要相同的修改。首先,我們再新增一個 switch 語句來處理角的數量,case 語句與層次結構版本完全相同:

/* ========================================================================

   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;

}

接下來,我們按照相同的方式計算面積:

/* ========================================================================

   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;

}

與直接求面積總和的版本相同,類層次結構與 switch 語句的實現程式碼幾乎相同。唯一的區別是,呼叫虛擬函式還是使用 switch 語句。
下面再來看看錶驅動的寫法,你可以看到將操作和資料融合在一起的效果有多棒。與所有其他版本不同,在這個版本中,唯一需要修改的只有表中的值。我們並不需要獲取形狀的次要資訊,我們可以將角的個數和麵積係數直接放入表中,而程式碼保持不變:

/* ========================================================================

   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;

}

執行上述所有“角加權面積”函式,我們可以看到它們的效能受第二個形狀屬性的影響程度:
如你所見,“整潔”程式碼的效能更糟糕。在直接計算面積時,switch 語句版本的速度快了 1.5 倍,而如今快了將近2倍,而查詢表版本快了近 15 倍。
這說明“整潔”的程式碼存在更深層次的問題:問題越複雜,程式碼整潔之道對效能的損害就越大。 當你嘗試將程式碼整潔之道擴充套件到具有許多屬性的物件時,程式碼的效能普遍會遭受損失。
使用程式碼整潔之道的次數越多,編譯器就越不清楚你在幹什麼。一切都在單獨的翻譯單元中,在虛擬函式呼叫的後面。無論編譯器多麼聰明,都無法最佳化這種程式碼。
更糟糕的是,你無法使用此類程式碼處理複雜的邏輯。如上所述,如果你的程式碼庫圍繞函式而建,那麼一些簡單的功能(例如將值提取到表中和刪除 switch 語句)很容易實現。但是,如果圍繞型別而建,那麼實際就會困難得多,若非大量重寫,甚至可能無法實現。
我們只是添加了一個屬性,速度差異就從 10 倍增至 15 倍。這就像 2023 年的硬體退步到了 2008 年。
然而,也許你已經注意到了,我甚至沒有提到最佳化。除了保證不產生迴圈帶來的依賴之外,出於測試的目的,我沒有做任何最佳化!
如果我使用一個略微最佳化過的 AVX 版本執行這些例程,得到的結果如下:
速度差異在 20~25 倍之間,當然,沒有任何 AVX 最佳化的程式碼使用了類似於程式碼整潔之道的原則。
以上,我們只提到了4個原則,還有第五個呢?
老實說,“不要重複自己”似乎很好。如上所述,我們沒有重複自己。也許你會說,四個計算累加和的展開版本有重複的嫌疑,但這只是為了演示目的。實際上,我們不必同時保留這兩個例程。
如果“不要重複自己”有更嚴格的要求,比如不要構建兩個不同的表來編碼相同係數的版本,那麼我就有不同意見了,因為有時我們必須這樣做才能獲得合理的效能。但是,一般來說,“不要重複自己”只是意味著不要重複編寫完全相同的程式碼,所以聽起來像是一個合理的建議。
最重要的是,我們不必違反它來編寫能夠獲得合理性能的程式碼。

“遵循整潔程式碼的規則,你的程式碼執行速度會降低15倍。”

因此,對於整潔程式碼之道實際影響程式碼結構的五條建議,我可能只會考慮一條,其餘四條就算了。因為正如你所見,遵循這些建議會嚴重影響軟體的效能。
有人認為,遵循程式碼整潔之道編寫的程式碼更易於維護。然而,即便這是真的,我們也不得不考慮:“代價是什麼?”
我們不可能只是為了減輕程式設計師的負擔,而放棄效能,導致硬體效能後退十年或更長時間。 我們的工作是編寫在給定的硬體上執行良好的程式。如果這些規則會導致軟體的效能變差,那就是不可接受的。
最後,我們應該嘗試提出經驗法則,幫助保持程式碼井井有條、易於維護和易於閱讀。這些目標本身沒什麼問題,然而因此提出的這些規則有待思考。下次,再談論這些規則時,我希望加上一條備註:“遵循這些規則,你的程式碼執行速度會變得慢15倍。

“整潔”的程式碼和效能可否兼得?

事實上,理想與現實往往存在一定的差距,如工整的字跡一樣,人人都希望能看到整潔程式碼,但並非人人都能夠做到,然而不做不到不意味規則有問題。對此,一些網友也開啟了熱議模式:
網友 1:
我認為作者將通用建議應用到了一個特殊情況。
違反了整潔程式碼之道的第一條規則(也是核心原則之一),計算每個面積的迴圈數量就從35次減少到了24次。
大多數現代軟體的99.9%時間都花在等待使用者輸入上,僅花費0.1%的時間實際計算。如果你正在編寫3A遊戲或高效能計算軟體,那麼當然可以瘋狂最佳化,獲得這些改進。
但我們大多數人都不是這樣做的。大多數開發人員只不過是新增計劃中的下一個功能。整潔的程式碼可以讓功能更快問世,而不是為了讓 CPU 做更少的工作。

網友 2:

一個經常被引用的經驗法則:先跑起來,再讓它變得好看,再提高速度 ——要按照這個順序。也就是說,如果你的程式碼一開始就很整潔,效能瓶頸就更容易找到和解決。
我有時希望我的專案中有效能問題,但實際上,效能問題經常出現在更高的層次,或者架構的層面上。比如一個API閘道器在一個迴圈中針對SAP伺服器發出了太多的查詢。這會執行數十億行程式碼,但效能的根源是為什麼操作員會一次性點選許多個連結。
除了學校作業,我從來沒有遇到過效能問題。但我遇到過很多情況,我不得不處理寫得不好(“不整潔”)的程式碼,並耗費大量腦細胞來理解這些程式碼。
為此,你是否覺得”整潔的程式碼“與效能是相互衝突的呢?

歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。
文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章