基於python的opcode優化和模塊按需加載機制研究(學習與個人思路)(原創)
基於python的opcode優化和模塊按需加載機制研究(學習與思考)
姓名:XXX
學校信息:XXX
主用編程語言:python3.5
個人技術博客:http://www.cnblogs.com/Mufasa/
文檔轉換為PDF有些圖片無法完全顯示,請移步我的博客查看
完成時間:2019.03.06
本項目希望您能完成以下任務:
- 優化python字節碼解析代碼,從底層提升python腳本運行效率;(底層、編譯器、虛擬機)
- 基本思路可以統計遊戲常用opcode指令,進行類似opcode合並,opcode排序;
- 另外,可以研究下指令預測相關資料,比如indirect threading,尋找更優的機制;(自然語言處理裏面的東西好像可以用,類似語言翻譯)
- 為了縮短應用的啟動時間,需要在應用啟動時,把模塊進行按需加載(或者延遲加載,lazy import);(優化啟動項)
- 目前的不同實現主要是針對 Python 標準庫進行處理,對第三方擴展庫,尤其是遊戲引擎相關的擴展支持不好,甚至無法支持;(軟件適配&通用化)
- 此課題不僅有一定的學術研究意義,更在手遊等App中有很好的實用價值;(意義價值)
- 希望在自適應學習的基礎上,能夠做到按需加載。(自適應)
一,Python 字節碼
Python 源代碼文件以.py結尾,字節碼文件以.pyc結尾;其中字節碼文件在一個叫__pycache__ 的子目錄中
圖1 python代碼運行過程
Python執行的四步操作:
1,lexing: 詞法分析,就是把一個句子分解成 token。大致來說,就是用str.split()可以實現的功能。
2,parsing:解析,就是把這些 token 組裝成一個邏輯結構。
3,compiling:編譯,把這個邏輯結構轉化成一個或者多個code object (代碼對象)
4,interpreting:解釋,執行每個code object 代表的代碼。
其中前三步可以歸類為“代碼編譯”,最後一步單獨成類。Python
分清function object、code object ,以及 bytecode
①function object:定義一個函數之後,它就成了一個function object (函數對象)。只要不使用函數調用符號——也就是小括號——這個函數就不會執行。但是它已經被編譯了,可以通過這個function object 的__code__ 屬性找到它的 code object
②code object:code object 的類型是‘code’
③bytecode:bytecode 是 code object 的一個屬性的值。這個屬性名為 co_code,它的類型是‘bytes’,長度是8。例:b‘|\x00\x00d\x01\x00\x14S‘
實例1:
1 >>> def double(a): 2 return a*2 # 並不知道為什麽貼在這裏縮進會是這樣 3 4 >>> import dis 5 >>> dis.dis(double) 6 2 0 LOAD_FAST 0 (a) 7 2 LOAD_CONST 1 (2) 8 4 BINARY_MULTIPLY 9 6 RETURN_VALUE
第1列的2源代碼中的行號;第2列的數字 0 3 6 7 是 bytecode 的偏移量;第3列很好理解,都是opcode。
因為可以節省編譯時間,這裏有一篇非常詳細的文章,作者在遺傳編程領域工作,發現他們Python 程序的總運算時間中,有50%都被編譯過程吃掉。於是作者深入到 bytecode 層次進行了小小改動,大幅削減了編譯時間,把總的運算時間降至不足原先的一半。(有改進的潛力)
猜想的優化方向
1,從字節碼bytecode上下手
圖2 python字節碼優化猜想1-代碼級優化
優點:有一些固定的套路可以使用並且實施起來比較簡單,例子:累加可以直接將很多分步直接在同一次處理中進行,節省步數
缺點:優化後的效率,不能達到量級變化
2,從串行轉並行入手(多線程、多進程、多核心)
圖3 python字節碼優化猜想2-處理方式優化
優點:需要從python解釋器底層進行重新布局
缺點:優化的效率可以成倍數提升,並且效率與相關硬件有一定關系,參考nvidia的pascal架構的並行計算卡
前景:現在的手機芯片、電腦芯片大都是多核心、多進程的,這個可以一試。
我自己之前去實習的公司中船重工709所淩久電子,設計過一臺擁有256顆C66X核心的DSP處理機,這臺機器這個就是實時、並發計算的,耗電快趕上空調,但是性能真的很強很強!!!(我在簡歷裏面寫過)
(註:我只是最近兩天看了一下相關的文檔資料,我現在不確定python解釋器是否已經在內部集成並行處理的功能)
參考:
用Python實現多核心並行計算
淺談多核CPU、多線程與並行計算
3,從python解釋器層級進行優化
圖4 python字節碼優化猜想3-解釋器層級
之前的兩個都是不觸及python最底層的東西,這裏是從最底層進行優化的思考。
如上圖4可知,我們現在常使用的CPython解釋器是通過C語言進行二級運行的,這就和android虛擬機一樣,一臺機器上運行另一個環境,當我們想要改變什麽的時候還需要通過中介來通知做出改變,這個就和兩個人隔著墻通過手機來通話,但是這樣不如我們面對面溝通的明了!!!
優點:可以省去中間的很多步驟,直接對計算機硬件進行操作,效率提升至C語言那般暢快
缺點:開發難度大,計算機越接近底層開發難度越大,這需要一個團隊來進行。
參考:Python解釋器
總體參考鏈接:
理解 Python 的執行方式,與字節碼 bytecode 玩耍 (上)
理解 Python 的執行方式,與字節碼 bytecode 玩耍 (下)
Fun with Python bytecode
二,opcode指令
根據上文中的bytecode以及其附屬的給人類理解查看opcode。opcode又稱為操作碼,是將python源代碼進行編譯之後的結果,python虛擬機無法直接執行human-readable的源代碼,因此python編譯器第一步先將源代碼進行編譯,以此得到opcode。例如在執行python程序時一般會先生成一個pyc文件,pyc文件就是編譯後的結果,其中含有opcode序列。Opcode和bytecode是有一定相關性的兩種不同表述。(這裏不做累贅表述)
python的目標不是一個性能高效的語言,出於腳本動態類型的原因虛擬機做了大量計算來判斷一個變量的當前類型,並且整個python虛擬機是基於棧邏輯的,頻繁的壓棧出棧操作也需要大量計算。
缺點即為可能的改進方向!
參考鏈接:
深入理解python之Opcode備忘錄
理解Python之opcode及優化
操作碼定義opcode.h
PythonCodeObjectParser
Peephole optimization
三,指令預測
這個可以參考自然語言處理(NLP),指令是計算機的語言,自然語言是人類的語言,這兩種語言都有自己需要表達的意思。如果未來機器有了自我意識那麽指令預測和我們自然人的語言詞句預測又有何分別?!!
自然語言處理中的詞句預測可以遷移到代碼的指令預測。
參考鏈接:
【PaddlePaddle】自然語言處理:句詞預測
Wikipedia-Threaded code
高性能虛擬機解釋器:DTC vs ITC(Indirect-Threaded Code)
Dynamically Disabling Way-prediction to Reduce Instruction Replay
The research of indirect transfer prediction technology based on information feedback
四,模塊按需加載
Python import原理:
圖5 import運行大致原理
使用import module_name語句就可以將這個文件作為模塊導入。系統在導入模塊時,要做以下三件事:
1.為源代碼文件中定義的對象創建一個名字空間,通過這個名字空間可以訪問到模塊中定義的函數及變量。
2.在新創建的名字空間裏執行源代碼文件。
3.創建一個名為源代碼文件的對象,該對象引用模塊的名字空間,這樣就可以通過這個對象訪問模塊中的函數及變量。
普通加載方式:
文件擡頭就對所有所需的庫進行加載,這樣的缺點是耗時(尤其是對快應用、啟動速度有要求的程序很敏感)
以前的兩種惰性/延遲加載方法:
①本地子功能區加載而非程序啟動時的全局加載。直到你的程序運行需要這個庫的時候才進行加載;缺點:易重復載入庫文件、容易遺忘庫載入的範圍。
②惰性加載。需要模塊的時候觸發 ModuleNotFoundError 提前發現這個模塊,而延遲的只是後續補加載過程;缺點:顯式優於隱式、如果一個模塊希望立即加載,那麽在延遲加載時,它可能會嚴重崩潰。(Mercurial實際上開發了一個模塊黑名單,以避免延遲加載來解決這個問題,但是他們必須確保對其進行更新,因此這也不是一個完美的解決方案。)
最新py3.7中的方法:
在Python 3.7中,模塊現在可以在其上定義__getattr__(),允許編寫一個函數,在模塊上的屬性不可用時導入模塊。這樣做的缺點是使它成為一個惰性導入而不是一個加載,因此很晚才發現是否會引發ModuleNotFoundError。但是它是顯式的,並且仍然是為您的模塊全局定義的,因此更容易控制。
改進方向:發現導入錯誤被推遲,如何提前獲知這個可能出現的導入錯誤防止程序拋出異常並終止。
缺點很明顯啊!當你用的時候才開始加載,這個會鎖住主線程進行庫加載動作,如果是帶有畫面的操作,那麽就會有視覺延遲(假設這個加載是第一次運行,且很耗時)
改進:能不能在主線程旁邊開一條線程提前進行預加載!!!
1 import importlib 2 3 # 這個是實現lazy_import的功能函數 4 def lazy_import(importer_name, to_import): 5 module = importlib.import_module(importer_name) # 直接加載調用的後一級函數 6 7 import_mapping = {} # 字典 鍵名:有可能為縮寫名 值名:為原始可查找庫名,例如:import_mapping[‘np‘] = ‘numpy‘ 8 for name in to_import: 9 importing, _, binding = name.partition(‘ as ‘) 10 if not binding: 11 _, _, binding = importing.rpartition(‘.‘) 12 import_mapping[binding] = importing 13 14 def __getattr__(name): 15 if name not in import_mapping: # 如果這個庫沒在import_mapping中,就拋出異常錯誤,並且中斷 16 message = f‘module {importer_name!r} has no attribute {name!r}‘ 17 raise AttributeError(message) 18 importing = import_mapping[name] 19 imported = importlib.import_module(importing,module.__spec__.parent) 20 # print(‘name=‘,name,‘module=‘,module,‘module.__spec__=‘,module.__spec__,‘module.__spec__.parent=‘,module.__spec__.parent) 21 setattr(module, name, imported) # sub, np, numpy 22 return imported 23 24 return module, __getattr__ #返回一個庫和一個方法
詳情見網址:lazy_import源碼解析(原創) 我自己的博客
現在的思路:
圖6 按需預加載
圖7 按需預加載運行邏輯
粗糙的實現代碼1:preload.py
優點:
將preload在需要的函數之前運行(這個是多線程的加載方式,不會鎖定主線程的相關計算,同時在計算機IO空閑的時候加載,見縫插針進行。提高程序運行效率),在後面需要相關函數時就直接調用這個功能即可
缺點:
①預加載的代碼提前多少,這個我現在還沒有辦法說清,要具體看機器的計算時間;②不太人性化,需要人為或者程序轉換原始.py程序。
總地來說也是一種嘗試!後面還可以試試其他的多種方法解決。
1 import threading 2 from importlib import import_module 3 # 可以返回值的功能函數 4 class MyThread(threading.Thread): 5 def __init__(self, func, args, name=‘‘): 6 threading.Thread.__init__(self) 7 self.name = name 8 self.func = func 9 self.args = args 10 self.result = self.func(*self.args) 11 12 # 返回一個函數 13 def get_result(self): 14 try: 15 return self.result 16 except Exception: 17 return None 18 19 def module_before(module_name): 20 t = MyThread(import_module, [module_name], import_module.__name__) 21 t.start() 22 return t 23 24 if __name__ == ‘__main__‘: 25 numpy_mid = module_before(‘numpy‘) 26 numpy = numpy_mid.get_result() 27 print(numpy.array([1, 2, 3, 4]))
代碼段2:測試_preload.py
1 import preload as pld 2 3 # 這裏面的預加載是放在需要這個函數的前面的相關代碼塊前, 4 modules = [‘numpy‘, ‘sys‘, ‘os‘] 5 module = {} 6 for i in modules: 7 module[i] = pld.module_before(i) 8 9 np = module[‘numpy‘].get_result() 10 print(np.array([1, 2, 3, 4]))
參考鏈接:
__getattr__使用方法
setattr() 函數
An approach to lazy importing in Python 3.7
動態加載lazy_import(利用__import__)
python之import機制
動態導入對象,importlib.import_module()使用
關於Python的import機制原理
備註:這個裏面的思路應該是有些問題的,畢竟自己也不是專業的,歡迎大家討論指教。
基於python的opcode優化和模塊按需加載機制研究(學習與個人思路)(原創)