99精品伊人亚洲|最近国产中文炮友|九草在线视频支援|AV网站大全最新|美女黄片免费观看|国产精品资源视频|精彩无码视频一区|91大神在线后入|伊人终合在线播放|久草综合久久中文

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

“干凈”的代碼,賊差的性能

jf_WZTOguxH ? 來源:InfoQ ? 2023-03-07 09:52 ? 次閱讀
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

如今很多機(jī)構(gòu)里傳授的所謂編程“最佳實(shí)踐”,壓根就是隨時可能爆炸的性能災(zāi)難。

很多程序員還是一個“小萌新”時就聽過這樣的說法:寫出來的代碼必須得“干凈”,為此很多人做了大量的閱讀和學(xué)習(xí)。

Redux 作者 Dan Abramov 就曾癡迷于“干凈代碼”和刪除重復(fù)代碼。多年前他和同事一起開發(fā)一個圖形編輯器畫布,當(dāng)看到同事提交代碼時,他吐槽道,“這些重復(fù)代碼看起來真的很礙眼。”隨后,他自己想辦法把重復(fù)的代碼刪掉了。

“夜已深,我把改好的代碼提交到 master 分支,然后上床睡覺。因?yàn)閹屯掳央s亂的代碼清理干凈了,我心里還引以為豪?!钡聦?shí)并不像他想象的美好,第二天老板看到后找他談話,希望他代碼回滾回去。

當(dāng)時的 Dan 很不理解,直到再工作了幾年后他才明白,除了團(tuán)隊協(xié)作方面考慮,他為了減少重復(fù)代碼犧牲了靈活性?!斑@算不上是一個好的權(quán)衡。”他坦誠道。

無獨(dú)有偶,專門從事游戲引擎研發(fā)的資深開發(fā)者 Casey Muratori 近日也發(fā)表文章稱,那些所謂“干凈”代碼的規(guī)則“其實(shí)挺無所謂的,多數(shù)情況下也不太影響代碼的實(shí)際運(yùn)行?!?/p>

這是 Casey 親自測試的結(jié)果,他表示,“認(rèn)真分析就會發(fā)現(xiàn),其中很多要求設(shè)置得相當(dāng)隨意,難以證實(shí)或證偽。但也有一些則非?!f惡’,確實(shí)會影響到代碼的運(yùn)行效果?!蔽覀儗?Casey 的測試分享做了翻譯,以饗讀者。

“干凈代碼”的性能測試

下面來看幾條有代表性的“干凈”建議:

? 相較于“if/else”和“switch”,盡量用多態(tài);

? 不要告訴代碼它所處理的對象內(nèi)部;

? 函數(shù)應(yīng)該小一點(diǎn);函數(shù)應(yīng)該只做一件事;

? “DRY”——別重復(fù)自己。

這些要求相當(dāng)具體,聽起來只要照著做了,就讓編寫出“干凈”的代碼。但問題是,這樣的代碼執(zhí)行起來效果如何?

為了更確切地測試“干凈”代碼的實(shí)際表現(xiàn),我決定直接用相關(guān)文獻(xiàn)里列出的示例代碼。這樣大家就不能說我故意黑了吧,這里只是用人家提供的現(xiàn)成結(jié)果來評估“干凈”代碼到底能不能打。

盡量用多態(tài)?

相信很多朋友都見過如下“干凈”代碼實(shí)例:

/* ========================================================================
   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;
};

這是一個基礎(chǔ)類,能提供幾種特定形狀:圓形、三角形、矩形、正方形。之后,它還提供一個用于計算面積的虛擬函數(shù)。

跟之前的要求一樣,這里用的是多態(tài),函數(shù)小而且只做一件事,總之完全符合規(guī)定。于是,我們最終得到了非?!案蓛簟钡念悓哟谓Y(jié)構(gòu)。每個派生的類都知道如何計算自己的面積,并存儲面積計算所需要的數(shù)據(jù)。

如果我們想要實(shí)際應(yīng)用這個層次結(jié)構(gòu),比如想求輸入的所有形狀的面積總和,那大概應(yīng)該是這樣:

/* ========================================================================
   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;
}

大家可能注意到了,我在這里沒有使用迭代器,因?yàn)椤案蓛簟币?guī)則里并沒有建議要使用迭代器。為了避免對編譯器的混淆和對性能差異造成的影響,這里我決定不引入任何抽象迭代器。

另外,這個循環(huán)還基于一系列指針。這是使用類層次結(jié)構(gòu)所帶來的直接結(jié)果:我們不知道這些形狀在內(nèi)存里有多大,所以除非添加另外一個虛擬函數(shù)調(diào)用來獲取各形狀的數(shù)據(jù)大小、并引入某種可變的跳過操作,否則就必須要靠指針來找到各個形狀的實(shí)際起始位置。

這里做的是累加計算,所以會存在循環(huán)依賴性,這會導(dǎo)致循環(huán)速度下降。為了能隨意對累加進(jìn)行重新排序,我還編寫了一個手填版本以確保安全:

/* ========================================================================
   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;
}

如果只對這兩個例程做簡單測試,我們就能粗略測量出每個形狀完成計算所消耗的 CPU 時鐘周期:

66684d1a-bc7d-11ed-bfe3-dac502259ad0.png

這里用兩種不同方式進(jìn)行代碼測試。第一種是僅運(yùn)行一次,表達(dá)“冷”狀態(tài)下的計算情況——這時數(shù)據(jù)應(yīng)存留于 L3 緩存內(nèi),但 L2 和 L1 已被刷新清空,而且分支預(yù)測變量也尚未在循環(huán)中“預(yù)演”過。

第二種則是多次運(yùn)行代碼,查看緩存和分支預(yù)測變量都“熱”著的時候,循環(huán)性能如何。請注意,我的這些辦法都不是真正的精準(zhǔn)測量。大家也能看到,其中的差異如此巨大,壓根就沒必要使用嚴(yán)肅的分析工具。

從結(jié)果來看,這兩個例程沒有太大區(qū)別。“干凈”代碼在計算形狀面積時大概消耗了 35 個計算周期,如果運(yùn)氣好,有時候是 34 個。也就是說,如果嚴(yán)格按照“干凈”編程的原則處理,那我們要用掉 35 個計算周期。

可如果不管第一條規(guī)矩,結(jié)果會怎樣?這里我們不使用多態(tài),直接上 switch 語句。

我在這里編寫了完全相同的代碼,只是不再采取類層次結(jié)構(gòu)的形式(也就是運(yùn)行時上的 vtable),而是通過枚舉和形狀類型把所有內(nèi)容都塞進(jìn)了單一結(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;
}

這就是我們被“干凈”代碼忽悠之前,那種最老派的編程方式。

請注意,因?yàn)檫@里不再為各種形狀變體指定相應(yīng)的數(shù)據(jù)類型,所以如果類型不具備所討論的某個值(例如「高度」),則直接忽略。

現(xiàn)在,這段代碼不再從虛擬函數(shù)調(diào)用中獲取面積,而是通過 switch 語句從函數(shù)中獲取——這跟“干凈”編程的原則完全不符。但大家應(yīng)該看得出來,后面這種更簡潔,而且代碼并沒多大變化。Switch 語句的每種執(zhí)行情況,都跟類層次結(jié)構(gòu)中的相應(yīng)虛擬函數(shù)有著相同的代碼。

至于加和循環(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;
}

唯一的區(qū)別,就是我們在這里沒有調(diào)用成員函數(shù)來獲取面積,而是調(diào)用了一個正則函數(shù)。就這么點(diǎn)不同。

但很明顯,與類層次結(jié)構(gòu)相比,扁平結(jié)構(gòu)是有很多好處的:形狀都在矩陣?yán)?,根本不需要指針。而且因?yàn)樗行螤畹拇笮《枷嗤?,所以也不需要其他間接轉(zhuǎn)換。

另外,編譯器現(xiàn)在可以準(zhǔn)確理解我們在循環(huán)中的操作,即查看 GetAreaSwitch 函數(shù)并查看整個代碼路徑。這樣,編譯器就用不著對只向運(yùn)行時開放的虛擬面積函數(shù)做操作猜測。

那這些好處到底會在編譯器里轉(zhuǎn)化成怎樣的效果?這里我們一口氣把運(yùn)行四種形狀,結(jié)果是:

6671829a-bc7d-11ed-bfe3-dac502259ad0.png

通過觀察結(jié)果,我們會發(fā)現(xiàn)一些很有趣的現(xiàn)象。單單把代碼改得“老派”一點(diǎn),我們就讓性能提升了 1.5 倍。是的,別用 C++ 多態(tài)這種無關(guān)緊要的東西,性能馬上就有了改善。

通過違反“干凈”代碼原則的頭一條(也是比較核心的一條),我們把各形狀面積計算的時鐘周期從 35 個降低到 24 個。如果要拿硬件做比較,就相當(dāng)于是 iPhone 14 Pro Max 降級成了 iPhone 11 Pro Max。這是三到四年的硬件演化進(jìn)程,只靠不用多態(tài)就給消弭掉了。

但這還只是剛剛開始。

忽略對象內(nèi)部?

如果我們違反更多規(guī)矩,會怎么樣?比如說去掉第二條,“忽略對象內(nèi)部”。我們能不能靠內(nèi)部知識幫函數(shù)提高運(yùn)行效率?

回顧一下計算面積的 switch 語句,我們會發(fā)現(xiàn)所有面積計算用的都是相似的方法:

        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;
caseShape_Circle:{Result=Pi32*Shape.Width*Shape.Width;}break;

也就是都在用高度乘以高度、寬度乘以寬度,需要時再乘個π之類的系數(shù)。如果是圓,那就除以 2。

這就是我跟“干凈”代碼原則最不對付的地方,我覺得 switch 語句很棒!它能向我們清晰地展示這些模式,因?yàn)樵诎床僮鳎ǘ皇前搭愋停┻M(jìn)行代碼組織時,可以很直觀地發(fā)現(xiàn)其中的常規(guī)模式。相比之下,再看“干凈”編程示例,我們可能永遠(yuǎn)發(fā)現(xiàn)不了這樣的模式。那邊不僅樣板更多,而且倡導(dǎo)者建議把每個類都放進(jìn)單獨(dú)的文件里。

所以從結(jié)構(gòu)上講,我一般不贊成使用類層次結(jié)構(gòu)??偠灾F(xiàn)在我想強(qiáng)調(diào)最重要的一點(diǎn)——我們可以通過觀察模式,來大大簡化這條 switch 語句。

請記?。哼@個示例不是我選的。這是“干凈”代碼自己選的說明示例。而且跟面積計算類似,其他很多任務(wù)也有相似的算法結(jié)構(gòu)。要想利用這種模式,我們可以整理一個簡單的表,用于說明每種類型所對應(yīng)的系數(shù)。如果我們將圓形和矩形等設(shè)定為單參數(shù)類型,就可以寫出更簡單的求面積函數(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;
}

這里的兩個求和循環(huán)不用做多大修改,除了只能調(diào)用 GetAreaUnion(而非 GetAreaSwitch),其余部分完全相同。

下面來看看這個版本的運(yùn)行性能如何:

6689472c-bc7d-11ed-bfe3-dac502259ad0.png

可以看到,通過對實(shí)際類型的理解,我們有效將基于類型的思路轉(zhuǎn)換成了基于函數(shù)的思路,從而大大提高了速度。跟之前的 iPhone 相比,現(xiàn)在我們的計算速度已經(jīng)相當(dāng)于登陸了臺式機(jī)。

而我們唯一所做的,就是一次表查找加一行代碼,沒別的了!這樣不僅更快,在語義上也更簡單。它涉及的 token 更少、操作更少、代碼行數(shù)也更少。

所以說,我們有必要把數(shù)據(jù)模型跟計算操作結(jié)合起來,而不是要求什么“忽略內(nèi)部”?,F(xiàn)在,我們對每個形狀的面積計算只消耗 3.0 到 3.5 個計算周期。

放棄前兩條“干凈”編程規(guī)則,已經(jīng)讓我們的代碼性能提升了 10 倍。

10 倍性能提升絕對非同小可,畢竟就連多年之前推出的 iPhone 6(現(xiàn)代性能基準(zhǔn)測試所能支持的最老機(jī)型),其性能也只是 iPhone 14 Pro Max 的三分之一。

如果用單線程桌面 CPU 性能來比較,那 10 倍的差距就相當(dāng)于拿現(xiàn)在的 CPU 跟 2010 年的產(chǎn)品對抗??吹搅税桑瑔问乔皟蓷l“干凈”編程規(guī)則,就消滅了這 12 年來的硬件演變成果。

函數(shù)應(yīng)該小一點(diǎn)、專一點(diǎn)?

更令人震驚的是,恢復(fù)這部分性能的操作如此簡單。這里我們沒有強(qiáng)調(diào)“函數(shù)要小”和“函數(shù)只做一件事”這兩條,畢竟我們這個測試很簡單,天然符合這些規(guī)定。那么,如果我們在問題里再加個要求,應(yīng)該就能看到它們的實(shí)際影響了吧?

這里,我在原有層次結(jié)構(gòu)之上又添加了一個虛擬函數(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;
};

矩形有四個角,三角形有三個角,圓形一個角都沒有。之后,我要調(diào)整問題的定義,從計算各形狀的總面積轉(zhuǎn)為計算各形狀的角加權(quán)面積和——也就是總面積再加上角總數(shù)。

跟總面積一樣,算這個角加權(quán)面積沒有任何實(shí)際意義,單純是為了演示性能差異,用的也是最簡單的數(shù)學(xué)計算。

這里,我用數(shù)學(xué)計算和其他虛擬函數(shù)調(diào)用更新了“干凈”求和循環(huán):

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ù),添加了新的間接層。同樣是出于明確起見,這里不用任何抽象。

在 switch 語句那邊,我做的變更也基本相同。先是給角數(shù)量添加另一條 switch 語句,跟層次結(jié)構(gòu)版本可以說是完美對應(yīng):

/* ========================================================================
   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;
}

跟之前的求總面積類似,類層次結(jié)構(gòu)和 switch 兩種實(shí)現(xiàn)之間的代碼基本相同。唯一的區(qū)別,就是調(diào)用虛擬函數(shù)還是使用 switch 語句。

再來看表驅(qū)動的示例,這種把計算操作跟數(shù)據(jù)結(jié)合起來辦法真的棒。而且這個版本需要修改的只有表里的值。我們甚至不需要獲取關(guān)于形狀的其他信息,只要把角數(shù)跟面積系數(shù)直接加進(jìn)表中,就能用幾乎相同的代碼得出結(jié)果:

/* ========================================================================
   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)行所有“角面積”函數(shù),就能看到第二個形狀的屬性如何影響其性能:

66f95a9e-bc7d-11ed-bfe3-dac502259ad0.png

可以看到,這次測試中“干凈”代碼的表現(xiàn)更差。Switch 語句的性能達(dá)到了“干凈”版本的 2 倍,而查表版本更是達(dá)到后者的 15 倍。

這也凸顯出“干凈”代碼的深層次問題:需求越復(fù)雜,這些規(guī)矩就越有損性能。當(dāng)我們把這種“干凈”編程方法引入各種真實(shí)用例時,最終性能肯定會大打折扣。

而且“干凈”代碼用得越多,編譯器就越理解不了你想干什么。一切都被放進(jìn)了單獨(dú)的翻譯單元,被藏在虛擬函數(shù)調(diào)用之后。這樣即使編譯器再聰明,也難以消化這混亂的實(shí)現(xiàn)。

更可怕的是,這樣的代碼連人看了都會束手無策!從之前的演示中可以看到,如果代碼庫圍繞著函數(shù)進(jìn)行架構(gòu)設(shè)計,那么從表中取值或者刪除 switch 語句等需求才會易于實(shí)現(xiàn);而如果是圍繞類型進(jìn)行架構(gòu)設(shè)計,那難度將大大增加。唯一的解決辦法,恐怕就只有大規(guī)模重寫。

總之,只是在形狀計算中增加了一個屬性,速度差就從 10 倍變成了 15 倍,相當(dāng)于硬件性能從 2023 年一下子倒退回了 2008 年!一個參數(shù),抹滅 14 年硬件發(fā)展,是不是很大膽?而且,咱們還完全沒涉及優(yōu)化呢。

之前的所有演示,都只是在拿循環(huán)依賴關(guān)系做文章,完全沒提有哪些優(yōu)化空間。下面,我們來看相同計算流程在經(jīng)過輕度優(yōu)化后的 AVX 版本:

672a02e8-bc7d-11ed-bfe3-dac502259ad0.png

速度差異到了 20 到 25 倍區(qū)間。當(dāng)然,AVX 優(yōu)化的代碼完全不理會“干凈”編程的那些奇談怪論。五大原則已經(jīng)祛魅了四條,再來看最后一條。

不要重復(fù)自己?

老實(shí)講,“不要重復(fù)自己”其實(shí)是有道理的。我們拿來測試的版本也沒有多少重復(fù)部分。只有 4 次累加的部分算是重復(fù),但這是為了演示。畢竟如果是在真實(shí)應(yīng)用當(dāng)中,我們甚至沒必要把它分成 2 個例程。

如果把“不要重復(fù)自己”說得更具體點(diǎn),比如不要把相同系數(shù)的兩個編碼版本分別構(gòu)建成兩個表,那我還可以反對一下。畢竟有時候這樣能獲得更好的性能。但人家沒那么講,只是說別自我重復(fù),那這話還是相當(dāng)合理的。

最重要的是,我們完全可以在遵循第五條的同時保持合理的代碼性能。

結(jié) 論

所以我現(xiàn)在給出結(jié)論:在這五條原則里,只有最后一條值得遵循,前面四條可以統(tǒng)統(tǒng)無視。為什么?大家可能注意到了,現(xiàn)在的軟件運(yùn)行起來真的越來越慢。跟現(xiàn)代硬件的真實(shí)性能相比,軟件的運(yùn)行表現(xiàn)太差了。

要問為什么這么慢,那答案可就多了,而最核心的因素要視實(shí)際開發(fā)環(huán)境和編程方法而定。但至少從特定角度出發(fā),“干凈”代碼絕對有著不可推卸的責(zé)任。雖然其底層邏輯都說得通,但造成的性能負(fù)擔(dān)卻是我們難以承受的。

所以面對這種種規(guī)矩,盡管有人認(rèn)為這樣能改善代碼庫的可維護(hù)性,但我們至少也該想想背后的代價是什么。

我們真的愿意放棄這十幾年的硬件發(fā)展,只為讓程序員的工作變得更輕松一點(diǎn)嗎?我們的職責(zé)就是開發(fā)出能順暢在硬件上運(yùn)行的程序。如果這些原則嚴(yán)重影響了軟件的運(yùn)行效果,那豈不背離了我們的從業(yè)初衷?

當(dāng)然,我們?nèi)匀豢梢岳^續(xù)探索更好的代碼組織、維護(hù)改進(jìn)和易讀性方法,這些都是非常合理的訴求。但“干凈”編程的這些規(guī)矩不是,它們根本就不靠譜。我強(qiáng)烈建議他們能用大星號標(biāo)明“采取這些規(guī)則,您的代碼性能將縮水十幾倍”。

審核編輯 :李倩


聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4381

    瀏覽量

    64904
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4900

    瀏覽量

    70758

原文標(biāo)題:“干凈”的代碼,賊差的性能

文章出處:【微信號:AI前線,微信公眾號:AI前線】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關(guān)推薦
    熱點(diǎn)推薦

    Intersil新推一流壓和瞬態(tài)性能的新款LDO穩(wěn)壓器

    創(chuàng)新電源管理與精密模擬解決方案領(lǐng)先供應(yīng)商Intersil公司(納斯達(dá)克交易代碼:ISIL)今天宣布,推出兩款新的高性能單路輸出低壓(LDO)穩(wěn)壓器--- ISL80510和ISL80505,它們可提供一流的壓
    發(fā)表于 10-09 11:15 ?1264次閱讀

    如何編寫高性能的Rust代碼

    為了最大限度地提高Rust應(yīng)用程序的性能,你需要了解支持代碼的底層硬件架構(gòu),如何優(yōu)化算法和數(shù)據(jù)結(jié)構(gòu),以及如何對代碼進(jìn)行配置和基準(zhǔn)測試。在本文中,我們將簡要介紹這些主題,希望能更好地理解如何編寫高
    的頭像 發(fā)表于 11-03 14:28 ?1174次閱讀
    如何編寫高<b class='flag-5'>性能</b>的Rust<b class='flag-5'>代碼</b>

    multisim如何干凈的卸載?

    multisim在卸載后往往不能重新安裝?如何才能干凈的卸載?
    發(fā)表于 03-17 13:16

    分ADC中不同電阻容對THD性能的影響

    本應(yīng)用筆記介紹了輸入端相同值電阻的不同容如何改變?nèi)?b class='flag-5'>差分ADC的THD性能。電阻器的成本隨著容的每個較低增量而顯著變化 概觀該MAX11905是一個20位全
    發(fā)表于 12-17 22:13

    如何測量高速信號比較快速干凈?

    您想在高速信號上進(jìn)行快速而又比較干凈(精確)的測量嗎?沒時間把探頭尖端焊接到器件上?不確定高速設(shè)計的問題來自哪兒?這些都是工程師們經(jīng)常遇到的問題。隨著時間壓力越來越大,偶發(fā)問題阻礙項(xiàng)目竣工,您需要一種快捷、簡便、高性能的方法,來測量高速信號。
    發(fā)表于 08-09 08:21

    新建C++工程生成比較干凈代碼

    這一章新建一個工程,主要目的是練習(xí)新建C++工程,生成比較干凈代碼,后來發(fā)現(xiàn)沒在太大的意義,直接在原示例中刪除文件,然后新建cpp文件即可,也可以把原有main.c的屬性變成c++,方法
    發(fā)表于 08-09 07:12

    LabVIEW軟件卸載不干凈

    LabVIEW刪掉后不能刪干凈,也不能重新下載
    發(fā)表于 03-24 13:24

    干凈地”指什么?具體是什么含義?

    \"在輸入輸出電路的位置設(shè)置“干凈地”以減小電纜上的共模電壓”,干凈地指什么?具體是什么含義?
    發(fā)表于 10-21 08:41

    如何使用Thumb-2改善代碼密度和性能

    如何使用Thumb-2改善代碼密度和性能
    發(fā)表于 01-12 18:07 ?9次下載

    6--時間分法(幀間分法)opencv和vc代碼實(shí)現(xiàn)

    時間分法(幀間分法)opencv和vc代碼實(shí)現(xiàn),用于目標(biāo)檢測
    發(fā)表于 05-17 10:31 ?13次下載

    寶馬也能協(xié)助警察抓捕盜車 遠(yuǎn)程鎖門功能大顯神威

    盜車們要小心了,千萬別想著去偷寶馬公司的最新款汽車,如果不信邪非要下手的話,就要有被困在車中的覺悟。
    發(fā)表于 12-07 10:45 ?1710次閱讀

    用于MPLAB X IDE代碼性能分析插件的工作原理和代碼性能分析參考

    MPLAB X IDE提供收集有關(guān)C代碼函數(shù)的函數(shù)級性能分析(Function Level Profiling, FLP)數(shù)據(jù)的功能。但是,該數(shù)據(jù)無法在未安裝MPLAB X IDE插件——代碼
    發(fā)表于 06-11 04:28 ?11次下載
    用于MPLAB X IDE<b class='flag-5'>代碼</b><b class='flag-5'>性能</b>分析插件的工作原理和<b class='flag-5'>代碼</b><b class='flag-5'>性能</b>分析參考

    對于代碼規(guī)范的一些總結(jié)

    都說代碼是程序員的第二張臉,長時間下來,寫的好的代碼定會受到大家的尊重。遵循一些簡單的規(guī)范,寫干凈一致的代碼!把個性用在寫出最簡單易懂的代碼
    的頭像 發(fā)表于 12-08 10:21 ?3636次閱讀

    分ADC中不同電阻容對THD性能的影響

    本應(yīng)用筆記解釋了輸入端相同值電阻的不同容如何改變?nèi)?b class='flag-5'>差分ADC的THD性能。電阻器的成本隨著容每降低一次而顯著變化
    的頭像 發(fā)表于 01-12 09:38 ?1885次閱讀
    <b class='flag-5'>差</b>分ADC中不同電阻容<b class='flag-5'>差</b>對THD<b class='flag-5'>性能</b>的影響

    什么是干凈的電壓波形呢?干凈的電壓波形與不干凈的電壓波形有什么區(qū)別呢?

    什么是干凈的電壓波形呢?干凈的電壓波形與不干凈的電壓波形有什么區(qū)別呢? 干凈的電壓波形是指在電路中傳輸?shù)碾妷盒盘枦]有噪聲干擾和失真,呈現(xiàn)出穩(wěn)定、規(guī)整、純凈的波形。而不
    的頭像 發(fā)表于 11-17 14:49 ?1422次閱讀