C/C++關鍵字之restrict


更多精彩內容,請關注微信公眾號:后端技術小屋

C/C++關鍵字之restrict

在C語言中,restrict關鍵字用於修飾指針(C99標准)。通過加上restrict關鍵字,編程者可提示編譯器:在該指針的生命周期內,其指向的對象不會被別的指針所引用。

需要注意的是,在C++中,並無明確統一的標准支持restrict關鍵字。但是很多編譯器實現了功能相同的關鍵字,例如gcc和clang中的__restrict關鍵字。

那么restrict關鍵字能給程序的實際運行帶來哪些好處呢?下面舉例說明

int add1(int* a, int* b)
{
    *a = 10;
    *b = 12;
    return *a + *b;
}

大家猜猜add1函數的返回值是多少?是10 + 12 = 22嗎?

答案是不一定。在指針a和b的地址不同時,返回22沒有問題。但是當指針a與b指向的是同一個int對象時,該對象先被賦值為10,后被賦值為12,因此a和b都返回12,因此add1函數最終返回24

使用-O3優化, add1對應的匯編代碼如下。可以看到,在計算返回值時,為了得到*a的值訪問了1次內存,而不管在何種條件下(a == b or a != b),*b的值都是12。因此聰明的編譯器將*a的值載入eax寄存器后,直接加上立即數12,而無需再訪問內存獲取*b的值。在無法確定指針a和b是否相同的情況下,編譯器只能幫你到這里了.

0000000000400a10 <_Z4add1PiS_>:
  400a10:   c7 07 0a 00 00 00       movl   $0xa,(%rdi) ; *a = 10
  400a16:   c7 06 0c 00 00 00       movl   $0xc,(%rsi) ; *b = 10
  400a1c:   8b 07                   mov    (%rdi),%eax ; 結果 = *a
  400a1e:   83 c0 0c                add    $0xc,%eax   ; 結果 += 12 
  400a21:   c3                      retq  

但是如果加上了restrict關鍵字,情況便大不相同。C/C++和經過-O3優化的匯編代碼如下。通過restrict關鍵字,編譯器依然確認指針a和b不可能指向同一個內存地址,因此在求*a + *b時,無需訪問內存,因為*a必然等於立即數10,*b必然等於立即數12。

int add2(int* __restrict  a, int* __restrict b) 
{
    *a = 10;
    *b = 12;
    return *a + *b ;
}
0000000000400a30 <_Z4add2PiS_>:
  400a30:   c7 07 0a 00 00 00       movl   $0xa,(%rdi) ; *a = 10
  400a36:   b8 16 00 00 00          mov    $0x16,%eax  ; 結果 = 22
  400a3b:   c7 06 0c 00 00 00       movl   $0xc,(%rsi) ; *b = 12
  400a41:   c3                      retq   

通過無restrict和有restrict兩種情況下的匯編指令可看到,后者比前者少訪問一次內存,且少執行一條指令。因此我們預期有restrict的版本能夠獲得可觀的性能提升:

int main() 
{
    int * a = new int;
    int * b = new int;


    {
        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
        for (size_t i=0; i<100000000; i++)
            add1(a, b);
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        std::cout << "Time difference = " << std::chrono::duration_cast<std::chrono::nanoseconds> (end - begin).count() << "[ns]" << std::endl;
    }


 {
        std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
        for (size_t i=0; i<100000000; i++)
            add2(a, b);
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        std::cout << "Time difference = " << std::chrono::duration_cast<std::chrono::nanoseconds> (end - begin).count() << "[ns]" << std::endl;
    }
    return 0;
}

以上代碼分別執行add1add2函數各一億次,計算二者耗時,結果如下。

Time difference = 146[ns]
Time difference = 56[ns]

在這個case里,使用restrict能夠獲得2+倍的性能提升!注意使用restrict的時候,編程者必須確保不會出現pointer aliasing, 即同一塊內存無法通過兩個或以上的指針變量名訪問。不滿足這個條件而強行指定restrict, 將會出現undefined behavior

PS: 此篇文章有感於clickhouse近期一個與restrict有關的性能優化(https://github.com/ClickHouse/ClickHouse/pull/19946),只是因為在聚合相關的函數中加上restrict關鍵字,便能使聚合性能提升1.6倍!所以對於我輩碼農來說,多了解一些底層原理永遠不虧,說不定哪天你就用上了~

推薦閱讀

更多精彩內容,請掃碼關注微信公眾號:后端技術小屋。如果覺得文章對你有幫助的話,請多多分享、轉發、在看。
二維碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM