奮戰一週,終於基本搞定了跨平臺內聯彙編的選擇和其他各種問題.分享一下

    其實本來我的需求挺簡單的,我甚至不能說自己是個程序員,各種類庫,API什麼的我也只是簡單瞭解過boost,ZThread之類常用的,連怎麼在windows裏用MFC畫個窗體都不知道....我主要是個搞算法的人,這幾年接觸最多的是matlab...其次纔是彙編和C++.

    介於工作中用到各種數學方面的方法,以及matlab與C++結合時非常噁心的效率和鏈接方式,以及我對彙編還算略微熟悉,我決定寫一套跨平臺比較適合我自己的數學庫.是,有人說這樣的庫很多,優秀的也很多,但我還是想自己實現一個,一方面是我這人比較喜歡鑽牛角尖,另一方面大多數學類庫要麼是科學計算多一些.要麼偏娛樂性,要麼精度低,要麼算的慢.對我這專業都不合適.總是有點彆扭的感覺.

再於是,介於我對彙編有點小研究,我決定使用匯編完成計算部分,用其他一些C++特性完成內存的分配,智能指針等等功能.我這領域的數學計算大致是:---準備工作,算半天,結束工作.準備和結束多慢都能接受,但中間的計算過程需要非常快速.這也是我打算使用匯編+ C++ 來完成的目的所在.各盡其職.


    廢話說完了,來講講我的經歷... 彙編和C++混合嘛,就3條路,要麼是inline彙編,要麼是彙編編個C函數在C++裏用extern "C"把函數包進去.再或者用編譯器提供的Intrinsics來進行宏彙編.這三條路各有各的好處和壞處.


    首先說說Intrinsics,這個東西之前我用了一段時間,其實是比較方便的,可以快速開發帶有SSE或者AVX指令集算法程序.但是再深入一點的時候,發現這玩意能幹的事情少了點.別的先不說,SSE指令相關的宏彙編並沒有包含所有SSE指令,AVX就更不用說了,而且VC的編譯器居然只支持到SSE2指令集,還不全.而且在實際使用中我發現這個東西的效率並不是=彙編效率.是,語句上看其實和彙編是一對一的,但是手寫彙編還有一大好處是隨意操作寄存器,有些方法的邏輯只有人能看出來,這使得優化能在彙編中進行地更徹底一點.另外,GC和VC的Intrinsics不大一樣,跨平臺很難搞,基本得一種方法寫兩遍.因此,Intrinsics----放棄!


    其實我最推崇的還是用匯編生成靜態庫的形式,畢竟這樣能讓C++部分和彙編部分的"耦合度"降低一些.以前開發的時候我經常這麼用,畢竟調用函數push和pop一下,只要__stdcall或者__fastcall這些東西設置對,基本不會出問題,以前總這樣用,把軟件數學算法部分彙編一寫,別人拿走就用去了.比較爽.但是我之前寫的,都是比較大的函數,比如一整套FIR濾波+頻譜分析的完整算法,直接調用就行,要考慮的是cache missing的問題,而不是push和pop影響效率的問題.可現在如果是一個類庫,那麼必然一個大的方法是由一些簡單的方法結合的,比如多個向量加法必然是一堆兩個向量的加法合成的.這時如果每次向量加法都是一次函數調用,對性能影響還是不小的.一般這時如果是在彙編裏,就用宏來搞就行了.(順便推薦一下面向對象的彙編器HLA)但現在要做的是C++類庫,總不能裏面再包個彙編庫吧...而且我對cache的尺寸老是拿不準,經常missing...因此我想到最好還是用內聯彙編比較好可以把函數是否inline的權力交給編譯器.當然,可以用extern inline來定義一個外部的inline函數,但神奇的是extern inline好像只能作用於外部的cpp函數或者c函數,一旦碰見外部彙編生成的靜態庫函數,iniline就失效了..VC和intel編譯器都是如此,extren forcedinline也不行.


    內聯彙編的話,在x86裏面比較簡單,直接__asm{}就行了,比較噁心的在於:

        1.VC居然不讓人在x64下面內聯彙編.

        2.GCC的彙編語法和VC完全不一樣.

        3.VC對內聯彙編的支持並不好.很容易出問題.

    內聯彙編語法的問題相信不少人都瞭解,GCC裏用的是AT&T風格,VC裏用的是intel風格.這個倒不是大問題,網上有這兩種格式的轉換器.AT&T看着比較暈但除了lea指令的寫法比較噁心外其他的都還算合理.一般語句後要緊跟數據類型比如mov要寫成movl之類,但我發現光寫mov也可以,SSE相關指令比如movapd寫了後綴反倒不能編譯..不知是爲何... 兩種格式的問題主要在於,把代碼放到GCC裏面或VS裏的時候,我總是要添加個宏判斷一下編譯器然後弄兩個格式各來一個.又麻煩又容易出錯(比如改了其中一個版本另一個忘了改).以前我一般都懶得寫AT&T風格,如果代碼需要用在mac裏面,可以通過把編譯器換成LLVM GCC來支持intel風格.至於x64下VS裏的內聯彙編,沒有任何辦法,有一些變通的辦法但很噁心,目前我能找到的唯一辦法就是用intel編譯器了.intel編譯器在VS裏面用着一個比較方便,二個它算法方面優化相當好,三個MSVC支持的特性它都支持,甚至連predefined宏都和MSVC一樣.總之就是很好.這幾個問題中最要命的是:內聯彙編不是C++標準的東西,編譯器不知道你到底在asm{}塊裏面幹了什麼,它只是簡單地把那段彙編代碼貼到正確地位置上而已.這樣主要會導致幾個問題:首先是棧更容易被破壞,改一下esp直接就崩了.(VS還算不錯,提醒你一下),另外是編譯器不知道所有寄存器在asm{}塊前後值的變化,其次是C代碼和內聯asm代碼的銜接很要命.這個是我最關心的問題,比如一個整型變量在C++代碼裏的名字叫X,我當然可以簡單地mov eax,X (intel風格)把它加載到eax寄存器裏,這是一個內存讀取操作,但實際上很多情況下會發現X的值根本就是在另外某個寄存器裏,甚至本來就在eax裏(由之前的C++代碼導致的).這時根本就不需要這個mov指令.但關鍵問題是你不能肯定X到底在哪個寄存器裏呆着.所以你還是mov eax,X最保險.然後你的最終代碼裏就會時不時帶有類似這種噁心代碼:

    mov esi,X //這是編譯器寫出來的代碼

    mov eax,X //這是內聯彙編你手寫的代碼

    這是有病啊,純有病...又或者asm{}塊的輸出寫在了一個內存上,然後緊接着C++編譯器又去那塊內存上去讀取變量了.生成的彙編代碼和上面那個一樣有病.再比如類方法,this指針一般存在ecx裏傳輸,但你真不能肯定當前的ecx真的是this(千萬別肯定).於是你又要從內存裏讀一遍this到某個寄存器裏.當然,你可以把一個函數聲明爲裸函數(naked),然後從頭至尾自己維護棧,寄存器等等.但是一旦一個函數裸了,它就不能內聯了.......在這些問題的解決上,GCC做的相當漂亮,首先你可以把輸入的值定義在一個虛擬的寄存器上,由編譯器來決定具體用哪個寄存器(輸出也一樣),其次你可以用過手動告知編譯器你這段asm()塊改變了哪些寄存器的值,是否操作了內存.GCC甚至可以深入到asm塊內去優化你寫的彙編代碼(前提是藉助你告訴它的信息).這點在vc的內聯彙編中是完全沒有的.GCC生成的代碼,反彙編的時候能看到內聯asm和編譯器生成的彙編語句的結合很舒服.以後有時間我會貼一些具體的代碼出來.綜上,最終我決定使用內聯彙編的方式,用GCC的內聯語法來寫這個類庫.wait,那VC怎麼辦? 簡單,intel編譯器同時支持intel彙編語法和AT&T語法(確實強).比如想在VC裏用AT&T,先把編譯器換成intel,再把VC默認的__asm{}這個語句,改爲__asm__();你就可以在其中使用AT&T了.



另外再說個題外話,想在AT&T的格式的asm塊中使用intel格式的內聯彙編...可以這樣:


asm (
".intel_syntax noprefix;"
"mov eax, _a;" //intel風格
"mov ecx, 3;"
"mul ecx;"
"mov _a, eax;"
"mov rax,rcx;"
"movupd xmm0,xmm2;"
".att_syntax noprefix"
);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章