自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

性能翻倍!揭秘編譯器如何偷偷加速你的 C++代碼:RVO/NRVO 詳解

開發(fā)
你有沒有好奇過,為什么有時(shí)候 C++ 代碼明明應(yīng)該很慢,卻跑得飛快?今天咱們就一起來扒一扒編譯器背后的那些小動(dòng)作!

前段時(shí)間我在調(diào)試一段代碼時(shí),發(fā)現(xiàn)了一個(gè)有趣的現(xiàn)象:我寫了一個(gè)函數(shù),它返回了一個(gè)超大的對象(幾G那種),按理說這玩意復(fù)制一次得花不少時(shí)間,可實(shí)際運(yùn)行起來卻快得出奇。

當(dāng)時(shí)我就納悶了:這不科學(xué)??!

直到我深究了"RVO"和"NRVO",這才恍然大悟。原來編譯器早就偷偷幫我們做了優(yōu)化,只是我們不知道而已!

今天,就讓我們一起來扒一扒這些編譯器背后的小動(dòng)作,看看它們是如何在你不經(jīng)意間就幫你的代碼提速的。不管你是剛?cè)腴T的小白,還是已經(jīng)寫了幾年代碼的老鳥,相信都能從中有所收獲。

一、什么是返回值優(yōu)化(RVO)?

1. 先來聊聊沒有優(yōu)化時(shí)會發(fā)生什么

想象一下這個(gè)場景:你寫了一個(gè)函數(shù),它需要返回一個(gè)大對象,比如說這樣:

class BigObject {
    // 假設(shè)這個(gè)類很大,有一大堆數(shù)據(jù)
    char *data;
    // ...其他成員
public:
    BigObject() { 
        cout << "構(gòu)造函數(shù)被調(diào)用" << endl; 
    }
    
    BigObject(const BigObject& other) { 
        cout << "復(fù)制構(gòu)造函數(shù)被調(diào)用" << endl; 
        // 復(fù)制數(shù)據(jù)
    }
    
    ~BigObject() { 
        cout << "析構(gòu)函數(shù)被調(diào)用" << endl; 
    }
};

BigObject createBigObject() {
    // 直接返回一個(gè)臨時(shí)對象
    return BigObject(); // 返回一個(gè)無名臨時(shí)對象
}

int main() {
    BigObject myObj = createBigObject(); // 調(diào)用函數(shù)并接收返回值
    // 使用myObj...
    return 0;
}

按照 C++ 的基本規(guī)則,這段代碼的執(zhí)行過程應(yīng)該是這樣的:

  • 在createBigObject()函數(shù)內(nèi)部創(chuàng)建一個(gè)臨時(shí)的BigObject對象
  • 當(dāng)函數(shù)返回時(shí),把這個(gè)臨時(shí)對象復(fù)制一份到main()函數(shù)的myObj變量中
  • 銷毀函數(shù)內(nèi)的臨時(shí)對象

所以按道理說,這里至少會調(diào)用一次構(gòu)造函數(shù)和一次復(fù)制構(gòu)造函數(shù),對吧?

但是!如果你實(shí)際運(yùn)行這段代碼并打印出構(gòu)造和復(fù)制構(gòu)造的調(diào)用情況,你很可能會驚訝地發(fā)現(xiàn):復(fù)制構(gòu)造函數(shù)根本沒被調(diào)用!

這是為什么呢?這就是今天的主角——返回值優(yōu)化(Return Value Optimization, RVO)在默默發(fā)揮作用。

2. RVO是什么鬼?

RVO,全稱 Return Value Optimization,中文叫"返回值優(yōu)化",是一種編譯器優(yōu)化技術(shù)。簡單來說,它可以消除函數(shù)返回時(shí)的對象復(fù)制操作。

回到剛才的例子,使用 RVO 后,編譯器會直接在main()函數(shù)的myObj變量所在的內(nèi)存位置上構(gòu)造對象,而不是先在createBigObject()函數(shù)內(nèi)構(gòu)造,再復(fù)制出來。這樣就完全省去了復(fù)制的開銷!

是不是很神奇?明明我們寫的代碼邏輯上需要復(fù)制,但編譯器卻偷偷幫我們優(yōu)化掉了。這種優(yōu)化在 C++11 標(biāo)準(zhǔn)中被稱為"復(fù)制省略"(copy elision),是少數(shù)幾個(gè)允許編譯器改變程序可觀察行為的優(yōu)化之一。

二、NRVO:RVO的近親兄弟

說完了RVO,我們再來看看它的"近親兄弟"——NRVO。

NRVO  全稱是 Named Return Value Optimization,中文可以叫做"具名返回值優(yōu)化"。這名字聽起來有點(diǎn)繞,但其實(shí)很好理解:它就是針對有名字的局部變量的返回值優(yōu)化。

看下面這個(gè)例子:

BigObject createBigObject() {
    BigObject result; // 創(chuàng)建一個(gè)具名對象
    // 對result做一些處理...
    return result; // 返回這個(gè)具名對象
}

這種情況下,我們創(chuàng)建了一個(gè)名為result的局部變量,并在最后返回它。這就是 NRVO 的應(yīng)用場景。

相比之下,我們前面已經(jīng)看到了RVO的例子,它是針對返回?zé)o名臨時(shí)對象的優(yōu)化:

BigObject createBigObject() {
    // 直接返回一個(gè)臨時(shí)對象
    return BigObject();
}

雖然兩者有細(xì)微差別,但目的都是一樣的:避免不必要的對象復(fù)制,提高程序性能。

三、深入理解:RVO和NRVO如何實(shí)現(xiàn)?

好了,現(xiàn)在我們知道了 RVO 和 NRVO 是什么,但它們是如何實(shí)現(xiàn)的呢?編譯器到底在背后做了什么魔法?讓我們揭開謎底!

1. 編譯器的巧妙把戲

傳統(tǒng)情況下,當(dāng)函數(shù)返回一個(gè)對象時(shí),會經(jīng)歷這樣的過程:

  • 在函數(shù)內(nèi)創(chuàng)建一個(gè)局部對象
  • 復(fù)制這個(gè)對象到返回值位置
  • 銷毀函數(shù)內(nèi)的局部對象

但使用 RVO/NRVO 時(shí),編譯器耍了個(gè)聰明的把戲:

  • 在調(diào)用者的棧上直接分配返回值的空間
  • 將這個(gè)空間的地址偷偷傳給被調(diào)用函數(shù)
  • 被調(diào)用函數(shù)直接在這個(gè)地址上構(gòu)造對象

就這么簡單!沒有復(fù)制,沒有移動(dòng),對象直接在它最終應(yīng)該在的位置上誕生。

我們來看看這在匯編代碼中是什么樣子的,以我們前面的RVO例子為例:

BigObject createBigObject() {
    return BigObject(); // 返回一個(gè)無名臨時(shí)對象
}

int main() {
    BigObject myObj = createBigObject();
    return 0;
}

讓我們來對比一下開啟 RVO 和未開啟 RVO 時(shí)的匯編代碼差異,這樣對比會更有說服力。

未開啟RVO優(yōu)化時(shí)(使用 -fno-elide-constructors的編譯選項(xiàng)):

createBigObject:
    ; rdi包含返回值的地址
    
    ; 在返回地址構(gòu)造BigObject
    call BigObject::BigObject()  ; 調(diào)用構(gòu)造函數(shù)
    ret                           ; 返回
    
main:
    ; 為myObj分配空間
    sub rsp, 40000        ; 假設(shè)BigObject占用40000字節(jié)
    
    ; 為臨時(shí)返回值分配空間
    sub rsp, 40000        ; 再分配一塊空間存儲函數(shù)返回值
    
    ; 調(diào)用createBigObject
    lea rdi, [rsp]        ; 傳遞臨時(shí)返回值的地址
    call createBigObject
    
    ; 現(xiàn)在需要把臨時(shí)返回值復(fù)制到myObj
    lea rdi, [rsp+40000]  ; 目標(biāo)地址(myObj)
    lea rsi, [rsp]        ; 源地址(臨時(shí)返回值)
    call BigObject::BigObject(BigObject const&)  ; 調(diào)用復(fù)制構(gòu)造函數(shù)
    
    ; 釋放臨時(shí)返回值
    lea rdi, [rsp]
    call BigObject::~BigObject  ; 調(diào)用臨時(shí)對象的析構(gòu)函數(shù)
    
    add rsp, 40000        ; 釋放臨時(shí)返回值的空間
    add rsp, 40000        ; 釋放myObj的空間
    xor eax, eax          ; 返回0
    ret

開啟RVO優(yōu)化時(shí)(默認(rèn)就開啟):

createBigObject:
    ; rdi中已經(jīng)包含了目標(biāo)對象的地址
    
    ; 直接在目標(biāo)地址上構(gòu)造BigObject
    mov QWORD PTR [rdi], 0    ; 初始化部分?jǐn)?shù)據(jù)
    mov QWORD PTR [rdi+8], 0  ; 初始化更多數(shù)據(jù)
    ; ...更多初始化代碼...
    
    ; 返回(對象已經(jīng)構(gòu)造在調(diào)用者提供的內(nèi)存中)
    ret
    
main:
    ; 為myObj分配空間
    sub rsp, 40000        ; 假設(shè)BigObject占用40000字節(jié)
    
    ; 調(diào)用createBigObject,并傳遞myObj的地址作為隱藏參數(shù)
    lea rdi, [rsp]        ; 將myObj的地址加載到rdi寄存器(第一個(gè)參數(shù))
    call createBigObject
    
    ; myObj已經(jīng)構(gòu)造好了,清理并返回
    add rsp, 40000
    xor eax, eax          ; 返回0
    ret

看一眼這兩段匯編代碼,差異顯而易見。未優(yōu)化的版本明顯更復(fù)雜:它要分配兩塊內(nèi)存空間,而不是一塊;它調(diào)用了構(gòu)造函數(shù),然后又調(diào)用復(fù)制構(gòu)造函數(shù)和析構(gòu)函數(shù);它需要進(jìn)行內(nèi)存復(fù)制,還有更多的棧操作。

相比之下,RVO優(yōu)化版本簡潔明了:只分配一塊內(nèi)存空間,只調(diào)用一次構(gòu)造函數(shù),沒有復(fù)制,沒有析構(gòu),也沒有額外的棧操作。對于大對象來說,這種差異帶來的性能提升是相當(dāng)可觀的!

2. NRVO與RVO有何不同?

那 NRVO 呢?它與 RVO 在實(shí)現(xiàn)上有什么區(qū)別?

在 RVO 中,編譯器一看到return BigObject()就知道這是個(gè)臨時(shí)對象,直接在目標(biāo)位置構(gòu)造它很容易。

而 NRVO 要復(fù)雜一些。當(dāng)編譯器看到BigObject obj;時(shí),它不確定這個(gè)對象是否只用于返回。只有分析整個(gè)函數(shù)后,確認(rèn) obj 沒有被多次修改或以復(fù)雜方式使用,才能將它直接構(gòu)造在返回位置。

舉個(gè)例子:

BigObject createComplex(bool condition) {
    BigObject obj1;
    BigObject obj2;
    // ...
    if (condition) {
        obj1 = obj2;  // obj1被修改了!
        return obj1;
    }
    return obj2;
}

這種情況下,編譯器可能無法應(yīng)用NRVO,因?yàn)椋?/p>

  • 可能返回不同的對象(obj1或obj2)
  • 對象在返回前被修改了
  • 函數(shù)邏輯依賴運(yùn)行時(shí)條件

簡單來說:

  • RVO:直接明了,容易實(shí)現(xiàn),優(yōu)化成功率高
  • NRVO:需要更全面的代碼分析,實(shí)現(xiàn)更復(fù)雜

雖然原理有差異,但成功應(yīng)用后的效果是相同的:對象都直接在最終位置上構(gòu)造,完全避免了復(fù)制。

3. 來看個(gè)實(shí)際例子

讓我們用實(shí)際代碼來驗(yàn)證一下 RVO 和 NRVO 的效果:

#include <iostream>
#include <chrono>
usingnamespacestd;
usingnamespacestd::chrono;

class BigObject {
private:
    int* data; // 指針,而不是數(shù)組
public:
    BigObject() {
        data = newint[1000000]; // 在堆上分配
        for (int i = 0; i < 1000000; i++) {
            data[i] = i;
        }
        cout << "構(gòu)造函數(shù)被調(diào)用" << endl;
    }

    BigObject(const BigObject& other) {
        data = newint[1000000]; // 在堆上分配
        for (int i = 0; i < 1000000; i++) {
            data[i] = other.data[i];
        }
        cout << "復(fù)制構(gòu)造函數(shù)被調(diào)用" << endl;
    }

    ~BigObject() {
        delete[] data; // 記得釋放內(nèi)存
    }
};

// RVO示例
BigObject createWithRVO() {
    return BigObject(); // 返回臨時(shí)對象
}
// NRVO示例
BigObject createWithNRVO() {
    BigObject obj;
    return obj; // 返回具名對象
}

int main() {
    // 測試RVO
    auto start = high_resolution_clock::now();
    BigObject obj1 = createWithRVO();
    auto end = high_resolution_clock::now();
    cout << "RVO耗時(shí): " << duration_cast<microseconds>(end - start).count() << "us" << endl;

    // 測試NRVO
    start = high_resolution_clock::now();
    BigObject obj2 = createWithNRVO();
    end = high_resolution_clock::now();
    cout << "NRVO耗時(shí): " << duration_cast<microseconds>(end - start).count() << "us" << endl;

    return 0;
}

運(yùn)行這段代碼,我們可以得到明顯不同的結(jié)果,這取決于編譯器是否啟用了 RVO/NRVO 優(yōu)化。

  • 禁用RVO優(yōu)化時(shí)(使用編譯選項(xiàng):g++ -fno-elide-constructors -o run test.cpp -std=c++11):
構(gòu)造函數(shù)被調(diào)用
復(fù)制構(gòu)造函數(shù)被調(diào)用
復(fù)制構(gòu)造函數(shù)被調(diào)用
RVO耗時(shí): 14428us
構(gòu)造函數(shù)被調(diào)用
復(fù)制構(gòu)造函數(shù)被調(diào)用
復(fù)制構(gòu)造函數(shù)被調(diào)用
NRVO耗時(shí): 9674us
  • 啟用RVO優(yōu)化時(shí)(默認(rèn)選項(xiàng):g++ -o run test.cpp -std=c++11):
構(gòu)造函數(shù)被調(diào)用
RVO耗時(shí): 4413us
構(gòu)造函數(shù)被調(diào)用
NRVO耗時(shí): 4424us

看到?jīng)]?差別蠻大!

  • 禁用優(yōu)化時(shí),每個(gè)函數(shù)調(diào)用都要復(fù)制兩次對象,耗時(shí)挺長。
  • 啟用優(yōu)化后,復(fù)制構(gòu)造函數(shù)直接消失了!只需構(gòu)造一次對象,速度整整快了2-3倍多。

