跳轉到內容

最佳化 C++/程式碼最佳化/指令計數

來自華夏公益教科書,開放的書籍,為開放的世界

即使生成內聯程式碼的語言特性也可能產生顯著的成本,因為這些指令無論如何都要執行。本節介紹了一些減少處理器執行給定操作所需機器指令總數的技術。

switch 語句中的 case 排序

[編輯 | 編輯原始碼]

switch 語句中,按機率降序排列 case。

在第 3.1 節的“switch 語句中的 case 排序”指南中,已經建議將最典型的 case(即被認為最有可能的 case)放在前面。作為進一步的最佳化,您可以在典型的執行中統計每個 case 被選擇的實際次數,並按照從最頻繁到最不頻繁的順序排序 case。 [存疑 ]

如果一個整數值在應用程式程式碼中是常量,但在庫程式碼中是變數,請將其設定為模板引數。

假設您正在編寫以下庫函式,其中 xy 在庫開發時沒有已知的值

int f1(int x, int y) { return x * y; }

該函式可能從以下應用程式程式碼中呼叫,其中 x 沒有常數值,但 y 是常量 4

int a = f1(b, 4);

如果在編寫庫時,您知道呼叫方肯定會為引數 y 傳遞一個常量,您可以將函式轉換為以下函式模板

template <int Y> int f2(int x) { return x * Y; }

該函式可能從以下應用程式程式碼中呼叫

int a = f2<4>(b);

這種呼叫會自動例項化以下函式

int f2(int x) { return x * 4; }

後一個函式比前一個函式 f1 更快,原因如下

  • 只有一個引數傳遞給函式 (x),而不是兩個 (xy)。
  • 用整數常量 (4) 乘法總是比用整數變數 (y) 乘法更快。
  • 由於常數值 (4) 是 2 的冪,編譯器會執行位移運算,而不是整數乘法。

一般來說,整數模板引數對於例項化模板的人(因此對於編譯器)來說是常量,而常量比變數處理得更有效率。此外,一些涉及常量的操作在編譯時預先計算。

如果已經有一個普通函式,而是函式模板,只需向該模板新增一個額外的引數即可。

奇怪的遞迴模板模式

[編輯 | 編輯原始碼]

如果您需要編寫一個庫抽象基類,以便在應用程式程式碼中的每個演算法中僅使用一個從該基類派生的類,請使用 奇怪的遞迴模板模式

假設您正在編寫以下庫基類

class Base {
public:
    void g() { f(); }
private:
    virtual void f() = 0;
};

在這個類中,函式 g 執行一個演算法,該演算法將函式 f 作為演算法的抽象操作呼叫。用設計模式術語來說,g 是一個 模板方法 設計模式。該類的目的是允許編寫以下應用程式程式碼

class Derived1: public Base {
private:
    virtual void f() { ... }
};
...
Base* p1 = new Derived1;
p1->g();

在這種情況下,可以將以前的庫程式碼轉換為以下程式碼

template <class Derived> class Base {
public:
    void g() { f(); }
private:
    void f() { static_cast<Derived*>(this)->f(); }
};

因此,應用程式程式碼將變為以下程式碼

class Derived1: public Base<Derived1> {
private:
    void f() { ... }
};
...
Derived1* p1 = new Derived1;
p1->g();

這樣一來,Base<Derived1>::g 中對 f 的呼叫就靜態繫結到成員函式 Derived1::f,也就是說對該函式的呼叫不再是 virtual,可以內聯。

不過,假設您想要新增以下定義

class Derived2: public Base<Derived2> {
protected:
    void f() { ... }
};

使用這種技術,不可能定義指向基類的指標或引用,該基類是 Derived1Derived2 的公共基類,因為這些基類是兩種不相關的型別;因此,當您想允許應用程式程式碼定義任意從類 Base 派生的物件的容器時,此技術不適用。

其他限制包括

  • Base 必須是抽象型別;
  • 型別 Derived1 的物件不能轉換為型別 Derived2 的物件,反之亦然;
  • 對於每個 Base 的派生類,為 Base 生成的所有機器程式碼都會被複制。

策略設計模式

[編輯 | 編輯原始碼]

如果實現 策略 設計模式(又名策略)的物件在應用程式程式碼的每個演算法中都是常量,請消除該物件,將所有成員設定為 static,並將它的類新增為模板引數。

假設您正在編寫以下庫程式碼,該程式碼實現了策略設計模式

class C;
class Strategy {
public:
    virtual bool is_valid(const C&) const = 0;
    virtual void run(C&) const = 0;
};

class C {
public:
    void set_strategy(const Strategy& s) { s_ = s; }
    void f() { if (s_.is_valid(*this)) s_.run(*this); }
private:
    Strategy s_;
};

這個庫程式碼的目的是允許以下應用程式程式碼

class MyStrategy: public Strategy {
public:
    virtual bool is_valid(const C& c) const { ... }
    virtual void run(C& c) const { ... }
};
...
MyStrategy s; // Object representing my strategy.
C c; // Object containing an algorithm with customizable strategy.
c.set_strategy(s); // Assignment of the custom strategy.
c.f(); // Execution of the algorithm with assigned strategy.

在這種情況下,可以將以前的庫程式碼轉換為以下程式碼

template <class Strategy>
class C {
public:
    void f() {
        if (Strategy::is_valid(*this)) Strategy::run(*this);
    }
};

因此,應用程式程式碼將變為以下程式碼

class MyStrategy {
public:
    static bool is_valid(const C<MyStrategy>& c) { ... }
    static void run(C<MyStrategy>& c) { ... }
};
...

C<MyStrategy> c; // Object with statically assigned strategy.
c.f(); // Execution with statically assigned strategy.

這樣一來,策略物件就消失了,成員函式 MyStrategy::is_validMyStrategy::run 就被靜態繫結,也就是說避免了對 virtual 函式的呼叫。

不過,這種解決方案不允許在執行時選擇策略,當然也不允許在物件生命週期內更改策略。此外,演算法程式碼對於其類的每次例項化都會被複制。

位運算子

[編輯 | 編輯原始碼]

如果您需要對一組位執行布林運算,請將這些位放入一個 unsigned int 物件中,並在其上使用位運算子。

位運算子 (&, |, ^, <<>>) 會被翻譯成單個快速指令,並在單個指令中對暫存器的所有位進行操作。

華夏公益教科書