天天看點

5.15 vs2019 靜态編譯_淺析同一段C++代碼在Win X64, X86,MAC,Android ARM64平台編譯器優化之美...

背景:定位一些Crash崩潰時,由于缺少更多資訊,可能需要從反彙編的靜态代碼段推測對應的C++代碼,并結合寄存器值分析出具體原因。對于Release釋出版,由于編譯器的強行函數内聯和生成指令優化,會出現反彙編代碼和C++源碼差別較大,加大我們從彙編代碼反推C++難度,一但我們分析清楚優化點,可以很欣賞編譯器優化之美。

本文是從ARM64平台的一次crash反彙編分析經曆出發,發現編譯器能在我們根本沒寫判斷的情況下,自動增加條件判斷,啟用128位寄存器的NENO指令進行加速,這是我以前在PC平台從未見過的優化,驚歎之佘,忍不住在我手上僅有的多平台做進一步分析,寫一段簡單C++代碼,同樣的代碼在Windows下用VS2019選X64,X86,MAC家的XCode C++ 64位 ,Android Studio的ARM64,這四個平台都用Release版本生成,然後用IDA進行靜态分析對比,體驗一下編譯器的強大之處。

我簡單寫了一個測試樣例,C++字元串,加上測試,全部隻有60行。為了簡化,沒有專門的記憶體配置設定器,也沒有引入一些疊代器,萃取機制,為了簡化也有直接用strlen()取長度等不符合模闆泛型程式設計等,但這些不影響分析。無論在那個平台下,都用同樣的代碼,都會重點分析TestMyString()函數和生成以及差别最大的TCopy()函數。

template<typename T>
T* TCopy(const T* pStart, const T* pFinish, T* pDst)
{
    for (; pStart != pFinish; ++pDst, ++pStart)
    {
        *pDst = *pStart;
    }
    return pDst;
};

template<typename T>
struct TString
{
    ~TString()
    {
        if (_StartPtr) delete _StartPtr;
    }
    TString(const T* pcStr)
    {
        Assign(pcStr, pcStr + strlen(pcStr));
    }
    TString<T>& operator = (const TString<T>& rkRight)
    {
        if (&rkRight != this)
        {
            Assign(rkRight._StartPtr, rkRight._FinishPtr);
        }
        return *this;
    }
    void Assign(const T* pStart, const T* pFinish)
    {
        if (pStart == pFinish || nullptr == pStart)
        {
            _FinishPtr = _StartPtr;
            return;
        }
        int iLen = pFinish - pStart;
        int iSelfSize = _FinishPtr - _StartPtr;
        if (iSelfSize < iLen)
        {
            if (_StartPtr) delete _StartPtr;
            _StartPtr = new char[iLen + 1];
            _EndOfStrore = _StartPtr + iLen;
        }

        _FinishPtr = TCopy<T>(pStart, pFinish, _StartPtr);
        *((char*)_FinishPtr) = '0';
    }
    T* _StartPtr = nullptr;
    T* _FinishPtr = nullptr;
    T* _EndOfStrore = nullptr;
};
using MyString = TString<char>;

void TestMyString()
{
    MyString s1("HelloWrold");
    MyString s2("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ");
    s1 = s2;
}
           

windows X64平台:

我們先用VS2019在main函數調用TestMyString(),然後生成windows的X64平台可執行檔案,選Release,VS預設速度最快優化,,用IDA反編譯工具打開生成的exe,跳到main代碼段。好家夥,TestMyString()不見了,直接優化内聯展開,省去call,ret開銷,且TestMyString裡面子函數調用,也全部優化掉,比如

TString<T>& operator = (const TString<T>& rkRight);
void Assign(const T* pStart, const T* pFinish);
T* TCopy(const T* pStart, const T* pFinish, T* pDst);
           

變成内聯,一股腦的全放到main裡面了,這也是最常見的優化。

5.15 vs2019 靜态編譯_淺析同一段C++代碼在Win X64, X86,MAC,Android ARM64平台編譯器優化之美...

我們重點分析一下:全部彙編代碼較長,我們選取部分,也是優化有差别的部分,C++中s1 = s2;這句的反彙編。這本應該調用TString的指派構造函數,隻是優化後全部放在一起了,為了對彙編更好的了解,我對每行都進行注釋解析,你隻要一行一行看看注釋就可以了。如果想深入,可以邊看C++代碼,邊看注釋,體驗對應關系,感受編譯器的優化。

.text:
           

X64總結:非常強的調用優化,除了new,delete系統函數,減少全部子函數的call,ret開銷,其中内聯TCopy(const T* pStart, const T* pFinish, T* pDst)代碼區塊,三個參數中pFinish算是略去(利用很早rbx就儲存了),沒有照本宣科。也能知道T是POD類型,沒有單獨的指派構造,也是比較巧秒的。

windows X86平台:

操作同X64,,用IDA反編譯工具打開生成的exe,一看哦呵,這優化有點弱啊,雖然代碼比較簡單,但不像x64代碼,所有函數一股腦優化到main中,win32函數跳轉都沒有省去。我又仔細對比了VS工程C++優化頁籤,發現win64對于omit-frame-pointer是空的,查所有參數也沒有标明“/Oy-”或者“Oy”;x86預設是否的,我手動打開吧,不然更比不過x64了。