即使是在禁用優(yōu)化時(shí),你可能注意到 NRVO 比 RVO 稍快 —— 這可能只是測試誤差,但確實(shí)有趣。不過重點(diǎn)是:開啟優(yōu)化后,兩者性能基本一致,完全符合我們的理論分析。

這就是 RVO 和 NRVO 的威力!它們不是魔法,而是實(shí)實(shí)在在的性能提升,特別是當(dāng)你的函數(shù)需要返回大對象時(shí)。

四、什么時(shí)候會失效?RVO 和 NRVO 的限制條件

前面我們了解了 RVO 和 NRVO 這兩個(gè)強(qiáng)大的優(yōu)化技術(shù),但它們也不是萬能的。什么情況下這些優(yōu)化會失效呢?讓我們一起來看看幾種常見情況。

1. 多個(gè)返回語句指向不同對象

當(dāng)函數(shù)里有多個(gè)返回語句,并且返回的是不同的對象時(shí),編譯器就無法確定應(yīng)該為哪個(gè)對象應(yīng)用優(yōu)化:

BigObject createObject(bool condition) {
    BigObject obj1;
    BigObject obj2;
    
    if (condition) {
        return obj1;  // 返回第一個(gè)對象
    } else {
        return obj2;  // 返回第二個(gè)對象
    }
}

在這種情況下,編譯器通常無法應(yīng)用NRVO,因?yàn)樗荒艽_定是obj1還是obj2會被返回。這完全取決于運(yùn)行時(shí)的condition值。

2. 返回的對象是函數(shù)參數(shù)

如果函數(shù)返回的是一個(gè)參數(shù),編譯器通常無法應(yīng)用RVO:

BigObject returnParameter(BigObject param) {
    return param;  // 返回的是函數(shù)參數(shù)
}

這里的param已經(jīng)在調(diào)用者那里構(gòu)造好了,函數(shù)只是返回了它的一個(gè)副本。編譯器無法在調(diào)用者的棧上"預(yù)先"構(gòu)造這個(gè)對象,因?yàn)樗呀?jīng)存在了。

3. 返回的是類成員變量

當(dāng)函數(shù)返回類的成員變量時(shí),這個(gè)變量已經(jīng)作為對象的一部分存在了,編譯器通常也無法應(yīng)用RVO:

class Container {
    BigObject member;
public:
    BigObject getMember() {
        return member;  // 返回的是類成員變量
    }
};

因?yàn)閙ember的生命周期與函數(shù)調(diào)用無關(guān)(它是Container對象的一部分),編譯器無法將它直接構(gòu)造在返回值位置。

4. 復(fù)雜控制流

當(dāng)函數(shù)中有復(fù)雜的控制流(如多層嵌套的條件語句、循環(huán)、異常處理等)時(shí),編譯器可能難以分析并應(yīng)用RVO/NRVO:

BigObject complexFunction() {
    BigObject obj;
    try {
        // 一些可能拋出異常的代碼
        if (someCondition) {
            throw SomeException();
        }
    } catch (...) {
        return obj;  // 在異常處理中返回
    }
    // 更多復(fù)雜控制流...
    return obj;
}

復(fù)雜的控制流會使編譯器難以確定返回路徑和返回對象的情況,從而影響優(yōu)化。

5. 如何確認(rèn)優(yōu)化是否生效?

想知道你的代碼是否觸發(fā)了 RVO/NRVO 優(yōu)化?最簡單的方法就是添加打印語句到構(gòu)造函數(shù)和復(fù)制構(gòu)造函數(shù)中,然后運(yùn)行看看:

class Tracer {
public:
    Tracer() { cout << "構(gòu)造函數(shù)" << endl; }
    Tracer(const Tracer&) { cout << "復(fù)制構(gòu)造函數(shù)" << endl; }
    ~Tracer() { cout << "析構(gòu)函數(shù)" << endl; }
};

Tracer getTracer() {
    return Tracer();
}

int main() {
    Tracer t = getTracer();
    return 0;
}

如果只看到"構(gòu)造函數(shù)"和"析構(gòu)函數(shù)"的輸出,沒有看到"復(fù)制構(gòu)造函數(shù)",那么RVO就成功了!

6. 小貼士:如何提高優(yōu)化成功率?

  • 盡量返回臨時(shí)對象(RVO 比 NRVO 更容易被應(yīng)用)
  • 一個(gè)函數(shù)只返回一個(gè)對象(避免多個(gè)返回語句返回不同對象)

記住這些小技巧,你的代碼就能更好地利用這些強(qiáng)大的優(yōu)化功能了!

五、C++17:強(qiáng)制的復(fù)制省略

前面我們講了這么多 RVO 和 NRVO 的好處,但你知道嗎?在 C++17 之前,這些優(yōu)化其實(shí)只是編譯器的"好心",并不是語言標(biāo)準(zhǔn)要求必須做的事情!

