編譯的目的是將源碼轉化為機器可識別的可執(zhí)行程序,在早期,每次編譯都需要重新構建所有東西,后來人們意識到可以讓編譯器自動完成一些工作,從而提升編譯效率。
但“編譯器不過是用于代碼生成的軟機器,你可以使用你想要的任何語言來生成代碼”,真的是必要的嗎?
誠然,編譯器可以為你生成高性能的代碼,但是你真的需要編譯器嗎?另一種方法是用 Assembly 編寫程序,雖然有點夸大,但這種方法有兩個主要缺陷:
1. 匯編代碼不可移植;
2. 雖然在現(xiàn)代工具的輔助下變得容易了些,但 Assembly 編程仍然需要大量繁瑣的工作。
值得慶幸的是,我們都生活在二十一世紀,這兩個問題都已得到解決。第一個解決方案是LLVM,最初,它意味著“低級虛擬機”,這正是我們可以確保可移植性的原因。簡而言之,它需要用一些非常低級別的與硬件無關語言編寫的代碼,并為特定的硬件平臺返回一些高度優(yōu)化的原生代碼。使用 LLVM,我們既具有低級編程的強大功能,又具有面向硬件微優(yōu)化的自動化功能。
第二個問題的解決方法是使用“腳本”語言,Scheme、Python、Perl,甚至 bash 或 AWK 都可以消除繁瑣的工作。
實驗計劃
首先,讓我們生成一個完全內聯(lián)展開的解決方案,并將其嵌入到基準測試代碼中。該計劃如下:
1. 使用 Clang 為基準生成 LLVM 中間代碼,該基準用于測量 solve_5,一個不存在的函數(shù);
2. 使 Python 在 LLVM 中生成線性求解器(linear solver)代碼;
3. 使用 Python 腳本測試基準,用生成求解器替換 solve_5 調用;
4. 使用 LLVM 靜態(tài)編譯器將中間代碼轉換為機器代碼;
5. 使用 GNU 匯編器和 Clang 的鏈接器將機器代碼轉換為可執(zhí)行的二進制文件。
這就是它在 Makefile 中的樣子:
Python 部分
我們需要 Python 中的線性求解器(linear solver),就像我們使用 C 和 C ++ 一樣,此處代碼為:
#thisgeneratesn-solverinLLVMcodewithLLVMCodeobjects.#NoLLVMstuffyet,justcompletelyPythonicsolutiondefsolve_linear_system(a_array,b_array,x_array,n_value):defa(i,j,n):ifn==n_value:returna_array[i*n_value+j]returna(i,j,n+1)*a(n,n,n+1)-a(i,n,n+1)*a(n,j,n+1)defb(i,n):ifn==n_value:returnb_array[i]returna(n,n,n+1)*b(i,n+1)-a(i,n,n+1)*b(n,n+1)defx(i):d=b(i,i+1)forjinrange(i):d-=a(i,j,i+1)*x_array[j]returnd/a(i,i,i+1)forkinrange(n_value):x_array[k]=x(k)returnx_array
當我們用數(shù)字運行時,我們可以得到數(shù)字。但我們想要代碼,因此,我們需要制作一個假裝成數(shù)字的對象(Object)來探測算法。該對象記錄下算法想要執(zhí)行的每一個操作,并準備好集成 LLVM 中間語言。
#thisisbasicallythewholeLLVMlayerI=0STACK=[]classLLVMCode:#theonlyconstructorfornowisbydouble*instructiondef__init__(self,io,code=''):self.io=ioself.code=codedef__getitem__(self,i):globalI,STACKcopy_code="%"+str(I+1)copy_code+="=getelementptrinboundsdouble,double*"copy_code+=self.io+",i64"+str(i)+" "copy_code+="%"+str(I+2)copy_code+="=loaddouble,double*%"+str(I+1)copy_code+=",align8 "I+=2STACK+=[I]returnLLVMCode(self.io,copy_code)def__setitem__(self,i,other_llvcode):globalI,STACKself.code+=other_llvcode.codeself.code+="%"+str(I+1)self.code+="=getelementptrinboundsdouble,double*"self.code+=self.io+",i64"+str(i)+" "self.code+="storedouble%"+str(I)self.code+=",double*%"+str(I+1)+",align8 "I+=1STACK=STACK[:-1]returnselfdefgeneral_arithmetics(self,operator,other_llvcode):globalI,STACKself.code+=other_llvcode.code;self.code+="%"+str(I+1)+"=f"+operatorself.code+="double%"+str(STACK[-2])+",%"self.code+=str(STACK[-1])+" ";I+=1STACK=STACK[:-2]+[I]returnselfdef__add__(self,other_llvcode):returnself.general_arithmetics('add',other_llvcode)def__sub__(self,other_llvcode):returnself.general_arithmetics('sub',other_llvcode)def__mul__(self,other_llvcode):returnself.general_arithmetics('mul',other_llvcode)def__div__(self,other_llvcode):returnself.general_arithmetics('div',other_llvcode)
接著,當我們使用這種對象運行求解器時,我們得到了一個用 LLVM 中間語言編寫的全功能線性求解器。然后我們將其放入基準代碼中進行速度測試(看它有多快)。
LLVM 中的指令有編號,我們希望保存枚舉,因此將代碼插入到基準測試中的函數(shù)很重要,但也不是很復雜。
#thisreplacesthefunctioncall#andupdatesalltheinstructions'indicesdefreplace_call(text,line,params):globalI,STACK#'%12'->12I=int(''.join([xiforxiinparams[2]ifxi.isdigit()]))first_instruction_to_replace=I+1STACK=[]replacement=solve_linear_system(LLVMCode(params[0]),LLVMCode(params[1]),LLVMCode(params[2]),5).codedelta_instruction=I-first_instruction_to_replace+1foriinxrange(first_instruction_to_replace,sys.maxint):not_found=sum([text.find('%'+str(i)+c)==-1forcinPOSSIBLE_CHARS_NUMBER_FOLLOWS_WITH])ifnot_found==4:#thelastinstructionhasalreadybeensubstitutedbreaknew_i=i+delta_instructionforcinPOSSIBLE_CHARS_NUMBER_FOLLOWS_WITH:#substituteinstructionnumbertext=text.replace('%'+str(i)+c,'%'+str(new_i)+c)returntext.replace(line,replacement)
實現(xiàn)解算器的整段代碼提供了 Python-to-LLVM 層,其中代碼插入只有 100 行!
另附 GitHub 鏈接:
https://github.com/akalenuk/wordsandbuttons/blob/master/exp/python_to_llvm/exp_embed_on_call/substitute_solver_call.py
基準
基準測試本身在 C 中。當我們運行 Makefile 時,它對 solve_5 的調用被 Python 生成的 LLVM 代碼所取代。
Step 1. Benchmark C source code
Step 2. LLVM 匯編語言
Step 3. 調用替換后的 LLVM
Step 4. 本地優(yōu)化裝配
最值得注意的是 Python 腳本生成的超冗長中間代碼如何變成一些非常緊湊且非常有效的硬件代碼。同時它也是高度標量化的,但它是否足以與 C 和 C++ 的解決方案競爭呢?
以下是三種情況的近似數(shù)字(帶有技巧的 C、C++ 與基于 LLVM 的 Python 的性能對比):
1. C 的技巧對 Clang 來說并不適用,因此測量 GCC 版本,其平均運行大約 70 毫秒;
2. C++ 版本是用 Clang 構建的,運行時間為 60 毫秒;
3. Python 版本(此處描述的版本)僅運行 55 毫秒。
當然,這種加速并不是關鍵,但這表明你可以用 Python 編寫出勝過用 C 或 C++ 編寫的程序。這也就暗示你不必學習一些特殊語言來創(chuàng)建高性能的應用程序或庫。
結論
快速編譯語言和慢速腳本語言之間的對立不過是虛張聲勢。原生代碼生成的可能不是核心功能,而是類似于可插拔選項。像是Python 編譯器Numba或Lua 的Terra,其優(yōu)勢就在于你可以用一種語言進行研究和快速原型設計,然后使用相同的語言生成高性能的代碼。
高性能計算沒有理由保留編譯語言的特權,編譯器只是用于代碼生成的軟機器。你可以使用你想要的任何語言生成代碼,我相信如果你愿意,你可以教 Matlab 生成超快的 LLVM 代碼。
本文涉及的所有測試均在 Intel(R)Core(TM)i7-7700HQ CPU @ 2.80GHz 上進行,代碼使用 Clang 3.8.0-2ubuntu4 和 g++5.4.0 編譯。
基準測試源代碼:
https://github.com/akalenuk/wordsandbuttons/tree/master/exp/python_to_llvm
-
代碼
+關注
關注
30文章
4819瀏覽量
68878 -
編譯器
+關注
關注
1文章
1642瀏覽量
49224 -
python
+關注
關注
56文章
4806瀏覽量
84933
原文標題:都有Python了,還要什么編譯器!
文章出處:【微信號:rgznai100,微信公眾號:rgznai100】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論