5.15 vs2019 靜态編譯_淺析同一段C++代碼在Win X64, X86,MAC,Android ARM64平台編譯器優化之美...

啟用omit-frame-pointe,重新編譯生成。用IDA打開,首先發現x86代碼雖然大部分也直接優化到main中,但一些函數調用還是沒有優化掉,比如TString<char>::Assign(char const *,char const *),其函數裡生成了很多條的指令,另外就是和C++寫法非常接近,幾乎一行對一行,很容易反彙編。

5.15 vs2019 靜态編譯_淺析同一段C++代碼在Win X64, X86,MAC,Android ARM64平台編譯器優化之美...

受限于篇幅,加上和X64代碼非常的像,我們隻分析最核心的,和後面ARM平台差别比較大的一部分彙編,下面彙編代碼對應TString<char>::Assign(char const *,char const *)部分。

.text:
           

X86總結:調用優化還算可以,除了new,delete系統函數,隻有TString<char>::Assign(char const *,char const *)一個函數的call,ret開銷,在和x64的指令具體對比中,優化度明顯不如x64,過于像C++一行一行的翻譯,且引如較多棧平衡,關鍵的拷貝也不夠精練。

MAC平台:

MAC平台用XCode生成64位的Release可執行程式,工程全部預設值,沒有開單獨的優化。發現和X86差不多的水準,函數調用開銷沒有怎麼省,該call還是call,而且有更多的棧保護與平衡,甚至弱于x86,雖是64位,但一點也沒有像X64那麼激進。

5.15 vs2019 靜态編譯_淺析同一段C++代碼在Win X64, X86,MAC,Android ARM64平台編譯器優化之美...

由于MAC下生成執行程式的反彙編和x86差不多代碼,再注釋也是重複的,我們就不一行一行的分析了,有興趣的可以看IDA。不過發現這裡有個亮點,就是MAC啟用SSE指令集,使用XMM的128位寄存器,屬于SIMD,還是可以的。摘錄一下這段代碼,并注釋,來自TestMyString()函數。

__text:
           

總結:函數調用方面比X86還保守點,沒有太多亮點,但居然自己動使用了SSE指令集,128位的XMM0直接清零兩個指針,SIMD操作,也是不單手動開其它可選優化時的亮點。

Android的ARM64平台:

采用Android Studio,選擇Native C++工程,将前面的C++代碼放到cpp,選擇Release版本,Arm64平台,編譯生成.so運作庫二進制代碼。為了這個我專門IDA動态反調試一把Release的APK,兩個字,舒服。不過我也是剛接觸IDA和Android開發,也不太熟悉,是以才驚歎它的優化,可能後面就會麻木。

5.15 vs2019 靜态編譯_淺析同一段C++代碼在Win X64, X86,MAC,Android ARM64平台編譯器優化之美...

這邊我就靜态分析了,總的看起來非常強的調用優化,除了new,delete系統函數,減少全部子函數的調用開銷全部優化掉,也是一股腦全部放到一起,加大反推C++難度,和X64差不多水準,但細節沒有x64秒。

下面說這裡面有個非常強的優化,那就是啟用NENO指令,由于有前面三個平台那麼多分析,特别通用的地方就不再贅述,下面對部分彙編代碼進行分析,實際是T* TCopy(const T* pStart, const T* pFinish, T* pDst)的代碼,這個優化在拷大字元串,還是真香,尤其真機Release調試一下,妙。我也用Android Studio生成Deubg版本,實機也調試過,發現沒有這段NENO優化。

NEON 技術可加速多媒體和信号處理算法(如視訊編碼/解碼、2D/3D 圖形、遊戲、音頻和語音處理、圖像處理技術、電話和聲音合成),其性能至少為ARMv5 性能的3倍,為 ARMv6 SIMD性能的2倍。本文是用了128位的Q0,Q1,由于有前面三個平台那麼多分析,特别通用的地方就不再贅述,下面對部分彙編代碼進行分析。

.text:
           

總結:和X64差不多一樣的優化水準,還引用NENO指令加速,一次處理256bit資料,自動增加和32條件判斷,第一次見,太強了,秒秒秒!

綜述:這個簡單的測試C++樣本,ARM64編譯表現最強,優化綜合也是最強的,啟用NENO指令,自動增加條件語句,一次處理32個字元,完全吊打的級别。如果沒有NENO指令,Windnows下X64可以說優化最美最強的。剩下MAC和X86都差不多,不過MAC還是用了一點點SSE,采用SIMD,至少128bit操作吧,還算亮點吧。兩者基本都是照C++一行一行翻譯,中規中矩。

注:本文原創,不足之處,歡迎點評。編譯器優化和開發軟體相關也和目标平台有關,同樣代碼,不同版本的開發軟體生成代碼也不同,同一平台,同一軟體,編譯選項也有非常大的關系,本文均是采用Release版的預設設定,除了X86預設下太拉跨,手動開了omit-frame-pointer。這裡不是說那個編譯器好那個差,隻是從彙編層面對比一下不同平台C++生成二進制代碼的美妙之處。