1. 從"可選"到"必選"

在 C++17 之前,編譯器可以選擇是否應(yīng)用 RVO 和 NRVO 優(yōu)化。也就是說,即使你的代碼寫得再完美,滿足了所有優(yōu)化條件,編譯器也可以說:"不,我就是不想優(yōu)化。"當(dāng)然,實(shí)際上大多數(shù)編譯器都會盡可能地進(jìn)行這些優(yōu)化,因?yàn)樗鼈兇_實(shí)能帶來很大的性能提升。

但從 C++17 開始,對于 RVO 這種情況(即返回臨時(shí)對象),標(biāo)準(zhǔn)明確要求 編譯器必須省略復(fù)制/移動(dòng)操作。這就是所謂的"強(qiáng)制的復(fù)制省略"(mandatory copy elision)。

2. 這意味著什么?

用大白話說,就是 C++17 把"情分"變成了"本分"。編譯器不再能偷懶,必須為臨時(shí)對象的返回做優(yōu)化。

最有趣的變化是,以下代碼在 C++17 之前可能無法編譯,但在 C++17 中一定能編譯并正常工作:

class NonCopyable {
public:
    NonCopyable() = default;
    // 禁止復(fù)制
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
    // 禁止移動(dòng)
    NonCopyable(NonCopyable&&) = delete;
    NonCopyable& operator=(NonCopyable&&) = delete;
};

NonCopyable createNonCopyable() {
    return NonCopyable(); // C++17前可能報(bào)錯(cuò),C++17一定沒問題
}

int main() {
    NonCopyable obj = createNonCopyable(); // 同上
    return 0;
}

這段代碼看起來很矛盾:我們創(chuàng)建了一個(gè)既不能復(fù)制也不能移動(dòng)的類,然后卻試圖返回它的一個(gè)臨時(shí)對象。按理說,既然不能復(fù)制也不能移動(dòng),這個(gè)對象就不應(yīng)該能夠從函數(shù)返回到調(diào)用者那里。

但在 C++17 中,這段代碼是完全合法的!因?yàn)闃?biāo)準(zhǔn)要求在這種情況下,編譯器必須直接在main函數(shù)的obj變量的內(nèi)存位置上構(gòu)造這個(gè)NonCopyable對象,完全跳過任何復(fù)制或移動(dòng)操作。

3. 為什么這個(gè)變化很重要?

  • 代碼行為更可預(yù)測:無論使用哪個(gè)編譯器,優(yōu)化效果都是一樣的
  • 使用不可復(fù)制類型更靈活:如上例所示,即使類禁止了復(fù)制和移動(dòng),也能輕松返回
  • 性能保證更強(qiáng):標(biāo)準(zhǔn)保證臨時(shí)對象返回時(shí)不會有額外開銷

不過要注意,NRVO(返回具名對象)在 C++17 中仍然是可選的優(yōu)化,編譯器可以自行決定是否應(yīng)用。只有RVO(返回臨時(shí)對象)是強(qiáng)制的。

所以,如果你希望代碼在所有 C++17 編譯器上都能獲得優(yōu)化,返回臨時(shí)對象會是更安全的選擇:

// 在所有C++17編譯器上都會被優(yōu)化
BigObject getBigObject() {
    return BigObject();  // 返回臨時(shí)對象,強(qiáng)制優(yōu)化
}

// 可能會被優(yōu)化,取決于編譯器
BigObject getBigObject2() {
    BigObject obj;
    return obj;  // 返回具名對象,優(yōu)化是可選的
}

六、實(shí)戰(zhàn)應(yīng)用:如何充分利用 RVO 和 NRVO

好了,了解了這么多理論知識,現(xiàn)在該談?wù)勗趺丛谌粘>幋a中實(shí)際運(yùn)用這些技巧了!下面我們就來看看如何寫出能夠充分利用RVO和NRVO的代碼。

1. 盡可能使用返回值,而不是輸出參數(shù)

在C++中,有兩種常見的方式向調(diào)用者傳遞新創(chuàng)建的對象:通過返回值或通過輸出參數(shù)。

// 方式1:使用輸出參數(shù)
void createBigObject(BigObject& outObj) {
    // 初始化outObj...
    outObj.setData(42);
}

// 方式2:使用返回值
BigObject createBigObject() {
    BigObject obj;
    obj.setData(42);
    return obj;
}

哪種更好? 毫無疑問是第二種!

使用返回值不僅代碼更加清晰(表明函數(shù)的目的是"創(chuàng)建"和"返回"某物),而且能夠利用 RVO/NRVO 優(yōu)化性能。而第一種方式無法利用這些優(yōu)化。

在現(xiàn)代 C++ 中,你完全不需要擔(dān)心返回大對象會影響性能。相反,你應(yīng)該擁抱返回值風(fēng)格!

2. 在函數(shù)末尾直接返回局部變量

看看下面兩種寫法:

// 不好的寫法
BigObject createBigObject() {
    BigObject result;
    // 初始化result...
    BigObject temp = result; // 多余的復(fù)制
    return temp;
}

