Instagram的Python性能調優方法介紹

Instagram的Python性能调优方法介绍

Instagram 是世界上最大的 Python 應用環境之一, 在這裡,Python被用來實現服務於8億活躍用戶的“業務邏輯”。我們使用Python的參考實現——CPython——作為我們代碼的運行環境。隨著我們的發展,越來越多的服務器設備需求成為了我們基礎設施開支的重要部分。這些設備都是計算密集型的,因此我們著重關注我們編寫和部署的代碼的效率,並且著力於構建用於檢測和診斷性能瓶頸的工具。這些工具工作得很好,然而我們web層的預計增長使我們開始從運行環境中去審查我們效率低下的根源。

起步

為了確定我們需要優化的目標和應該使用的技術,我們必須確定以下兩點:

  • 從解釋器的角度看,Instagram的負載是什麼樣的。

  • 解釋器在運行我們的代碼時,究竟將時間花費在了哪裡

我們從收集CPython在執行Instagram服務器代碼時產生的各種數據開始。不出意外,從解釋器的角度看,Instagram的負載是一個面向對象的網絡服務器負載:

  • 90%的解釋器命令是在進行操作數棧的操作、控制流和屬性獲取。

  • 我們的負載不是循環性的——94%的循環在經歷四次以下的迭代之後就終止了。

  • 函數調用的花銷非常高,大約佔解釋器時間花銷的30%。

  • 屬性獲取是其次的資源密集性操作,大約佔用瞭解釋器時間花銷的28%。這些屬性負載通常不是多態的,其中85%的負載發生在單態。

基於以上數據,我們決定將工作重心放在降低函數調用和屬性獲取的計算開銷上。

數據收集方法

通過監測運行在我們實驗室環境(InstaLab)上的解釋器,我們收集了所有我們需要的數據。InstaLab將來自前五視圖中的請求混合(通過CPU指令),復現了生產環境的網絡情況,並將其應用於一臺隔離的web服務器。這一方法幫助我們得以在不影響用戶體驗的前提下,收集具有代表性的性能數據。對每一個不同的數據集,我們對CPython進行測量,搭建新的解釋器,並從InstaLab中收集相關數據。

字節碼頻率分佈

從解釋器的角度來看,Instagram就是一串待執行的字節碼指令序列。作為了解解釋器行為的第一步,我們統計了CPython的字節碼執行頻率。下圖是執行頻率排名前十的字節碼:

Instagram的Python性能调优方法介绍

如圖所示,我們可以看到LOAD_FAST指令佔據主導位置。事實上,LOAD_FAST, STORE_FAST和LOAD_CONST大約佔據了所有執行的指令碼的40%。這和預期是一樣的——CPython是一個堆棧機器,每一個指令都是在對操作數棧進行入棧和出棧操作。儘管這些命令十分簡單,但是它們執行得十分頻繁,並且產生了內存操作。因此,有效的優化技術應該要儘量避免load和store操作(比如轉換為基於寄存器的字節碼操作)。

排除上面的操作碼後,我們可以更好地理解解釋器在執行Instagram代碼時所做的“實際工作”。下圖顯示了剩餘的前20個操作碼(總共120個)的執行頻率分佈:

Instagram的Python性能调优方法介绍

這些指令佔剩餘開銷的90%。我們把它們大體分為以下幾類:

  • 屬性獲取——LOAD_ATTR, STORE_ATTR, LOAD_GLOBAL

  • 控制流——CALL_FUNCTION, RETURN_ VALUE, POP JUMP IF FALSE, COMPARE_OP, FOR_ITER, JUMP ABSOLUTE, POP JUMP IF_ TRUE, POP BLOCK, JUMP FORWARD, YIELD_ VALUE, CALL_FUNCTION_KW, SETUP_EXCEPT

  • 容器操作——BINARY_SUBSCR, BUILD_TUPLE, UNPACK_SEQUENCE

  • 操作數棧操作——POP_TOP, LOAD_DEREF

這與我們所預期的,面向對象的web服務器的負載相一致(與數值計算負載相對)。

執行字節碼的時間花費

字節碼的執行頻率只告訴了我們一半的故事。為了進一步瞭解解釋器所做的工作,我們還希望知道解釋器將時間花在了哪裡。為了這一目的,我們使用pref_event APIs 測量了執行每一個操作碼時,經過退出階段(retire)的CPU指令數量。我們在解釋器循環調度中將每個操作碼的主體括起來,分別讀取硬件指令計數器,然後將這兩個值相減,以計算執行操作碼所花費的“時間”。對於回調到解釋器或回調到C函數(例如,CALL_FUNCTION操作碼族)的情況,我們從操作碼的開銷中扣除被調用者花費的“時間”量。

下圖顯示了累計退出CPU指令佔比前10的操作碼:

Instagram的Python性能调优方法介绍

這些數據與之前的數據告訴我們的有所不同,函數調用和屬性加載跳到了花銷分佈的前列。從“時間花銷”的角度,最資源密集的操作碼分類如下:

  • 控制流-CALL_FUNCTION, CALL_FUNCTION_KW, COMPARE_OP

  • 屬性讀取-LOAD_ATTR, STORE_ATTR, LOAD_GLOBAL

  • 操作數棧操作-LOAD_FAST, STORE_FAST,LOAD_CONST

基於以上的數據,我們決定將時間花銷優化的第一步放在優化屬性獲取和降低函數調用花銷上。

加速屬性獲取

一個加速動態語言環境中屬性獲取和方法調度的經典技術是多態內聯緩存。內聯緩存與密集屬性存儲(使用類似隱藏類的方法)相結合,可以顯著加快屬性查找速度。 不過,它們的效果取決於調用位置的多態性程度。

我們通過測量CPython,記錄了每次LOAD_ATTR執行時的類型數量,以此來精確計算內聯緩存的潛力。下圖顯示了LOAD_ATTR操作時的多態性程度分佈:

Instagram的Python性能调优方法介绍

基於以上數據,可以看到內聯緩存似乎是一個加速屬性獲取十分有效的技術。大約85%的LOAD_ATTR指令發生在單態場景中,大小為4的內聯緩存足以應對大約96%的屬性加載操作。

負載循環程度

最後一個我們希望回答的問題是“我們的代碼循環程度有多高?”。回答這一問題有助於我們確定那些降低循環負載的技術能夠在多大程度上為我們的任務加速。

我們通過測量CPython記錄了每一次循環中的迭代次數。下圖展示了循環迭代次數的分佈:

Instagram的Python性能调优方法介绍

如圖所示,我們的工作負載循環程度並不高。事實上,大約94%的循環經歷了四次以下的迭代就終止了。

結論

根據這些測試,我們確定了CPython中導致我們工作負載效率低下的兩個主要根源:函數調用花銷和屬性獲取需求。我們收集到的數據顯示,一些眾所周知的技術能夠有效緩解這種效率低下。下一步,我們將會使用這些信息來指導我們努力優化CPython。

Matt PageInstagram效率與可靠性團隊的一位軟件工程師

英文原文:https://ogmcsrgk5.qnssl.com/vcdn/1/優質文章長圖2/Instagram調試CPython.pdf
譯者:九天攬月


分享到:


相關文章: