華為云專家心得:20個開發技巧教你開發高性能計算代碼
高性能計算,是一個非常廣泛的話題,可以從專用硬件/處理器/體系結構/GPU,說到操作系統/線程/進程/并行/并發算法,再到集群/網格計算,最后到天河二號(TH-1)。
我們這次的分享會從個人的實踐項目探索出發,與大家分享自己摸爬滾打得出的心得體會,一如既往的堅持原創。其中內容涉及到優化規劃 / 執行 / 多進程 / 開發心理等約20個要點,其中例子代碼片段,使用Python。
高性能計算,在商業軟件應用開發過程中,要解決的核心問題,用很白話的方式來說,“在有限的硬件條件下,如何讓一段原本跑不動的代碼,跑起來,甚至飛起來。”
性能提升經驗
舉2個例子,隨意感受下。
? 635萬條用戶閱讀文檔的歷史行為數據,數據處理時間,由50小時,優化到15秒。(是的,你沒有看錯)
? 基于Mongo的寬表創建,由20小時,優化到出去打杯水的功夫。
在大數據的時代,一個優秀的程序員,可以寫出性能比其他人的程序高出數百倍,甚至數千倍,具備這樣的技能,對產品的貢獻無疑是很大的,對個人而言,也是自己履歷上亮點和加分項。
聊聊歷史
2000年前后,由于PC硬件限制,那一代的程序員,比如,國內的求伯君 / 雷軍,國外的比爾蓋茨 / 卡馬特,都是可以從機器碼 / 匯編的角度來提升程序性能。
到2005年前后,PC硬件性能發展迅速,高性能優化常常聽到,來自嵌入式設備和移動設備。那個年代的移動設備主流使用J2ME開發,可用內存128KB。那個年代的程序員,需要對程序大小(OTA下載,有數據流量限制,如128KB),內存使用都精打細算,真的是掐著指頭算。比如,通常一個程序,只有一個類,因為新增一個類,會多使用幾K內存。數據文件會合并為一個,減少文件數,這樣需要算,比如從第幾個字節開始,是什么數據。
2008年前后,第一代iOS / Android智能手機上市,App可用內存達到1GB,App可以通過WIFI下載,App大小也可以達到一百多MB。我剛才看了下我的P30,就存儲空間而言,QQ使用了4G,而微信使用了10G。設備性能提升,可用內存和存儲空間大了,程序員們終于“解放”了,直到–大數據時代的到來。
在大數據時代下,數據量瘋狂增長,一個大的數據集操作,你的程序跑一晚上才出結果,是常有的事。
基礎知識
本次分享假設讀者已經了解了線程/進程/GIL這些概念,如果不了解,也沒有關系,可以讀下以下的摘要,并記住下面3點基礎知識小結即可。
什么是進程?什么是線程?兩者的差別?
以下內容來自Wikipedia: https://en.wikipedia.org/wiki/Thread_(computing)
Threads differ from traditional multitasking operating-system processes in several ways:
? processes are typically independent, while threads exist as subsets of a process
? processes carry considerably more state information than threads, whereas multiple threads within a process share process state as well as memory and other resources
? processes have separate address spaces, whereas threads share their address space
? processes interact only through system-provided inter-process communication mechanisms
? context switching between threads in the same process typically occurs faster than context switching between processes
著名的GIL (Global interpreter lock)
以下內容來自 wikipedia.
A global interpreter lock (GIL) is a mechanism used in computer-language interpreters to synchronize the execution of threads so that only one native thread can execute at a time.[1] An interpreter that uses GIL always allows exactly one thread to execute at a time, even if run on a multi-core processor. Some popular interpreters that have GIL are CPython and Ruby MRI.
基礎知識小結:
? 因為著名的GIL,為了線程安全,Python里的線程,只能跑在同一個CPU核,無法做到真正的并行
? 計算密集型應用,選用多進程
? IO密集型應用,選用多線程
實踐要點
以上都是一些鋪墊,從現在開始,我們進入正題,如何開發高性能代碼。
一直以來,我都在思考,如何做有效的分享?首先,我堅持原創,如果同樣的內容可以在網絡上找到,那就沒有分享的必要,浪費自己和其他人的時間。其次,對不同的人,采用不同的方法,講不同的內容。
所以,這次分享,聽眾大都是有開發經驗的python程序員,所以,我們不在一些基礎的內容上花太多時間,不了解也沒關系,下來自已看看也都能看懂。這次我們更多來從實踐問題出發,我總結了約20個要點和開發技巧,希望能對大家今后的工作有幫助。
規劃和設計盡可能早,而實現則盡可能晚
接到一個項目時,我們可以先識別下,哪些部分可能會出現性能問題,做到心里有數。在設計上,可以早點想著,比如,選用合適的數據結構,把類和方法設計解耦,便于將來做優化。
在我們以前的項目中,見過有些項目,因為早期沒有去提前設計,后期想優化,發現改動太大,風險非常高。
但是,這里一個常見的錯誤是,上來就優化。在軟件開發的世界里,這點一直被經常提起。我們需要控制自己想早優化的心理,而應優先把大框架搭起來,實現主要功能,然后再考慮性能優化。
先簡單實現,再評估,做好計劃,再優化實施
評估改造成本和收益,比如,一個模塊費時一小時,如果優化,需要花費開發和測試時間3小時,可能節省30分鐘,性能提升50%;另一模塊,費時30秒,如果優化,開發和測試需要花費同樣的時間,可以節省20秒,性能提升67%。你會優先優化哪個模塊?
我們建議優先考慮第一個模塊,因為收益更大,可節省30分鐘;而第二個模塊,費時30秒,不優化也能接受,應該把優化優先級放到最低。
另一個情況,如第2個模塊被其它模塊高頻調用,那我們又要重新評估優先級。
優化時,我們要控制我們可能產生的沖動:優化一切能優化的部分。
當我們沒有“錘子”時,我們遇到問題很苦惱,缺乏技能和工具;但是,當我們擁有“錘子”時,我們又很容易看一切事物都像“釘子”。
開發調試時,使用Sampling數據,并配合開關配置
開發時,對費時的計算,可以設置sampling參數,調動時,傳入不同的參數,既可以快速測試,又可以安全管理調試和生產代碼。千萬不要用注釋的方式,來開/關代碼。
參考以下示意代碼:
1 #?Bad 2 def?calculate_bad(): 3 #?uncomment?for?debugging 4 #?data?=?load_sampling_data() 5 data?=?load_all_data() 6 7 #?Good 8 def?calculate(sampling=False): 9 if?sampling: 10 data?=?load_sampling_data() 11 else: 12 data?=?load_all_data()
梳理清楚數據Pipeline,建立性能評估機制
我自己寫了個Decorator @timeit 可以很方便地打印代碼的用時。
1 @timeit 2 def?calculate(): 3 pass
這樣生成的log,菜市場大媽都看的懂。上了生產后,也可以通知配置來控制是否打印。
[2020-07-09?14:44:09,138]?INFO:?TrialDataContainer.load_all_data?-?Start ... [2020-07-09?14:44:09,158]?INFO:?preprocess_demand?-?Start [2020-07-09?14:44:09,172]?INFO:?preprocess_demand?-?End?-?Spent:?0.012998?s ... [2020-07-09?14:44:09,186]?INFO:?preprocess_warehouse?-?Start [2020-07-09?14:44:09,189]?INFO:?preprocess_warehouse?-?End?-?Spent:?0.002611?s ... [2020-07-09?14:44:09,454]?INFO:?preprocess_substitution?-?Start [2020-07-09?14:44:09,628]?INFO:?preprocess_substitution?-?End?-?Spent:?0.178258?s ... [2020-07-09?14:44:10,055]?INFO:?preprocess_penalty?-?Start [2020-07-09?14:44:20,823]?INFO:?preprocess_penalty?-?End?-?Spent:?10.763566?s [2020-07-09?14:44:20,835]?INFO:?TrialDataContainer.load_all_data?-?End?-?Spent:?11.692677?s [2020-07-09?14:44:20,836]?INFO:?ObjectModelsController.build?-?Start [2020-07-09?14:44:20,836]?INFO:?ObjectModelsController.build_penalties?-?Start [2020-07-09?14:44:20,836]?INFO:?ObjectModelsController.build_penalties?-?End?-?Spent:?0.000007?s [2020-07-09?14:44:20,837]?INFO:?ObjectModelsController.build_warehouses?-?Start [2020-07-09?14:44:20,848]?INFO:?ObjectModelsController.build_warehouses?-?End?-?Spent:?0.011002?s
另外,Python也提供了Profiling工具,可以用于費時函數的定位。
優先處理數據讀取性能
一個完整的項目,可能會有很多性能提升的部分,我建議,優先處理數據讀取,原因是,問題容易定位,修改代碼相對獨立,見效快。
舉例來說,很多機器學習項目,都需要建立數據樣本數據,用于模型訓練。而數據樣本的建立,常通過創建一個寬表來實現。很多DB都提供了很多提升操作性能的方法。假設我們使用MongoDB,其提供了pipeline函數,可以把多個數據操作,放在一個語句中,一次傳給DB。
如果我們粗暴地單條處理,在一個項目中我們試過,需要近20個小時,花了半天的時間來優化,跑起來,離開座位去接杯水,回來就已經跑完了,費時降為1分鐘。
注意,很多時候我們沒有動力去優化數據讀取的性能,因為數據讀取可能次數并不多,但事實上,特別是在試算階段,數據讀取的次數其實并不少,因為我們總是沒有停止過對數據的改變,比如加個字段,加個特征什么的,這時候,數據讀取的代碼就要經常被用到,那么優化的收益就體現出來了。
再考慮降低時間復雜度,考慮使用預處理,用空間換時間
我們如果把性能優化當做一桌宴席,那么可以把數據讀取部分的性能優化,當作開胃小菜。接下來,我們進入更好玩的部分,優化時間復雜度,用空間換時間。
舉例來說,如果你的程序的復雜度為O(n^2),在數據很大時,一定會非常低效,如果能優化為復雜度為O(n),甚至O(1),那就會帶來幾個數據級的性能提升。
比如上面提到的,使用倒排表,來做數據預處理,用空間換時間,達到從50小時到15秒的性能提升。
因著名的GIL,使用多進程提升性能,而非多線程
在Python的世界里,由于著名的GIL,如果要提升計算性能,其基本準則為:對于I/O操作密集型應用,使用多線程;對于計算密集型應用,使用多進程。
一個多進程的例子:
我們準備了一個長數組,并準備了一個相對比較費時的等差數列求和計算函數。
1 MAX_LENGTH?=?20_000 2 data?=?[i?for?i?in?range(MAX_LENGTH)] 3 4 def?calculate(num): 5 """Calculate?the?number?and?then?return?the?result.""" 6 result?=?sum([i?for?i?in?range(num)]) 7 return?result 單進程執行例子代碼: 1 def?run_sinpro(func,?data): 2 """The?function?using?a?single?process.""" 3 results?=?[] 4 5 for?num?in?data: 6 res?=?func(num) 7 results.append(res) 8 9 total?=?sum(results) 10 11 return?total 12 13 %%time 14 result?=?run_sinpro(calculate,?data) 15 result
CPU?times:?user?8.48?s,?sys:?88?ms,?total:?8.56?s Wall?time:?8.59?s 1333133340000
從這里我們可以看到,單進程需要 ~9 秒。
接下來,我們來看看,如何使用多進程來優化這段代碼。
1 #?import?multiple?processing?lib 2 import?sys 3 4 from?multiprocessing?import?Pool,?cpu_count 5 from?multiprocessing?import?get_start_method,?\ 6 set_start_method,?\ 7 get_all_start_methods 8 9 def?mulp_map(func,?iterable,?proc_num): 10 """The?function?using?multi-processes.""" 11 with?Pool(proc_num)?as?pool: 12 results?=?pool.map(func,?iterable) 13 14 return?results 15 16 def?run_mulp(func,?data,?proc_num): 17 results?=?mulp_map(func,?data,?proc_num) 18 total?=?sum(results) 19 20 return?total 21 22 %%time 23 result?=?run_mulp(calculate,?data,?4) 24 result
CPU?times:?user?14?ms,?sys:?19?ms,?total:?33?ms Wall?time:?3.26?s 1333133340000
同樣的計算,使用單進程,需要約9秒;在8核的機器上,如果我們使用多進程則只需要3秒,耗時節省了 66%。
多進程:設計好計算單元,應盡可能小
我們來設想一個場景,假設你有10名員工,同時你有10項工作,每項工作中,都由相同的5項子工作組成。你會如何來做安排呢?理所當然的,我們應該把這10名員工,分別安排到這10項工作中,讓這10項工作并行執行,沒毛病,對吧?但是,在我們的項目中,如果這樣來設計并行計算,很可能出問題。
這里是一個真實的例子,最后性能提升的效果很差。原因是什么呢?(此處可按Pause鍵,思考一下)
主要的原因有2個,并行的計算單元顆粒度不應太大,大了以后,通常會有數據交換或共享問題。其次,顆粒度大了以后,完成時間會差別比較大,形成短板效應。也就是,顆粒度大了以后,任務完成時間可能會差別很大。
在一個真實的例子中,并行計算需要1個小時,最后分析后才發現,只有一個進程需要1小時,而其他進程的任務都在5分鐘內完成了。
另一個好處是,出錯了,好定位,代碼也好維護。所以,計算單元應盡可能小。
多進程:避免進程間通信或同步
當我們把計算單元設計的足夠小后,應該盡量避免進程間通信或同步,避免造成等待,影響整體執行時間。
多進程:調試是個問題,除了log外,嘗試gdb / pdb
并行計算的公認問題是,難調試。通常的IDE只可以中斷一個進程。通過打印log,并加上pid,來定位問題,會是一個比較好的方法。注意,并行計算時,不要打太多log。如果你按照上面講的,先調通了單進程的實現,那么這時,最重要是,打印進程的啟動點,進程數據和關閉點,就可以了。比如,觀測到某個進程拖了大家的后腿,那就要好好看看那個進程對應的數據。
這是個細致活,特別是,當多進程啟動后,可能跑著數小時,你也不知道在發生什么?可以使用linux下的top,或windows下的activity等工具來觀測進程的狀態。也可以使用gdb / pdb這樣的工具,進入某個進程中,看看卡在哪里。
多進程:避免大量數據作為參數傳輸
在真實的項目中,我們設計的計算單元,不會像上面的簡單例子一樣,通常都會帶有不少參數。這時需要注意,當大數據作為參數傳輸時,會導致內存消耗很大,并且,子進程的創建也會很慢。
多進程:Fork? Spawn?
Python的多進程支持3種模式去啟一個進程,分別是,spawn, fork, forkserver。他們之間的差別是啟動速度,和繼承的資源。spawn只繼承必要的資源,而fork和forkserver則與父進程完全相同。
依賴于不同的操作系統,和不同版本的python,其默認模式也不同。對python 3.8,Windows默認spawn;從python 3.8開始,macOS也默認使用spawn;Unix類OS默認fork;fork和forkserver在windows上不可用。
靈魂拷問:多進程一定比單進程快嗎?
講到這里,我們的分享基本可以結束了,對吧?按照python multiprocessing API,找幾個例子,并參考我上面說的幾點,能解決80%以上的問題。夠了,畢竟性能優化也不是天天需要。以下內容可能要從事性能優化一年后,才會思考到,這里寫出來,供參考,幫助以后少走些彎路。
比如,多進程一定更快嗎?
正如第一點所說,任何優化都有開銷。當多進程解決不了你的問題時,別忘了試試,改回單進程,說不定就解決了。(這也是一個真實的例子,花了2周去優化一個,10進程也需要3小時才能執行完的程序,改回單進程后,直接跑進30分鐘內了。)
優化心理:手里有了錘子,一切都長的像釘子
同上要點,有時候需要的,可能是優化數據結構,而不是多進程。
優化心理:不要迷信“專家”
相信很多團隊都這樣,當項目遇到重大技術問題,比如性能需要優化,管理者都會召集一些專家來幫忙。根據我的觀察,80%的情況下,沒有太多幫助,有時甚至更糟。
原因很簡單,用一句話來說,你花了20個小時解決不了的問題,其他人用5分鐘,根據你提供的信息,指出問題所在,可能性很低,無論他相關的經驗有多么豐富。如果不信,你可以回想下自己的經驗,或將來注意觀察下,再回過頭來看這個觀點。為什么可能更糟?因為依賴心理。有了專家的依賴,人們是不會真拼的,“反正有專家指引”。就像尼采說過,“人們要完成一件看似不可能的事時,需要鼓脹到超過自己的能力。”,所以,如果這件事真的很難,你“瘋狂”地相信,“這件事只有你能解決,只能靠你自己,其他人都無法解決”,說不定效果更好。
在一個持續近一個月的性能優化項目中,我腦海中時常響起《名偵探柯南》中的一句臺詞:真相只有一個。我堅定無比地相信,解法離我越來越近,哪怕事實是,一次又一次地失敗,但這份信念到最后的成功幫助很大。
優化心理:優化可能是一個長期過程,每天都在迷茫中掙扎
性能優化的過程,漫長而煎熬,如果能有一個耐心的聽眾,會幫助很大。他/她可能不會幫你指出問題的解決辦法,只是耐心地聽著,只說,“it will be fine.” 但這樣的述說,會幫助理清思路,能靈感迸發也說不定。這跟生活中其它事情的道理,應該也是一樣的吧。
優化心理:管理者幫助爭取時間,減輕心理壓力
比如,有經驗的管理者,會跟業務協商,分階段交付。而有些同學,則會每隔幾小時就過來問下,“性能有提升嗎?” 然后臉上露出一種詭異的表情:“真的有那么難?”
目前我所有知道的一個案例,其性能優化持續了近一年,期間幾撥外協人員,來了,又走了,搞得奔潰。
所以,我們呼吁,項目管理者應該多理解開發人員,幫助開發人員擋住外部壓力,而不是直接透傳壓力,或者甚至增大壓力。
References
? https://baike.baidu.com/item/高性能計算
? https://www.liaoxuefeng.com/wiki/1016959663602400/1017627212385376
? https://en.wikipedia.org/wiki/Thread_(computing)
? https://en.wikipedia.org/wiki/Global_interpreter_lock#:~:text=A global interpreter lock (GIL,on%20a%20multi%2Dcore%20processor.
? https://git.huawei.com/x00349737/nqutils
? https://docs.python.org/3/library/profile.html
多線程 高性能計算 Python
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。