// 更好的寫法
BigObject createBigObject() {
    BigObject result;
    // 初始化result...
    return result; // 直接返回,可能觸發(fā)NRVO
}

第一種寫法中,我們創(chuàng)建了一個(gè)多余的temp對象,并做了一次不必要的復(fù)制。這不僅增加了代碼的復(fù)雜性,還破壞了NRVO優(yōu)化的條件。

第二種寫法簡單直接,而且更有可能觸發(fā) NRVO 優(yōu)化。記住:直接返回你想要返回的局部變量,不要繞彎子!

3. 小心使用std::move

初學(xué) C++11 的同學(xué)可能會有一個(gè)常見誤區(qū):認(rèn)為給所有返回的對象都加上std::move會提高效率。實(shí)際上,這通常是一個(gè)巨大的錯(cuò)誤!

// 錯(cuò)誤示范!會破壞RVO/NRVO
BigObject createBigObject() {
    BigObject obj;
    // ...
    return std::move(obj); // ? 不要這樣做!可能會阻止NRVO!
}

// 正確做法:直接返回局部變量
BigObject createBigObject() {
    BigObject obj;
    // ...
    return obj; // ? 讓編譯器做優(yōu)化
}

為什么std::move反而會降低性能?因?yàn)樗嬖V編譯器:"我要移動(dòng)這個(gè)對象",這就阻止了編譯器直接在目標(biāo)位置構(gòu)造對象的優(yōu)化路徑。記住:在返回局部變量時(shí),不要使用 std::move!

唯一應(yīng)該使用 std::move的情況是當(dāng)你確定 RVO/NRVO 無法應(yīng)用,而你又想避免復(fù)制的時(shí)候:

BigObject createBigObject(bool condition) {
    BigObject obj1;
    BigObject obj2;
    
    // 多返回路徑情況下,NRVO可能失效
    // 此時(shí)使用移動(dòng)語義作為"備胎"
    if (condition) {
        return std::move(obj1); // 這里使用move是合理的
    } else {
        return std::move(obj2); // 這里也是
    }
}

4. 使用右值引用和移動(dòng)構(gòu)造函數(shù)作為后備

從 C++11 開始,我們有了移動(dòng)語義。即使在 RVO/NRVO 無法應(yīng)用的場景,移動(dòng)語義也能提供比復(fù)制更高效的方案:

class BigObject {
private:
    vector<int> data; // 可能很大的數(shù)據(jù)
public:
    // 移動(dòng)構(gòu)造函數(shù)
    BigObject(BigObject&& other) noexcept
        : data(std::move(other.data)) { // 只是轉(zhuǎn)移指針,不復(fù)制數(shù)據(jù)
        cout << "移動(dòng)構(gòu)造" << endl;
    }
    
    // 常規(guī)復(fù)制構(gòu)造函數(shù)
    BigObject(const BigObject& other)
        : data(other.data) { // 復(fù)制所有數(shù)據(jù),可能很慢
        cout << "復(fù)制構(gòu)造" << endl;
    }
};

通過實(shí)現(xiàn)移動(dòng)構(gòu)造函數(shù),即使在RVO/NRVO失效的情況下,編譯器也會選擇調(diào)用移動(dòng)構(gòu)造而不是復(fù)制構(gòu)造,這能顯著提升性能。

5. 實(shí)戰(zhàn)小貼士總結(jié)

  • 優(yōu)先使用返回值風(fēng)格,而不是輸出參數(shù)
  • 直接返回局部變量,不要?jiǎng)?chuàng)建臨時(shí)副本
  • 不要對返回的局部變量使用std::move,除非你確定 RVO/NRVO 無法應(yīng)用
  • 實(shí)現(xiàn)移動(dòng)構(gòu)造函數(shù)作為后備優(yōu)化
  • 閱讀編譯器生成的匯編代碼(如果你想確認(rèn)優(yōu)化是否生效)

掌握了這些技巧,你就能寫出既清晰又高效的C++代碼,充分利用編譯器為你提供的這些免費(fèi)的性能優(yōu)化!

七、實(shí)際測量:驗(yàn)證優(yōu)化效果

理論講得再多,不如親自驗(yàn)證一下。下面是一個(gè)更全面的基準(zhǔn)測試代碼,你可以用它來測量不同情況下的性能差異:

#include <iostream>
#include <chrono>
#include <vector>
#include <string>
usingnamespace std;
usingnamespace std::chrono;

// 一個(gè)足夠大的類,使性能差異明顯
class BigObject {
private:
    vector<int> data;
    string name;
public:
    BigObject(size_t size = 1000000) : data(size) {
        for (size_t i = 0; i < size; i++) {
            data[i] = static_cast<int>(i);
        }
        name = "BigObject";
    }
    
    BigObject(const BigObject& other) : data(other.data), name(other.name) {
        cout << "復(fù)制構(gòu)造: 復(fù)制了 " << data.size() << " 個(gè)元素" << endl;
    }
    
    BigObject(BigObject&& other) noexcept : 
        data(std::move(other.data)), name(std::move(other.name)) {
        cout << "移動(dòng)構(gòu)造被調(diào)用" << endl;
    }
    
    BigObject& operator=(const BigObject& other) {
        if (this != &other) {
            data = other.data;
            name = other.name;
            cout << "復(fù)制賦值: 復(fù)制了 " << data.size() << " 個(gè)元素" << endl;
        }
        return *this;
    }
    
    BigObject& operator=(BigObject&& other) noexcept {
        if (this != &other) {
            data = std::move(other.data);
            name = std::move(other.name);
            cout << "移動(dòng)賦值被調(diào)用" << endl;
        }
        return *this;
    }
    
    ~BigObject() {
        // 析構(gòu)函數(shù)
    }
    
    size_t getSize() const { return data.size(); }
};

// 使用RVO(返回臨時(shí)對象)
BigObject createWithRVO(size_t size) {
    return BigObject(size);
}

// 使用NRVO(返回具名對象)
BigObject createWithNRVO(size_t size) {
    BigObject obj(size);
    return obj;
}

// 故意阻止RVO/NRVO
BigObject createWithDisabledOptimization(size_t size, bool flag) {
    BigObject obj1(size);
    BigObject obj2(size);
    
    if (flag) {
        return obj1;
    } else {
        return obj2;
    }
}

// 使用移動(dòng)語義
BigObject createWithMove(size_t size, bool flag) {
    BigObject obj1(size);
    BigObject obj2(size);
    
    if (flag) {
        return std::move(obj1);
    } else {
        return std::move(obj2);
    }
}

// 運(yùn)行基準(zhǔn)測試
template<typename Func>
long long runBenchmark(Func func, int iterations) {
    auto start = high_resolution_clock::now();
    
    for (int i = 0; i < iterations; i++) {
        BigObject obj = func();
        // 做一些操作以防止編譯器過度優(yōu)化
        if (obj.getSize() < 0) cout << "不可能發(fā)生" << endl;
    }
    
    auto end = high_resolution_clock::now();
    return duration_cast<milliseconds>(end - start).count();
}

int main() {
    constint iterations = 10;
    constsize_t objSize = 1000000;
    
    cout << "測試RVO優(yōu)化..." << endl;
    auto rvoTime = runBenchmark([objSize]() { 
        return createWithRVO(objSize); 
    }, iterations);
    
    cout << "\n測試NRVO優(yōu)化..." << endl;
    auto nrvoTime = runBenchmark([objSize]() { 
        return createWithNRVO(objSize); 
    }, iterations);
    
    cout << "\n測試無優(yōu)化情況..." << endl;
    auto noOptTime = runBenchmark([objSize]() { 
        return createWithDisabledOptimization(objSize, rand() % 2); 
    }, iterations);
    
    cout << "\n測試移動(dòng)語義..." << endl;
    auto moveTime = runBenchmark([objSize]() { 
        return createWithMove(objSize, rand() % 2); 
    }, iterations);
    
    cout << "\n性能比較:" << endl;
    cout << "RVO: " << rvoTime << "ms" << endl;
    cout << "NRVO: " << nrvoTime << "ms" << endl;
    cout << "無優(yōu)化: " << noOptTime << "ms" << endl;
    cout << "移動(dòng)語義: " << moveTime << "ms" << endl;
    
    return0;
}

在 Visual Studio 2022 上的測試結(jié)果:

測試RVO優(yōu)化...

測試NRVO優(yōu)化...

測試無優(yōu)化情況...
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用

測試移動(dòng)語義...
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用
移動(dòng)構(gòu)造被調(diào)用

性能比較:
RVO: 127ms
NRVO: 118ms
無優(yōu)化: 241ms
移動(dòng)語義: 243ms

從結(jié)果可以看出:

  • RVO 和 NRVO 的性能幾乎相同,都非常優(yōu)秀
  • 有趣的是,"無優(yōu)化情況"和"顯式使用移動(dòng)語義"的性能也幾乎相同
  • 最令人驚訝的是,即使在"無優(yōu)化情況"下,也調(diào)用了移動(dòng)構(gòu)造函數(shù),而不是復(fù)制構(gòu)造函數(shù)!

1. 編譯器和平臺的影響

不過,值得注意的是,測試結(jié)果會受到編譯器、編譯選項(xiàng)和平臺的影響。我是在Visual Studio 2022上進(jìn)行的測試,發(fā)現(xiàn)了一些有趣的現(xiàn)象:

關(guān)于移動(dòng)構(gòu)造函數(shù)的重要發(fā)現(xiàn):

(1) 如果注釋掉BigObject類的移動(dòng)構(gòu)造函數(shù),測試結(jié)果會有顯著變化:

  • "無優(yōu)化情況"和"移動(dòng)語義"測試都會調(diào)用復(fù)制構(gòu)造函數(shù)
  • 兩者的性能幾乎完全相同

(2) 反之,如果定義了移動(dòng)構(gòu)造函數(shù):

  • 兩種情況都會調(diào)用移動(dòng)構(gòu)造函數(shù)
  • 性能同樣會非常接近

這個(gè)現(xiàn)象解釋了為什么在某些測試環(huán)境中,"無優(yōu)化"和"移動(dòng)語義"的性能差異不明顯。它說明:

  • C++編譯器非常智能:即使在無法應(yīng)用RVO/NRVO的情況下,如果有移動(dòng)構(gòu)造函數(shù)可用,現(xiàn)代編譯器會自動(dòng)選擇移動(dòng)而非復(fù)制
  • 添加std::move并不總是必要的:在多返回路徑的情況下,即使不顯式使用std::move,編譯器也可能自動(dòng)應(yīng)用移動(dòng)語義
  • 但定義移動(dòng)構(gòu)造函數(shù)很重要:要讓編譯器能夠選擇移動(dòng)而不是復(fù)制,必須定義移動(dòng)構(gòu)造函數(shù)

這個(gè)測試提醒我們:在進(jìn)行性能優(yōu)化時(shí),務(wù)必在自己的實(shí)際環(huán)境中測試,因?yàn)椴煌幾g器和不同編譯選項(xiàng)可能導(dǎo)致不同的優(yōu)化結(jié)果。

這也進(jìn)一步強(qiáng)調(diào)了 C++ 標(biāo)準(zhǔn)庫中"Rule of Five"(五法則)的重要性:如果你定義了任何一個(gè)復(fù)制構(gòu)造、復(fù)制賦值、移動(dòng)構(gòu)造、移動(dòng)賦值或析構(gòu)函數(shù),通常應(yīng)該考慮定義所有五個(gè)函數(shù),以確保類的行為一致且性能最優(yōu)。

八、總結(jié)與最佳實(shí)踐

講了這么多,是時(shí)候把重點(diǎn)內(nèi)容簡單總結(jié)一下了!

1. RVO與NRVO:不再是"大對象別返回"

以前我們常被告誡:"C++返回大對象很慢,盡量用指針或引用傳遞"?,F(xiàn)在看來,這個(gè)說法已經(jīng)過時(shí)啦!

有了RVO和NRVO這兩個(gè)強(qiáng)大的優(yōu)化技術(shù),返回對象不再是性能瓶頸:

  • RVO處理臨時(shí)對象返回:return BigObject();
  • NRVO處理局部變量返回:BigObject obj; return obj;
  • C++17讓RVO成為必選項(xiàng):編譯器必須優(yōu)化臨時(shí)對象返回
  • 移動(dòng)語義是不錯(cuò)的備胎:當(dāng)RVO/NRVO失效時(shí)的保底方案

最佳編碼實(shí)踐包括:直接返回對象而非用輸出參數(shù)、直接返回局部變量不做額外復(fù)制、不對返回局部變量使用std::move、實(shí)現(xiàn)移動(dòng)構(gòu)造函數(shù)作為后備、使用現(xiàn)代編譯器并開啟優(yōu)化等。

2. 別被"過早優(yōu)化"困住

有句名言:"過早優(yōu)化是萬惡之源"。但利用 RVO/NRVO 并非過早優(yōu)化 — 這些寫法本身就是現(xiàn)代C++的自然表達(dá),代碼更清晰,還能獲得更好性能,何樂而不為?

九、結(jié)語:不只是一個(gè)優(yōu)化技巧

RVO 和 NRVO 代表了 C++ 的一個(gè)重要理念:零開銷抽象。通過它們,我們可以寫出既清晰又高效的代碼。這正是C++的魅力所在!

希望這篇文章能幫你更好理解和利用這兩個(gè)強(qiáng)大的優(yōu)化技術(shù)。C++的優(yōu)化技巧還有很多,后續(xù)我會繼續(xù)分享更多實(shí)用的 C++ 性能優(yōu)化知識。

責(zé)任編輯:趙寧寧 來源: 跟著小康學(xué)編程
相關(guān)推薦

2023-11-15 17:58:58

C++代碼

2010-01-14 15:29:44

C++編譯器

2010-10-20 13:43:37

C++編譯器

2010-01-18 10:34:21

C++編譯器

2010-01-21 09:11:38

C++編譯器

2010-01-27 16:39:48

C++編譯器

2010-01-18 10:28:15

C++編譯器

2010-01-12 16:42:59

C++編譯器

2010-01-20 11:15:38

CC++編譯器

2010-02-03 13:14:03

C++編譯器命令

2010-01-27 14:48:55

優(yōu)秀C++編譯器

2017-09-25 08:36:01

CUDAPython編譯器

2010-01-19 13:01:32

C++數(shù)據(jù)類型

2010-01-21 09:26:53

CC++編譯器

2010-01-08 16:00:46

C++編譯器

2009-01-12 10:16:11

Visual C++編譯器選項(xiàng)設(shè)置

2010-01-14 14:55:14

C++編譯器

2013-03-18 09:42:47

C++C++ 11

2023-12-07 19:19:21

C++模板代碼

2015-03-23 10:04:43

c++編譯器c++實(shí)現(xiàn)原理總結(jié)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號