《python源碼剖析》閱讀筆記
第一章 python的內(nèi)建對(duì)象
python中一切都是對(duì)象。在PyIntObject中定義了很多函數(shù)指針,這些函數(shù)指針對(duì)應(yīng)著類型對(duì)象所定義的操作。其中有三組非常重要的操作族,tp_as_number, tp_as_sequence, tp_as_mapping,分別對(duì)應(yīng)著PyNumberMethods, PySequenceMethods, PyMappingMethods函數(shù)族,這三個(gè)函數(shù)都是分別定義著一個(gè)整數(shù)對(duì)象、序列對(duì)象、關(guān)聯(lián)對(duì)象應(yīng)該有的一些操作。在python中對(duì)于任何一種類型來說,都可以同時(shí)定義三個(gè)函數(shù)族中的所有操作,也就是說,我們可以在繼承python固有的類型時(shí),又同時(shí)重寫python的special method,從而實(shí)現(xiàn)我們自己定義的特殊類型。比如可以實(shí)現(xiàn)一個(gè)對(duì)象,既可以表現(xiàn)出數(shù)值特性,也可以表現(xiàn)出關(guān)聯(lián)對(duì)象的屬性。
第二章 python中的整數(shù)對(duì)象
python的整數(shù)對(duì)象。在python中會(huì)有一個(gè)整數(shù)對(duì)象池,里面維護(hù)著-5到256之間的整數(shù),當(dāng)我們需要使用這些范圍的數(shù)字時(shí),直接到對(duì)象池中去提取。這樣設(shè)計(jì)是因?yàn)樵趯?shí)際的開發(fā)中,這些小數(shù)可能會(huì)被頻繁的使用,頻繁的創(chuàng)建和湮滅會(huì)導(dǎo)致效率非常低。對(duì)于超出這些范圍的數(shù)字,python底層則是通過單獨(dú)申請(qǐng)一塊內(nèi)存塊來給這些大整數(shù)輪流使用,當(dāng)某個(gè)對(duì)象的引用次數(shù)為零的時(shí)候,會(huì)把該對(duì)象所占用的這塊內(nèi)存添加到空閑區(qū)(注意,這里python并沒有將釋放的內(nèi)存還給系統(tǒng),所以當(dāng)出現(xiàn)很多大數(shù)要被使用的時(shí)候,而這些數(shù)又是使用一次,可能就會(huì)占用系統(tǒng)的所有內(nèi)存。)
第三章 python中的字符串對(duì)象
-
在python中,PyStringObject是字符串對(duì)象的實(shí)現(xiàn),它是一個(gè)可變長度內(nèi)存的對(duì)象,同時(shí)又是一個(gè)不可變對(duì)象(字符串定義之后不可再改變)。python字符串對(duì)象中的intern機(jī)制,類似于前面的python整數(shù)對(duì)象池。其關(guān)鍵是在于在系統(tǒng)中有一個(gè)(key, value)的映射關(guān)系集合,集合名字叫做Intered。在這個(gè)集合中記錄過被intern機(jī)制處理過的字符串。當(dāng)python在創(chuàng)建一個(gè)字符串時(shí),首先會(huì)建立一個(gè)PyStringObject對(duì)象a,然后利用intern機(jī)制處理,如果當(dāng)前這個(gè)字符串對(duì)象已經(jīng)在interned中存在,記為b,則將指向a的對(duì)象的指針指向b,然后intern機(jī)制會(huì)將對(duì)象a的引用計(jì)數(shù)減為0而被銷毀。這樣的話,可以達(dá)到減少內(nèi)存的目的。
也許會(huì)有一個(gè)問題,為什么一定要先創(chuàng)建一個(gè)臨時(shí)PyStringObject對(duì)象呢?
這是因?yàn)閕ntern機(jī)制只能應(yīng)用在PyStringObject對(duì)象之上,別的任何對(duì)象都不可以。因此這里必須要先創(chuàng)建一個(gè)臨時(shí)對(duì)象。 - python中也為一個(gè)字節(jié)的字符設(shè)計(jì)了字符緩沖池,與小整數(shù)對(duì)象池不一樣的是,小整數(shù)對(duì)象池在python初始化的時(shí)候就會(huì)被創(chuàng)建,而字符串對(duì)象中的字符緩沖池是以靜態(tài)變量形式存在的,在python初始化完成之后,字符緩沖池是空的。當(dāng)python在創(chuàng)建一個(gè)PyStringObject的時(shí)候,如果字符串長度為1(即單個(gè)字符串),則將會(huì)對(duì)這個(gè)字符進(jìn)行intern操作,將intern的這個(gè)結(jié)果緩存到字符緩沖池中。當(dāng)通過PyString_FromStringandSize(const char *str, int size)來創(chuàng)建字符對(duì)象時(shí),如果size等于1,并且在字符緩沖池中存在,則直接返回。
- python中字符串的連接效率問題。當(dāng)我們采用+號(hào)來連接多個(gè)字符串時(shí),效率是非常低的。其根源在于,python中的字符串對(duì)象是一個(gè)不可變對(duì)象,因此在連接兩個(gè)字符串的時(shí)候,則要去新建一個(gè)PyStringObject對(duì)象,當(dāng)我們用‘+’來連接N個(gè)字符串時(shí),實(shí)際上會(huì)進(jìn)行N-1次內(nèi)存申請(qǐng)和內(nèi)存搬運(yùn),因而效率很低。 解決方案是,我們采用字符串對(duì)象的join操作來實(shí)現(xiàn)多個(gè)字符串的連接,當(dāng)使用join操作的時(shí)候,會(huì)一次性計(jì)算好需要的總的內(nèi)存,也就是只需要執(zhí)行一次內(nèi)存申請(qǐng),因此效率會(huì)比用‘+’號(hào)連接的效率更高
strs_list = ['hello', 'fay', 'welcome', 'to', 'python ']
' '.join(strs_list)
https://www.cnblogs.com/megachen/p/9156646.html
Python源碼讀后小結(jié)
?
?
?
《改善Python程序的91個(gè)建議》
訪問flyai.club,一鍵創(chuàng)建你的人工智能項(xiàng)目。
作者 | 笑虎
來源 | 代碼灣
自己寫Python也有四五年了,一直是用自己的“強(qiáng)迫癥”在維持自己代碼的質(zhì)量,除了Google的Python代碼規(guī)范外,從來沒有讀過類似的書籍。偶然的機(jī)會(huì)看到這么一本書,讀完之后覺得還不錯(cuò),所以做個(gè)簡(jiǎn)單的筆記。有想學(xué)習(xí)類似知識(shí)的朋友,又懶得去讀完整本書籍,可以參考一下。
1:引論
建議1、理解Pythonic概念—-詳見Python中的《Python之禪》
建議2、編寫Pythonic代碼
避免不規(guī)范代碼,比如只用大小寫區(qū)分變量、使用容易混淆的變量名、害怕過長變量名等。有時(shí)候長的變量名會(huì)使代碼更加具有可讀性。
深入學(xué)習(xí)Python相關(guān)知識(shí),比如語言特性、庫特性等,比如Python演變過程等。 深入學(xué)習(xí)一兩個(gè)業(yè)內(nèi)公認(rèn)的Pythonic的代碼庫,比如Flask等。
https://angelteng.github.io/blog/2019/07/25/Flask源碼學(xué)習(xí)/
?
建議3:理解Python與C的不同之處,比如縮進(jìn)與{},單引號(hào)雙引號(hào),三元操作符?,Switch-Case語句等。
建議4:在代碼中適當(dāng)添加注釋
建議5:適當(dāng)添加空行使代碼布局更加合理
建議6:編寫函數(shù)的4個(gè)原則
函數(shù)設(shè)計(jì)要盡量短小,嵌套層次不宜過深
函數(shù)聲明應(yīng)該做到合理、簡(jiǎn)單、易用
函數(shù)參數(shù)設(shè)計(jì)應(yīng)該考慮向下兼容
一個(gè)函數(shù)只做一件事,盡量保證函數(shù)粒度的一致性
建議7:將常量集中在一個(gè)文件,且常量名盡量使用全大寫字母
2:編程慣用法
建議8:利用assert語句來發(fā)現(xiàn)問題,但要注意,斷言assert會(huì)影響效率
建議9:數(shù)據(jù)交換值時(shí)不推薦使用臨時(shí)變量,而是直接a, b = b, a
建議10:充分利用惰性計(jì)算(Lazy evaluation)的特性,從而避免不必要的計(jì)算
?
建議11:理解枚舉替代實(shí)現(xiàn)的缺陷(最新版Python中已經(jīng)加入了枚舉特性)
建議12:不推薦使用type來進(jìn)行類型檢查,因?yàn)橛行r(shí)候type的結(jié)果并不一定可靠。如果有需求,建議使用isinstance函數(shù)來代替
建議13:盡量將變量轉(zhuǎn)化為浮點(diǎn)類型后再做除法(Python3以后不用考慮)
建議14:警惕eval()函數(shù)的安全漏洞,有點(diǎn)類似于SQL注入
建議15:使用enumerate()同時(shí)獲取序列迭代的索引和值
建議16:分清==和is的適用場(chǎng)景,特別是在比較字符串等不可變類型變量時(shí)
建議17:盡量使用Unicode。在Python2中編碼是很讓人頭痛的一件事,但Python3就不用過多考慮了
建議18:構(gòu)建合理的包層次來管理Module
3:基礎(chǔ)用法
建議19:有節(jié)制的使用from…import語句,防止污染命名空間
建議20:優(yōu)先使用absolute import來導(dǎo)入模塊(Python3中已經(jīng)移除了relative import)
建議21:i+=1不等于++i,在Python中,++i前邊的加號(hào)僅表示正,不表示操作
建議22:習(xí)慣使用with自動(dòng)關(guān)閉資源,特別是在文件讀寫中
建議23:使用else子句簡(jiǎn)化循環(huán)(異常處理)
建議24:遵循異常處理的幾點(diǎn)基本原則
注意異常的粒度,try塊中盡量少寫代碼
謹(jǐn)慎使用單獨(dú)的except語句,或except Exception語句,而是定位到具體異常
注意異常捕獲的順序,在合適的層次處理異常
使用更加友好的異常信息,遵守異常參數(shù)的規(guī)范
建議25:避免finally中可能發(fā)生的陷阱
建議26:深入理解None,正確判斷對(duì)象是否為空。Python中下列數(shù)據(jù)會(huì)判斷為空:
?
建議27:連接字符串應(yīng)優(yōu)先使用join函數(shù),而不是+操作
建議28:格式化字符串時(shí)盡量使用.format函數(shù),而不是%形式
建議29:區(qū)別對(duì)待可變對(duì)象和不可變對(duì)象,特別是作為函數(shù)參數(shù)時(shí)
建議30:[], {}和():一致的容器初始化形式。使用列表解析可以使代碼更清晰,同時(shí)效率更高
?
建議31:函數(shù)傳參數(shù),既不是傳值也不是傳引用,而是傳對(duì)象或者說對(duì)象的引用
建議32:警惕默認(rèn)參數(shù)潛在的問題,特別是當(dāng)默認(rèn)參數(shù)為可變對(duì)象時(shí)
建議33:函數(shù)中慎用變長參數(shù)*args和**kargs
這種使用太靈活,從而使得函數(shù)簽名不夠清晰,可讀性較差
如果因?yàn)楹瘮?shù)參數(shù)過多而是用變長參數(shù)簡(jiǎn)化函數(shù)定義,那么一般該函數(shù)可以重構(gòu)
建議34:深入理解str()和repr()的區(qū)別
兩者之間的目標(biāo)不同:str主要面向客戶,其目的是可讀性,返回形式為用戶友好性和可讀性都比較高的字符串形式;而repr是面向Python解釋器或者說Python開發(fā)人員,其目的是準(zhǔn)確性,其返回值表示Python解釋器內(nèi)部的定義
?
在解釋器中直接輸入變量,默認(rèn)調(diào)用repr函數(shù),而print(var)默認(rèn)調(diào)用str函數(shù)
repr函數(shù)的返回值一般可以用eval函數(shù)來還原對(duì)象
兩者分別調(diào)用對(duì)象的內(nèi)建函數(shù)__str__()和__repr__()
建議35:分清靜態(tài)方法staticmethod和類方法classmethod的使用場(chǎng)景
4:庫
建議36:掌握字符串的基本用法
建議37:按需選擇sort()和sorted()函數(shù)
sort()是列表在就地進(jìn)行排序,所以不能排序元組等不可變類型。
sorted()可以排序任意的可迭代類型,同時(shí)不改變?cè)兞勘旧怼?
建議38:使用copy模塊深拷貝對(duì)象,區(qū)分淺拷貝(shallow copy)和深拷貝(deep copy)
建議39:使用Counter進(jìn)行計(jì)數(shù)統(tǒng)計(jì),Counter是字典類的子類,在collections模塊中
建議40:深入掌握ConfigParser
建議41:使用argparse模塊處理命令行參數(shù)
建議42:使用pandas處理大型CSV文件
Python本身提供一個(gè)CSV文件處理模塊,并提供reader、writer等函數(shù)。
Pandas可提供分塊、合并處理等,適用于數(shù)據(jù)量大的情況,且對(duì)二維數(shù)據(jù)操作更方便。
建議43:使用ElementTree解析XML
建議44:理解模塊pickle的優(yōu)劣
優(yōu)勢(shì):接口簡(jiǎn)單、各平臺(tái)通用、支持的數(shù)據(jù)類型廣泛、擴(kuò)展性強(qiáng)
劣勢(shì):不保證數(shù)據(jù)操作的原子性、存在安全問題、不同語言之間不兼容
建議45:序列化的另一個(gè)選擇JSON模塊:load和dump操作
建議46:使用traceback獲取棧信息
建議47:使用logging記錄日志信息
建議48:使用threading模塊編寫多線程程序
建議49:使用Queue模塊使多線程編程更安全
5:設(shè)計(jì)模式
建議50:利用模塊實(shí)現(xiàn)單例模式
?
建議51:用mixin模式讓程序更加靈活
建議52:用發(fā)布-訂閱模式實(shí)現(xiàn)松耦合
建議53:用狀態(tài)模式美化代碼
?
6:內(nèi)部機(jī)制
建議54:理解build-in對(duì)象
建議55:__init__()不是構(gòu)造方法,理解__new__()與它之間的區(qū)別
?
建議56:理解變量的查找機(jī)制,即作用域
局部作用域
全局作用域
嵌套作用域
內(nèi)置作用域
建議57:為什么需要self參數(shù)
建議58:理解MRO(方法解析順序)與多繼承
?
建議59:理解描述符機(jī)制
建議60:區(qū)別__getattr__()與__getattribute__()方法之間的區(qū)別
?
建議61:使用更安全的property
建議62:掌握元類metaclass
建議63:熟悉Python對(duì)象協(xié)議
?
建議64:利用操作符重載實(shí)現(xiàn)中綴語法
?
建議65:熟悉Python的迭代器協(xié)議
建議66:熟悉Python的生成器
建議67:基于生成器的協(xié)程和 greenlet ,理解協(xié)程、多線程、多進(jìn)程之間的區(qū)別
?
建議68:理解GIL的局限性
建議69:對(duì)象的管理和垃圾回收
7:使用工具輔助項(xiàng)目開發(fā)
建議70:從PyPI安裝第三方包
建議71:使用pip和yolk安裝、管理包
建議72:做paster創(chuàng)建包
建議73:理解單元測(cè)試的概念
建議74:為包編寫單元測(cè)試
建議75:利用測(cè)試驅(qū)動(dòng)開發(fā)(TDD)提高代碼的可測(cè)性
建議76:使用Pylint檢查代碼風(fēng)格
代碼風(fēng)格審查
代碼錯(cuò)誤檢查
發(fā)現(xiàn)重復(fù)以及不合理的代碼,方便重構(gòu)
高度的可配置化和可定制化
支持各種IDE和編輯器的集成
能夠基于Python代碼生成UML圖
能夠與Jenkins等持續(xù)集成工具相結(jié)合,支持自動(dòng)代碼審查
建議77:進(jìn)行高效的代碼審查
建議78:將包發(fā)布到PyPI
8:性能剖析與優(yōu)化
建議79:了解代碼優(yōu)化的基本原則
建議80:借助性能優(yōu)化工具
建議81:利用cProfile定位性能瓶頸
建議82:使用memory_profiler和objgraph剖析內(nèi)存使用
建議83:努力降低算法復(fù)雜度
建議84:掌握循環(huán)優(yōu)化的基本技巧
減少循環(huán)內(nèi)部的計(jì)算
將顯式循環(huán)改為隱式循環(huán),當(dāng)然這會(huì)犧牲代碼的可讀性
在循環(huán)中盡量引用局部變量
關(guān)注內(nèi)層嵌套循環(huán)
建議85:使用生成器提高效率
建議86:使用不同的數(shù)據(jù)結(jié)構(gòu)優(yōu)化性能
建議87:充分利用set的優(yōu)勢(shì)
建議88:使用multiprocessing模塊克服GIL缺陷
建議89:使用線程池提高效率
建議90:使用C/C++模塊擴(kuò)展提高性能
建議91:使用Cythonb編寫擴(kuò)展模塊
訪問 flyai.club ,一鍵創(chuàng)建你的人工智能項(xiàng)目。
End
?
《Python 面向?qū)ο缶幊讨改稀?
?
Python 面向?qū)ο缶幊讨改希ǜ咔灏妫? PDF
?
百度網(wǎng)盤
鏈接: https://pan.baidu.com/s/1SbD4gum4yGcUruH9icTPCQ
提取碼: fzk5
?
內(nèi)置方法 ? ? 說明
?__init__(self,...) ? ? 初始化對(duì)象,在創(chuàng)建新對(duì)象時(shí)調(diào)用
?__del__(self) ? ? 釋放對(duì)象,在對(duì)象被刪除之前調(diào)用
?__new__(cls,*args,**kwd) ? ? 實(shí)例的生成操作
?__str__(self) ? ? 在使用print語句時(shí)被調(diào)用
?__getitem__(self,key) ? ? 獲取序列的索引key對(duì)應(yīng)的值,等價(jià)于seq[key]
?__len__(self) ? ? 在調(diào)用內(nèi)聯(lián)函數(shù)len()時(shí)被調(diào)用
?__cmp__(stc,dst) ? ? 比較兩個(gè)對(duì)象src和dst
?__getattr__(s,name) ? ? 獲取屬性的值
?__setattr__(s,name,value) ? ? 設(shè)置屬性的值
?__delattr__(s,name) ? ? 刪除name屬性
?__getattribute__() ? ? __getattribute__()功能與__getattr__()類似
?__gt__(self,other) ? ? 判斷self對(duì)象是否大于other對(duì)象
?__lt__(slef,other) ? ? 判斷self對(duì)象是否小于other對(duì)象
?__ge__(slef,other) ? ? 判斷self對(duì)象是否大于或者等于other對(duì)象
?__le__(slef,other) ? ? 判斷self對(duì)象是否小于或者等于other對(duì)象
?__eq__(slef,other) ? ? 判斷self對(duì)象是否等于other對(duì)象
?__call__(self,*args) ? ? 把實(shí)例對(duì)象作為函數(shù)調(diào)用
?
https://blog.csdn.net/huangyimo/article/details/50561841
?
?
?
?
??
?
?
?
《Python 高性能編程》
python高性能編程第一章讀書筆記
計(jì)算機(jī)底層組件分為三大基本部分:計(jì)算單元、存儲(chǔ)單元以及兩者之間的連接。
計(jì)算單元:具有將接收到的任意輸入轉(zhuǎn)換成輸出的能力以及改變當(dāng)前處理狀態(tài)的能力。CPU是最常見的計(jì)算單元。它的主要屬性是其每個(gè)周期能進(jìn)行的操作數(shù)量以及每秒能完成多少個(gè)周期。第一個(gè)屬性通過每周期完成的指令數(shù)(IPC)來衡量。第二個(gè)屬性通過其時(shí)鐘速度來衡量。時(shí)鐘速度的提高,可以使得每秒進(jìn)行更多的計(jì)算,提高該計(jì)算單元所有程序的運(yùn)行速度。IPC的提高則在矢量計(jì)算能力上有相當(dāng)程度的影響。矢量計(jì)算指的是一次提供多個(gè)數(shù)據(jù)給一個(gè)CPU并能同時(shí)被操作。這種類型的CPU指令被稱為SIMD(單指令多數(shù)據(jù))。
???????? 由于時(shí)鐘速度和IPC提升陷入停滯,開始 依靠超線程技術(shù),亂序執(zhí)行和多核架構(gòu)來提高速度 。超線程技術(shù)為主機(jī)的操作系統(tǒng)(OS)虛擬了第二個(gè)CPU,硬件邏輯則試圖將兩個(gè)指令線程交錯(cuò)地插入單個(gè)CPU的執(zhí)行單元。亂序執(zhí)行允許編譯器檢測(cè)出一個(gè)線性程序中某部分可以不依賴于之前的工作,也就是說兩個(gè)工作能夠以各種順序執(zhí)行或同時(shí)執(zhí)行。使得當(dāng)一些指令被阻塞(比如等待一次內(nèi)存訪問),另一些指令得以執(zhí)行。多核架構(gòu)指的是給CPU增加更多的核心。但是不一定多核就會(huì)更快,阿姆達(dá)爾定律認(rèn)為:如果一個(gè)可以運(yùn)行在多核上的程序有某些執(zhí)行路徑必須運(yùn)行在單核上,那么這些路徑就會(huì)成為瓶頸導(dǎo)致最終速度無法加快。
???????? 對(duì)于python來說,python的全局解釋器鎖(GIL),確保Python進(jìn)程一次只能執(zhí)行一條指令,無論當(dāng)前有多少個(gè)核心。使得無法使用多核,但是我們可以 使用標(biāo)準(zhǔn)庫的multiprocessing,或numexpr、Cython技術(shù),或分布式計(jì)算模型來避免 。
???????? 存儲(chǔ)單元,用于保存比特。比如主板上的寄存器、RAM以及硬盤。所有這些不同類型的存儲(chǔ)單元主要區(qū)別在于讀寫數(shù)據(jù)的速度。且速度與讀寫方式有關(guān),如順序讀取要比隨機(jī)讀取快得多。此外還有延時(shí)來表示設(shè)備查找數(shù)據(jù)所花費(fèi)的時(shí)間。
???????? 比如說判斷一個(gè)數(shù)是否為質(zhì)數(shù),首先將number的值保存在RAM中。為了計(jì)算sqrt_number和number_float,將該值傳入CPU中,理想情況下只需要傳一次,它將被保存在CPU的L1/L2緩存中,然后CPU進(jìn)行兩次計(jì)算并將結(jié)果傳回RAM保存。在循環(huán)部分,我們 更希望一次就將 number——float和 多個(gè)i的值傳入CPU進(jìn)行檢查 。
???????? Python虛擬機(jī)抽象層的影響之一就是矢量操作變得不是直接可用 。而numpy這樣的外部庫可以 通過增加矢量化數(shù)學(xué)操作 來幫助我們解決這個(gè)問題。
???????? Python抽象還影響了任何需要為下一次計(jì)算保存L1/L2緩存中相關(guān)數(shù)據(jù)的優(yōu)化。首先是python對(duì)象不再是內(nèi)存中最優(yōu)化的布局。這是因?yàn)閜ython是一種垃圾收集語言----內(nèi)存會(huì)被自動(dòng)分配并在需要時(shí)釋放。這會(huì)導(dǎo)致內(nèi)存碎片并影響CPU緩存的傳輸。
???????? Python的優(yōu)勢(shì)在于可以輕易調(diào)用其他系統(tǒng), 正確運(yùn)用庫可以使python代碼在速度上和c媲美。
?
?
Python 高性能編程第4章 字典和集合
對(duì)次序未知的列表/元組的最優(yōu)查詢時(shí)間O(logn),字典和集合 基于鍵的查詢 則可以帶給我們O(1)的查詢時(shí)間。除此之外,和列表/元組一樣,字典和集合的插入時(shí)間O(1)。為了達(dá)到O(1)的查詢時(shí)間,在底層使用的數(shù)據(jù)結(jié)構(gòu)是一個(gè)開放地址散列表。然而,使用字典和集合有其代價(jià),首先通常會(huì)占用更多的內(nèi)存。同時(shí)雖然插入/查詢的復(fù)雜度是O(1),但實(shí)際的速度極大取決于使用的散列函數(shù)。如果散列函數(shù)的運(yùn)行速度較慢,那么在字典和集合上進(jìn)行的任何操作也會(huì)相應(yīng)變慢。
集合保證了它包含的鍵的唯一性,如果嘗試添加一個(gè)已有的項(xiàng),該項(xiàng)不會(huì)被添加進(jìn)集合,這一操作的代價(jià)是O(1),而列表進(jìn)行相應(yīng)操作的代價(jià)是O(n)
對(duì)于散列表,新插入數(shù)據(jù)的位置取決于數(shù)據(jù)的兩個(gè)屬性:鍵的散列值以及該值如何跟其他對(duì)象比較。這是因?yàn)楫?dāng)我們插入數(shù)據(jù)時(shí),首先需要計(jì)算鍵的散列值并掩碼來得到一個(gè)有效的數(shù)據(jù)索引。掩碼是為了保證一個(gè)可能是任意數(shù)字的散列值最終能落入分配的桶中。所以,如果我們分配了8個(gè)塊的內(nèi)存,而我們的散列值是28957,那么它將落入的桶的索引是28957&0b111 = 7,如果我們的字典增長到了需要512塊內(nèi)存,那么掩碼就變成了0b11111111(此時(shí)我們會(huì)使用28957&0b11111111的桶)。現(xiàn)在我們檢查這個(gè)桶是否已經(jīng)被使用,如果是空桶,那么可以將鍵和值插入這一內(nèi)存塊。我們保存鍵是為了在獲取時(shí)確保獲得的是正確的值。如果桶已經(jīng)被使用,且桶內(nèi)的值和我們希望插入的值相等,說明這一鍵值對(duì)已經(jīng)被保存,則直接返回。如果值不相等,則找一個(gè)新的位置來保存位置。
為了找到新的索引,我們用一個(gè) 簡(jiǎn)單的線性函數(shù)計(jì)算出一個(gè)新的索引,這一方法稱為嗅探 。 Pyhon的嗅探機(jī)制使用了原始散列值的高位比特 (對(duì)于之前那個(gè)長度為8的散列表,由于使用的掩碼mask=0b111=bin(8-1),我們只用了最后3個(gè)bit作為初始索引)。使用這些 高位比特使得每一個(gè)散列值生成的下一可用散列序列都是不同的 ,這樣就能幫助防止未來的碰撞。
當(dāng)我們?cè)诓樵兡硞€(gè)鍵時(shí)也有一個(gè)類似的過程:給出的鍵會(huì)被轉(zhuǎn)化為一個(gè)索引進(jìn)行檢索。如果和該索引指向的位置中的鍵符合(在插入操作時(shí)我們會(huì)保存原始的鍵),那么我們就會(huì)返回那個(gè)值。如果不符合,我們用同一方案繼續(xù)創(chuàng)建新的索引,直到我們找到數(shù)據(jù)或找到一個(gè)空桶。如果我們找到一個(gè)空桶,我們就可以認(rèn)為表里不存在該數(shù)據(jù)。
當(dāng)越來越多的項(xiàng)目被插入列表時(shí),表本身必須改變大小來適應(yīng)。研究顯示 一個(gè)不超過三分之二滿的表在具有最佳空間節(jié)約的同時(shí),依然具有不錯(cuò)的散列碰撞避免率 。因此,當(dāng)一個(gè)表到達(dá)關(guān)鍵點(diǎn)時(shí),它就會(huì)增長。為了做到這一點(diǎn),需要分配一個(gè)更大的表(也就是在內(nèi)存中預(yù)留更多的桶),將掩碼調(diào)整為適合新的表,舊表中的所有元素被重新插入新表。這需要重新計(jì)算索引,因?yàn)楦淖兒蟮难诖a會(huì)改變索引計(jì)算結(jié)果。結(jié)果就是, 改大散列表的代價(jià)非常大! 不過因?yàn)槲覀冎辉诒硖r(shí)而不是在每一次操作時(shí)進(jìn)行這一操作,分?jǐn)偤竺恳淮尾迦氲拇鷥r(jià)依然是O(1)。值得注意的是,當(dāng)一個(gè)散列表變大或變小時(shí)都可能發(fā)生改變大小。也就是說,如果散列表中足夠多的元素被刪除,表可能會(huì)被改小,但是 改變大小僅發(fā)生在插入時(shí) 。
每當(dāng)Python訪問一個(gè)變量、函數(shù)或模塊時(shí),都有一個(gè)體系來決定它去哪里查找這些對(duì)象。首先Python查找 locals()數(shù)組 ,其內(nèi)保存了所有本地變量的條目。這是整條鏈上唯一一個(gè)不需要字典查詢的部分。如果它不再本地變量里,那么會(huì)搜索 globals()字典 。最后如果對(duì)象也不在那里,則會(huì)搜索 __builtin__對(duì)象 。要注意locals()和globals()是顯式的字典而__builtin__則是模塊對(duì)象,在搜索__builtin__中的一個(gè)屬性時(shí),我們其實(shí)在搜索它的locals(字典)(對(duì)所有的模塊對(duì)象和類對(duì)象都是如此)。
?
《Python高性能編程》筆記
2019-07-23
|?In?Python
性能分析
- 基本技術(shù)如 IPython 的 timeit 魔法函數(shù)、time.time()、以及一個(gè)計(jì)時(shí)修飾器,使用這些技術(shù)來了解語句和函數(shù)的行為。
- 內(nèi)置工具如 cProfile,了解代碼中哪些函數(shù)耗時(shí)最長,并用 runsnake 進(jìn)行可視化。
- line_profiler 工具,對(duì)選定的函數(shù)進(jìn)行逐行分析,其結(jié)果包含每行被調(diào)用的次數(shù)以及每行花費(fèi)的時(shí)間百分比。
- memory_profiler 工具,以圖的形式展示RAM的使用情況隨時(shí)間的變化,解釋為什么某個(gè)函數(shù)占用了比預(yù)期更多的 RAM。
- Guppy 項(xiàng)目的 heapy 工具,查看 Python 堆中對(duì)象的數(shù)量以及每個(gè)對(duì)象的大小,這 對(duì)于消滅奇怪的內(nèi)存泄漏特別有用 。
- dowser 工具,通過Web瀏覽器界面審查一個(gè)持續(xù)運(yùn)行的進(jìn)程中的實(shí)時(shí)對(duì)象。
- dis 模塊,查看 CPython 的字節(jié)碼,了解基于棧的 Python 虛擬機(jī)如何運(yùn)行。
- 單元測(cè)試,在性能分析時(shí)要避免由優(yōu)化手段帶來的破壞性后果。(參見原文,實(shí)用)
數(shù)據(jù)結(jié)構(gòu)影響
-
列表和元組就類似于其它編程語言的數(shù)組,主要用于存儲(chǔ)具有內(nèi)在次序的數(shù)據(jù);
- 高效搜索必需的兩大要素是排序算法和搜索算法。Python 列表有一個(gè)內(nèi)建的排序算法使用了Tim排序。
- 動(dòng)態(tài)數(shù)組支持 resize 操作,可以增加數(shù)組的容量。當(dāng)一個(gè)大小為N的列表第一次需要添加數(shù)據(jù)時(shí),Python會(huì)創(chuàng)建一個(gè)新的列表,足夠存放原來的N個(gè)元素以及額外需要添加的元素。
-
元組固定且不可變。這意味著一旦元組被創(chuàng)建,和列表不同,它的內(nèi)容無法被修改或它的大小也無法被改變。(
元組緩存于Python運(yùn)行時(shí)環(huán)境,這意味著我們每次使用元組時(shí)無須訪問內(nèi)核去分配內(nèi)存。)
-
bi-sect 模塊:提供了一個(gè)簡(jiǎn)便的函數(shù)讓你可以在保持排序的同時(shí)往列表中添加元素,以及一個(gè)高度優(yōu)化過的二分搜索算法函數(shù)來查找元素。
字典和集合就類似其它編程語言的哈希表/散列集,主要用于存儲(chǔ)無序的數(shù)據(jù)。
-
- 新插入數(shù)據(jù)的位置取決于數(shù)據(jù)的兩個(gè)屬性:鍵的散列值以及該值如何跟其他對(duì)象比較。這是因?yàn)楫?dāng)我們插入數(shù)據(jù)時(shí),首先需要計(jì)算鍵的散列值并掩碼來得到一個(gè)有效的數(shù)組索引。
- 如果被占用,那么要找到新的索引,我們用一個(gè)簡(jiǎn)單的線性函數(shù)計(jì)算出一個(gè)新的索引,這一方法稱為嗅探。Python的嗅探機(jī)制使用了原始散列值的高位比特。使用這些高位比特使得每一個(gè)散列值生成的下一可用散列序列都是不同的,這樣就能幫助防止未來的碰撞。
- 當(dāng)一個(gè)值從散列表中被刪除時(shí),我們不能簡(jiǎn)單地寫一個(gè)NULL到內(nèi)存的那個(gè)桶里。這是因?yàn)槲覀円呀?jīng)用NULL來作為嗅探散列碰撞的終止值。所以,我們必須寫一個(gè)特殊的值來表示該桶雖空,但其后可能還有別的因散列碰撞而插入的值。
- 不超過三分之二滿的表在具有最佳空間節(jié)約的同時(shí)依然具有不錯(cuò)的散列碰撞避免率 。 改大散列表的代價(jià)非常昂貴 ,但因?yàn)槲覀冎辉诒硖r(shí)而不是在每一次插入時(shí)進(jìn)行這一操作。
- Python 對(duì)象通常以散列表實(shí)現(xiàn),因?yàn)樗鼈円呀?jīng)有 內(nèi)建的 hash 和 cmp 函數(shù) 。
-
每當(dāng) Python 訪問一個(gè)變量、函數(shù)或模塊時(shí),都有一個(gè)體系來決定它去哪里查找這些對(duì)象。
- locals()數(shù)組
- globals()字典
- __builtin__對(duì)象( builtin 中的一個(gè) 屬性時(shí),我們其實(shí)是在搜索它的 locals()字典)
迭代器、生成器
- 節(jié)約內(nèi)存
- 延遲估值
矩陣與矢量計(jì)算
原生 Python 并不支持矢量操作,因?yàn)?Python 列表存儲(chǔ)的不是實(shí)際的數(shù)據(jù),而是對(duì)實(shí)際數(shù)據(jù)的引用 ; 且Python 字節(jié)碼并沒有針對(duì)矢量操作進(jìn)行優(yōu)化,所以for 循環(huán)無法預(yù)測(cè)何時(shí)使用矢量操作能帶來好處 ; 在矢量和矩陣操作時(shí),這種存儲(chǔ)結(jié)構(gòu)會(huì)造成極大的性能下降 。
?
6.3 內(nèi)存碎片: ? 數(shù)據(jù)被分成小片,你只能對(duì)每一片分別進(jìn)行傳輸,而不是一次性傳輸整個(gè)塊。這意 味著你引入了更多的內(nèi)存?zhèn)鬏旈_銷,且強(qiáng)制?CPU?在數(shù)據(jù)傳輸?shù)倪^程中等待。我們 可以用?perf?看到在緩存失效的情況下這個(gè)問題會(huì)有多嚴(yán)重。 這個(gè)在正確的時(shí)候?qū)⒄_的數(shù)據(jù)傳輸給?CPU?的問題被稱為“ 馮諾伊曼瓶頸 ”。意思 是 現(xiàn)代計(jì)算機(jī)所使用的層次化的內(nèi)存架構(gòu)會(huì)導(dǎo)致?CPU?和內(nèi)存之間的帶寬受到限 制 。如果我們數(shù)據(jù)傳輸?shù)乃俣瓤梢詿o限快,我們就不需要任何緩存,因?yàn)?CPU?可 以立即獲得任何它需要的數(shù)據(jù)。此時(shí)瓶頸就不再存在。 由于數(shù)據(jù)傳輸?shù)乃俣炔豢赡軣o限快,我們必須從?RAM?中預(yù)取數(shù)據(jù)并將其保存在 一個(gè)更小但更快的?CPU?緩存中,并希望當(dāng)?CPU?需要某個(gè)數(shù)據(jù)時(shí),它可以從中更 快讀取到。雖然這已經(jīng)是一個(gè)嚴(yán)重理想化了的場(chǎng)景,我們依然可以看到其中的一 些問題—?我們?nèi)绾沃牢磥硇枰男?shù)據(jù)?CPU?內(nèi)部的 分支預(yù)測(cè)和流水線技 術(shù)會(huì)試圖在處理當(dāng)前指令的同時(shí)預(yù)測(cè)其下一條指令并將相應(yīng)的內(nèi)存讀進(jìn)緩存 。但 是 減少瓶頸最好的方法是讓代碼知道如何分配我們的內(nèi)存以及如何使用我們的 數(shù)據(jù)進(jìn)行計(jì)算 。 探測(cè)內(nèi)存移動(dòng)至?CPU?的性能相當(dāng)困難 ,不過, Linux?上的?perf?工具可以讓我們 洞察?CPU?如何處理運(yùn)行中的程序 。比如,我們可以對(duì)例?6-6?的純?Python?代碼運(yùn) 行?perf?并看到?CPU?運(yùn)行我們代碼的效率。結(jié)果見例?6-8。注意該例以及之后的perf?例子的輸出都被截取以適應(yīng)頁面邊界。被刪除的數(shù)據(jù)包括各測(cè)量值的方差, 用于表示測(cè)量值在幾次測(cè)量中發(fā)生了多大的變化。這有助于看到測(cè)量值在多大程 度上依賴于實(shí)際的程序特性以及多大程度上受到來自系統(tǒng)的其他干擾,比如其他 正在使用系統(tǒng)資源的程序。 |
? |
? | ? |
理解 Perf: ? 讓我們花一秒鐘來理解?perf?告訴我們的各種性能指標(biāo)以及它們跟我們代碼的關(guān) 系。task-clock?指標(biāo)告訴我們的 任務(wù)花了多少個(gè)時(shí)鐘周期 。這跟總體的運(yùn)行時(shí) 間不同,因?yàn)槿绻覀兊某绦蚧艘幻腌妬磉\(yùn)行但是使用了兩個(gè)?CPU,那么task-clock?將是?1000(task-clock?的單位是毫秒)。方便的是,perf?會(huì)幫我 們計(jì)算并在該指標(biāo)旁邊告訴我們有多少個(gè)?CPU?被使用了。這個(gè)數(shù)字不完全等于?1是因?yàn)檫M(jìn)程有一段時(shí)間依賴于其他子系統(tǒng)的指令(比如分配內(nèi)存時(shí))。 context-switches?和?CPU-migrations?告訴我們 程序在等待內(nèi)核操作 (如?I/O操作)完成時(shí),為了讓其他進(jìn)程得以運(yùn)行, 被掛起或遷移到另一個(gè)?CPU?核心上執(zhí) 行的次數(shù) 。當(dāng)一個(gè)?context-switch?發(fā)生時(shí)程序的執(zhí)行會(huì)被掛起,讓另一個(gè)程序 得以執(zhí)行。這是一個(gè)對(duì)時(shí)間要求非常精細(xì)的任務(wù),也是我們需要盡量避免的,但是 我們對(duì)它的發(fā)生無能為力。只要進(jìn)程允許切換,內(nèi)核就會(huì)接手;不過,我們可以做 一些事來抑制內(nèi)核切換我們的程序。 總的來說,內(nèi)核會(huì)在程序進(jìn)行?I/O(比如讀取 內(nèi)存、磁盤或網(wǎng)絡(luò))時(shí)將其掛起 。我們?cè)诤罄m(xù)章節(jié)會(huì)看到,我們 可以用異步操作來 確保我們的程序在等待?I/O?時(shí)繼續(xù)使用?CPU,這會(huì)讓我們的進(jìn)程繼續(xù)運(yùn)行而不被切 換出去 。另外,我們還可以 設(shè)置程序的?nice?值來給我們的程序更高的優(yōu)先級(jí)以防 止內(nèi)核將它切換出去 。類似的, CPU-migrations?會(huì)發(fā)生在進(jìn)程被掛起并遷移到 另一個(gè)?CPU?上繼續(xù)執(zhí)行的情況,這是為了讓所有的?CPU?都有同樣程度的利用率 。 這可以被認(rèn)為是一個(gè)特別糟糕的進(jìn)程切換,因?yàn)槲覀兊某绦虿粌H被暫時(shí)掛起,而且 丟失了?L1?緩存內(nèi)所有的數(shù)據(jù)(每個(gè)?CPU?都有它自己的?L1?緩存)。 page-fault?是現(xiàn)代?UNIX?內(nèi)存分配機(jī)制的一部分。分配內(nèi)存時(shí),內(nèi)核除了告訴 程序一個(gè)內(nèi)存的引用地址以外沒做任何事。但是,之后在這塊內(nèi)存第一次被使用時(shí), 操作系統(tǒng)會(huì)拋出一個(gè)缺頁小中斷,這將暫停程序的運(yùn)行并正確分配內(nèi)存。這被稱為 延遲分配系統(tǒng) 。雖然這種手段相比以前的內(nèi)存分配系統(tǒng)是一個(gè)很大的優(yōu)化,缺頁小 中斷本身依然是一個(gè)相當(dāng)昂貴的操作,因?yàn)榇蠖鄶?shù)操作都發(fā)生在你的程序外部。另 外還有一種缺頁大中斷,發(fā)生于當(dāng)你的程序需要從設(shè)備(磁盤、網(wǎng)絡(luò)等)上請(qǐng)求還 未被讀取的數(shù)據(jù)時(shí)。這些操作更加昂貴,因?yàn)樗麄儾粌H中斷了你的程序,還需要讀 取數(shù)據(jù)所在的設(shè)備。這種缺頁不總是影響?CPU?密集的工作,但是,它會(huì)給任何需 要讀寫磁盤或網(wǎng)絡(luò)的程序帶來痛苦。 |
? |
Numpy 能夠?qū)?
數(shù)據(jù)連續(xù)存儲(chǔ)
在內(nèi)存中并支持?jǐn)?shù)據(jù)的矢量操作,在數(shù)據(jù)處理方面,它是高性能編程的最佳解決方案之一。
Numpy 帶來性能提升的關(guān)鍵在于,它使用了高度優(yōu)化且特殊構(gòu)建的對(duì)象,取代了通用的列表結(jié)構(gòu)來處理數(shù)組,由此
減少了內(nèi)存碎片
;此外,
自動(dòng)矢量化
的數(shù)學(xué)操作使得矩陣計(jì)算非常高效。
Numpy 在矢量操作上的
缺陷是一次只能處理一個(gè)操作
。例如,當(dāng)我們做 A B + C 這樣的矢量操作時(shí),先要等待 A B 操作完成,并保存數(shù)據(jù)在一個(gè)臨時(shí)矢量中,然后再將這個(gè)新的矢量和 C 相加。
編譯器
讓你的代碼運(yùn)行更快的最簡(jiǎn)單的辦法就是讓它做更少的工作。編譯器把代碼編譯成機(jī)器碼,是提高性能的關(guān)鍵組成部分。
- Cython ——這是編譯成C最通用的工具,覆蓋了Numpy和普通的Python代碼(需要一些C語言的知識(shí))。
- Shed Skin —— 一個(gè)用于非Numpy代碼的,自動(dòng)把Python轉(zhuǎn)換成C的轉(zhuǎn)換器。
- Numba —— 一個(gè)專用于Numpy代碼的新編譯器。
- Pythran —— 一個(gè)用于Numpy和非numpy代碼的新編譯器。
- PyPy —— 一個(gè)用于非Numpy代碼的,取代常規(guī)Python可執(zhí)行程序的穩(wěn)定的即時(shí)編譯器。
? | ? |
7.6.1?使用Cython編譯純Python版本開始寫一個(gè)擴(kuò)展編譯模塊的簡(jiǎn)單方法涉及?3?個(gè)文件。使用我們的?Julia?作為例子, 它們是:
|
? |
? | ? |
如果你的?CPU?密集型代碼在頻繁解引用的循環(huán)中,嘗試禁止邊界檢 查和外圍檢查。
? | ? |
一般情況下,可能最消耗?CPU?時(shí)間的代碼行是下面這些:??在緊湊的內(nèi)循環(huán)內(nèi)。
|
? |
cdef 關(guān)鍵字 | ? |
? | ? |
我們?cè)趫D?7-4?中可以看到?while?循環(huán)還是相當(dāng)耗時(shí)的(黃色部分)。耗時(shí)的調(diào)用在 于?Python?對(duì)復(fù)數(shù)?z?的? abs?函數(shù) 中。Cython?沒有對(duì)復(fù)數(shù)提供原生的?abs?函數(shù)。作 為替代,我們可以提供自己的本地?cái)U(kuò)展。 |
? |
這個(gè)改變有巨大的效果——通過減少在最內(nèi)循環(huán)中?Python?調(diào)用的次數(shù),我們大大 降低了函數(shù)的運(yùn)算時(shí)間。這個(gè)新版本只用?0.25?秒就執(zhí)行完畢了,具有超過原版本40?倍的速度提升,令人驚嘆。 |
? |
?
?
?
?
密集型任務(wù)
-
I/O 密集型:異步編程?
- Gevent
- Tornado
- Asyncio
-
CPU 密集型:多核 CPU 進(jìn)行多進(jìn)程
-
Multiprocessing
- multiprocessing.Pool
-
內(nèi)存共享
- multiprocessing.Manager()
- redis等中間件
- mmap
-
Multiprocessing
集群與現(xiàn)場(chǎng)教訓(xùn)
-
集群帶來的問題:
- 機(jī)器間信息同步的延遲
- 機(jī)器間配置與性能的差異
- 機(jī)器的損耗與維護(hù)
- 其它難以預(yù)料的問題
-
集群化解決方案:
- Parallel Python
- IPython Parallel?
- NSQ
?
?
Python貓薦書系列之五:Python高性能編程
Python貓
稍微關(guān)心編程語言的使用趨勢(shì)的人都知道,最近幾年,國內(nèi)最火的兩種語言非 Python 與 Go 莫屬,于是,隔三差五就會(huì)有人問:這兩種語言誰更厲害/好找工作/高工資……
對(duì)于編程語言的爭(zhēng)論,就是猿界的生理周期,每個(gè)月都要鬧上一回。到了年末,各類榜單也是特別抓人眼球,鬧得更兇。
其實(shí),它們各有對(duì)方所無法比擬的優(yōu)勢(shì)以及用武之地,很多爭(zhēng)論都是沒有必要的。身為一個(gè)正在努力學(xué)習(xí) Python 的(準(zhǔn))中年程序員,我覺得吧,先把一門語言精進(jìn)了再說。沒有差勁的語言,只有差勁的程序員,等真的把語言學(xué)好了,必定是“山重水復(fù)疑無路,柳暗花明又一村”。
鋪墊已了,進(jìn)入今天的正題,Python 貓薦書系列之五——
Python高性能編程
?
本書適合已入門 Python、還想要進(jìn)階和提高的讀者閱讀。
所有計(jì)算機(jī)語言說到底都是在硬件層面的數(shù)據(jù)操作,所以高性能編程的一個(gè)終極目標(biāo)可以說是“高性能硬件編程”。然而,Python 是一門高度抽象的計(jì)算機(jī)語言,它的一大優(yōu)勢(shì)是開發(fā)團(tuán)隊(duì)的高效,不可否認(rèn)地存在這樣或那樣的設(shè)計(jì)缺陷,以及由于開發(fā)者的水平而造成的人為的性能缺陷。
本書的一大目的就是通過介紹各種模塊和原理,來促成在快速開發(fā) Python 的同時(shí)避免很多性能局限,既減低開發(fā)及維護(hù)成本,又收獲系統(tǒng)的高效。
1、性能分析是基礎(chǔ)
首先的一個(gè)關(guān)鍵就是性能分析,借此可以找到性能的瓶頸,使得性能調(diào)優(yōu)做到事半功倍。
性能調(diào)優(yōu)能夠讓你的代碼能夠跑得“足夠快”以及“足夠瘦”。性能分析能夠讓你用最小的代價(jià)做出最實(shí)用的決定。
書中介紹了幾種性能分析的工具:
(1)基本技術(shù)如 IPython 的 %timeit 魔法函數(shù)、time.time()、以及一個(gè)計(jì)時(shí)修飾器,使用這些技術(shù)來了解語句和函數(shù)的行為。
(2)內(nèi)置工具如 cProfile,了解代碼中哪些函數(shù)耗時(shí)最長,并用 runsnake 進(jìn)行可視化。
(3)line_profiler 工具,對(duì)選定的函數(shù)進(jìn)行逐行分析,其結(jié)果包含每行被調(diào)用的次數(shù)以及每行花費(fèi)的時(shí)間百分比。
(4)memory_profiler 工具,以圖的形式展示RAM的使用情況隨時(shí)間的變化,解釋為什么某個(gè)函數(shù)占用了比預(yù)期更多的 RAM。
(5)Guppy 項(xiàng)目的 heapy 工具,查看 Python 堆中對(duì)象的數(shù)量以及每個(gè)對(duì)象的大小,這對(duì)于消滅奇怪的內(nèi)存泄漏特別有用。
(6)dowser 工具,通過Web瀏覽器界面審查一個(gè)持續(xù)運(yùn)行的進(jìn)程中的實(shí)時(shí)對(duì)象。
(7)dis 模塊,查看 CPython 的字節(jié)碼,了解基于棧的 Python 虛擬機(jī)如何運(yùn)行。
(8)單元測(cè)試,在性能分析時(shí)要避免由優(yōu)化手段帶來的破壞性后果。
作者強(qiáng)調(diào)了性能分析的重要性,同時(shí)也對(duì)如何確保性能分析的成功提了醒,例如, 將測(cè)試代碼與主體代碼分離、避免硬件條件的干擾(如在BIOS上禁用了TurboBoost、禁用了操作系統(tǒng)改寫SpeedStep、只使用主電源等) 、 運(yùn)行實(shí)驗(yàn)時(shí)禁用后臺(tái)工具如備份和Dropbox、多次實(shí)驗(yàn)、重啟并重跑實(shí)驗(yàn)來二次驗(yàn)證結(jié)果 ,等等。
性能分析對(duì)于高性能編程的作用,就好比復(fù)雜度分析對(duì)于算法的作用,它本身不是高性能編程的一部分,但卻是最終有效的一種評(píng)判標(biāo)準(zhǔn)。
2、數(shù)據(jù)結(jié)構(gòu)的影響
高性能編程最重要的事情是了解數(shù)據(jù)結(jié)構(gòu)所能提供的性能保證。
高性能編程的很大一部分是了解你查詢數(shù)據(jù)的方式,并選擇一個(gè)能夠迅速響應(yīng)這個(gè)查詢的數(shù)據(jù)結(jié)構(gòu)。
書中主要分析了 4 種數(shù)據(jù)結(jié)構(gòu):列表和元組就類似于其它編程語言的數(shù)組,主要用于存儲(chǔ)具有內(nèi)在次序的數(shù)據(jù);而字典和集合就類似其它編程語言的哈希表/散列集,主要用于存儲(chǔ)無序的數(shù)據(jù)。
本書在介紹相關(guān)內(nèi)容的時(shí)候很克制,所介紹的都是些影響“速度更快、開銷更低”的內(nèi)容,例如:內(nèi)置的 Tim 排序算法、列表的 resize 操作帶來的超額分配的開銷、元組的內(nèi)存滯留(intern機(jī)制)帶來的資源優(yōu)化、散列函數(shù)與嗅探函數(shù)的工作原理、散列碰撞帶來的麻煩與應(yīng)對(duì)、Python 命名空間的管理,等等。
?
散列碰撞的結(jié)果
理解了這些內(nèi)容,就能更加了解在什么情況下使用什么數(shù)據(jù)結(jié)構(gòu),以及如何優(yōu)化這些數(shù)據(jù)結(jié)構(gòu)的性能。
另外,關(guān)于這 4 種數(shù)據(jù)結(jié)構(gòu),書中還得出了一些有趣的結(jié)論:對(duì)于一個(gè)擁有100 000 000個(gè)元素的大列表,實(shí)際分配的可能是112 500 007個(gè)元素;初始化一個(gè)列表比初始化一個(gè)元組慢5.1 倍;字典或集合默認(rèn)的最小長度是8(也就是說,即使你只保存3個(gè)值,Python仍然會(huì)分配 8 個(gè)元素)、對(duì)于有限大小的字典不存在一個(gè)最佳的散列函數(shù)。
3、矩陣和矢量計(jì)算
矢量計(jì)算是計(jì)算機(jī)工作原理不可或缺的部分,也是在芯片層次上對(duì)程序進(jìn)行加速所必須了解的部分。
然而,原生 Python 并不支持矢量操作,因?yàn)?Python 列表存儲(chǔ)的不是實(shí)際的數(shù)據(jù),而是對(duì)實(shí)際數(shù)據(jù)的引用。在矢量和矩陣操作時(shí),這種存儲(chǔ)結(jié)構(gòu)會(huì)造成極大的性能下降。比如,
grid[5][2]
?中的兩個(gè)數(shù)字其實(shí)是索引值,程序需要根據(jù)索引值進(jìn)行兩次查找,才能獲得實(shí)際的數(shù)據(jù)。
同時(shí),因?yàn)閿?shù)據(jù)被分片存儲(chǔ),我們只能分別對(duì)每一片進(jìn)行傳輸,而不是一次性傳輸整個(gè)塊,因此,內(nèi)存?zhèn)鬏數(shù)拈_銷也很大。
減少瓶頸最好的方法是讓代碼知道如何分配我們的內(nèi)存以及如何使用我們的數(shù)據(jù)進(jìn)行計(jì)算。
Numpy 能夠?qū)?shù)據(jù)連續(xù)存儲(chǔ)在內(nèi)存中并支持?jǐn)?shù)據(jù)的矢量操作,在數(shù)據(jù)處理方面,它是高性能編程的最佳解決方案之一。
Numpy 帶來性能提升的關(guān)鍵在于,它使用了高度優(yōu)化且特殊構(gòu)建的對(duì)象,取代通用的列表結(jié)構(gòu)來處理數(shù)組,由此減 少了內(nèi)存碎片 ;此外, 自動(dòng)矢量化的數(shù)學(xué)操作使得矩陣計(jì)算非常高效 。
Numpy 在矢量操作上的缺陷是一次只能處理一個(gè)操作。例如,當(dāng)我們做 A * B + C 這樣的矢量操作時(shí),先要等待 A * B 操作完成,并保存數(shù)據(jù)在一個(gè)臨時(shí)矢量中,然后再將這個(gè)新的矢量和 C 相加。
?
Numexpr 模塊可以將矢量表達(dá)式編譯成非常高效的代碼,可以 將緩存失效以及臨時(shí)變量的數(shù)量最小化 。另外,它還能利用多核 CPU 以及 Intel 芯片專用的指令集來將速度最大化。()
書中嘗試了多種優(yōu)化方法的組合,通過詳細(xì)的分析,展示了高性能編程所能帶來的提升效果。
What is NumExpr?
NumExpr is a fast numerical expression evaluator for NumPy. With it, expressions that operate on arrays (like?
In addition, its multi-threaded capabilities can make use of all your cores – which generally results in substantial performance scaling compared to NumPy. Last but not least, numexpr can make use of Intel’s VML (Vector Math Library, normally integrated in its Math Kernel Library, or MKL). This allows further acceleration of transcendent expressions. How NumExpr achieves high performanceThe main reason why NumExpr achieves better performance than NumPy is that it avoids allocating memory for intermediate results. This results in better cache utilization and reduces memory access in general. Due to this, NumExpr works best with large arrays. NumExpr parses expressions into its own op-codes that are then used by an integrated computing virtual machine. The array operands are split into small chunks that easily fit in the cache of the CPU and passed to the virtual machine. The virtual machine then applies the operations on each chunk. It’s worth noting that all temporaries and constants in the expression are also chunked. Chunks are distributed among the available cores of the CPU, resulting in highly parallelized code execution.
The result is that NumExpr can get the most of your machine computing capabilities for array-wise computations. Common speed-ups with regard to NumPy are usually between 0.95x (for very simple expressions like?
NumExpr performs best on matrices that are too large to fit in L1 CPU cache. In order to get a better idea on the different speed-ups that can be achieved on your platform, run the provided benchmarks. |
? |
? | ? |
? | ? |
4、編譯器
書中提出一個(gè)觀點(diǎn): 讓你的代碼運(yùn)行更快的最簡(jiǎn)單的辦法就是讓它做更少的工作。
編譯器把代碼編譯成機(jī)器碼,是提高性能的關(guān)鍵組成部分。
?
不同的編譯器有什么優(yōu)勢(shì)呢,它們對(duì)于性能提升會(huì)帶來多少好處呢?書中主要介紹了如下編譯工具:
- Cython ——這是編譯成C最通用的工具,覆蓋了Numpy和普通的Python代碼(需要一些C語言的知識(shí))。
- Shed Skin —— 一個(gè)用于非Numpy代碼的,自動(dòng)把Python轉(zhuǎn)換成C的轉(zhuǎn)換器。
- Numba —— 一個(gè)專用于Numpy的新編譯器。
- Pythran —— 一個(gè)用于Numpy和非numpy代碼的新編譯器。
- PyPy —— 一個(gè)用于非Numpy代碼的,取代常規(guī)Python可執(zhí)行程序的穩(wěn)定的即時(shí)編譯器。
書中分析了這幾種編譯器的工作原理、優(yōu)化范圍、以及適用場(chǎng)景等,是不錯(cuò)的入門介紹。此外,作者還提到了其它的編譯工具,如Theano、Parakeet、PyViennaCL、ViennaCL、Nuitka 與 Pyston 等,它們各有取舍,在不同領(lǐng)域提供了支撐之力。
5、密集型任務(wù)
高性能編程的一個(gè)改進(jìn)方向是提高密集型任務(wù)的處理效率,而這樣的任務(wù)無非兩大類:I/O 密集型與 CPU 密集型。
I/O 密集型任務(wù)主要是磁盤讀寫與網(wǎng)絡(luò)通信任務(wù),占用較多 I/O 時(shí)間,而對(duì) CPU 要求較少;CPU 密集型任務(wù)恰恰相反,它們要消耗較多的 CPU 時(shí)間,進(jìn)行大量的復(fù)雜的計(jì)算,例如計(jì)算圓周率與解析視頻等。
改善 I/O 密集型任務(wù)的技術(shù)是 異步編程 ?,它使得程序在 I/O 阻塞時(shí),并發(fā)執(zhí)行其它任務(wù),并 通過“事件循環(huán)”機(jī)制 來管理各項(xiàng)任務(wù)的運(yùn)行時(shí)機(jī),從而提升程序的執(zhí)行效率。
書中介紹了 三種異步編程的庫:Gevent、Tornado 和 Asyncio ,對(duì)三種模塊的區(qū)別做了較多分析。
改善 CPU 密集型任務(wù)的主要方法是 利用多核 CPU 進(jìn)行多進(jìn)程 的運(yùn)算。
Multiprocessing 模塊 基于進(jìn)程和基于線程 的并行處理, 在隊(duì)列上共享任務(wù),以及在進(jìn)程間共享數(shù)據(jù) ,是處理CPU密集型任務(wù)的重要技術(shù)。
書中沒有隱瞞它的局限性:Amdahl 定律揭示的優(yōu)化限度、適應(yīng)于單機(jī)多核而多機(jī)則有其它選擇、全局解釋鎖 GIL 的束縛、以及進(jìn)程間通信(同步數(shù)據(jù)和檢查共享數(shù)據(jù))的開銷。針對(duì)進(jìn)程間通信問題,書中還分析了多種解決方案,例如 Less Na?ve Pool、Manager、 Redis 、RawValue、MMap 等。
6、集群與現(xiàn)場(chǎng)教訓(xùn)
集群是一種多服務(wù)器運(yùn)行相同任務(wù)的結(jié)構(gòu),也就是說,集群中的各節(jié)點(diǎn)提供相同的服務(wù),其優(yōu)點(diǎn)是系統(tǒng)擴(kuò)展容易、具備容災(zāi)恢復(fù)能力。
集群需要克服的挑戰(zhàn)有:機(jī)器間信息同步的延遲、機(jī)器間配置與性能的差異、機(jī)器的損耗與維護(hù)、其它難以預(yù)料的問題。書中列舉了兩個(gè)慘痛的教訓(xùn):華爾街公司騎士資本由于軟件升級(jí)引入的錯(cuò)誤,損失4.62億美元;Skype 公司 24 小時(shí)全球中斷的嚴(yán)重事故。
書中給我們重點(diǎn)介紹了 三個(gè)集群化解決方案:Parallel Python、IPython Parallel 和 NSQ。引申也介紹了一些普遍使用的方案,如 Celery、Gearman、PyRes、SQS。
?
關(guān)于現(xiàn)場(chǎng)教訓(xùn),它們不僅僅是一些事故或者故事而已,由成功的公司所總結(jié)出來的經(jīng)驗(yàn)更是來之不易的智慧。書中單獨(dú)用一章內(nèi)容分享了六篇文章,這些文章出自幾個(gè)使用 Python 的公司/大型組織,像是Adaptive Lab、RadimRehurek、Smesh、PyPy 與 Lanyrd ,這些國外組織的一線實(shí)踐經(jīng)驗(yàn),應(yīng)該也能給國內(nèi)的 Python 社區(qū)帶來一些啟示。
7、寫在最后
眾所周知,Python 應(yīng)用前景大、簡(jiǎn)單易學(xué)、方便開發(fā)與部署,然而與其它編程語言相比,它的性能幾乎總是落于下風(fēng)。如何解決這個(gè)難題呢?本期薦書的書目就是一種回應(yīng)。
《Python高性能編程》全書從微觀到宏觀對(duì)高性能編程的方方面面做了講解,主要包含以下主題:計(jì)算機(jī)內(nèi)部結(jié)構(gòu)的背景知識(shí)、列表和元組、字典和集合、迭代器和生成器、矩陣和矢量計(jì)算、 編譯器、并發(fā)、集群和工作隊(duì)列 等。這些內(nèi)容為編寫更快的 Python 指明了答案。
本篇文章主要以梳理書中的內(nèi)容要點(diǎn)為主,平均而兼顧地理清了全書脈絡(luò)(PS:太面面俱到了,但愿不被指責(zé)為一篇流水賬的讀書筆記才好……)。我認(rèn)為,鑒于書中談及的這些話題,它就足以成為我們薦書欄目的一員了。除去某些句段的糟糕翻譯、成書時(shí)間比較早(2014年)而造成的過時(shí)外,這本書總體質(zhì)量不錯(cuò),可稱為是一份優(yōu)秀的高性能編程的指引手冊(cè)。
關(guān)于薦書欄目,我最后多說幾句。本欄目原計(jì)劃兩周左右出一篇,但由于其它系列文章花費(fèi)了我不少時(shí)間,而要寫好一篇薦書/書評(píng)也特別費(fèi)勁,最后生生造成了現(xiàn)在兩月一更的尷尬局面……這篇文章是個(gè)錯(cuò)誤的示范,我不該試圖全面通讀與概括其內(nèi)容的。因此,我決定今后選一些易讀的書目,在寫作上也盡量走短小精悍風(fēng),希望能持續(xù)地將本欄目運(yùn)作下去。若你有什么建議(如書目推薦、書評(píng)推薦、寫作建議、甚至是投稿),我隨時(shí)歡迎,先行致謝啦。
?
?
?
?
?
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
