對 Java JIT 編譯之父 Cliff Click 的精彩採訪

對 Java JIT 編譯之父 Cliff Click 的精彩採訪懸崖點擊 — Cratus(用於流程改進的物聯網感測器)的首席技術官,多家新創公司(包括 Rocket Realtime School、Neurensic 和 H2O.ai)的創始人和聯合創始人,並多次成功退出。 Cliff 在 15 歲時寫了他的第一個編譯器(Pascal for the TRS Z-80)! 他最出名的是他在 Java 中的 C2(節點之海 IR)方面的工作。 這個編譯器向世界展示了 JIT 可以產生高品質的程式碼,這是 Java 成為主要現代軟體平台之一的因素之一。 隨後 Cliff 協助 Azul Systems 使用純 Java 軟體建置了一個 864 核心大型主機,支援 500 毫秒內在 10 GB 堆上進行 GC 暫停。 總的來說,Cliff 成功地研究了 JVM 的各個方面。

 
這篇 habrapost 是對 Cliff 的精彩訪談。 我們將討論以下主題:

  • 過渡到低階優化
  • 如何進行大重構
  • 成本模型
  • 低階優化訓練
  • 績效改善的實際例子
  • 為什麼要創建自己的程式語言
  • 性能工程師職業生涯
  • 技術挑戰
  • 關於暫存器分配和多核心的一些知識
  • 人生最大的挑戰

面試由以下人員進行:

  • 安德烈·薩塔林 來自亞馬遜網路服務。 在他的職業生涯中,他成功地從事過完全不同的專案:他測試了Yandex 中的NewSQL 分散式資料庫、卡巴斯基實驗室中的雲端偵測系統、Mail.ru 中的多人遊戲以及德意志銀行中計算外匯價格的服務。 對測試大規模後端和分散式系統感興趣。
  • 弗拉基米爾·西特尼科夫 來自網路駭客。 十年來致力於 NetCracker OS 的效能和可擴展性,該軟體是電信營運商用來自動化網路和網路設備管理流程的軟體。 對 Java 和 Oracle 資料庫效能問題感興趣。 官方 PostgreSQL JDBC 驅動程式中十幾項效能改進的作者。

過渡到低階優化

安德魯:您是 JIT 編譯、Java 和一般效能工作領域的知名人士,對嗎? 

懸崖: 就像那樣!

安德魯:讓我們從一些有關績效工作的一般性問題開始。 您如何看待進階最佳化和低階最佳化(例如在 CPU 層級工作)之間的選擇?

懸崖: 是的,這裡一切都很簡單。 最快的程式碼是永遠不會運行的程式碼。 因此,你總是需要從高層次開始,研究演算法。 更好的 O 表示法將擊敗更差的 O 表示法,除非有一些足夠大的常數介入。 低級的事情放在最後。 通常,如果您已經充分優化了堆疊的其餘部分,並且仍然剩下一些有趣的東西,那麼這就是一個低水平。 但如何從高層次開始呢? 您如何知道已經完成了足夠的高水準工作? 嗯……沒辦法。 沒有現成的食譜。 您需要了解問題,決定要做什麼(以免將來採取不必要的步驟),然後您可以發現探查器,它可以說出一些有用的信息。 在某些時候,你自己意識到你已經擺脫了不必要的東西,是時候進行一些低階的微調了。 這絕對是一種特殊的藝術。 有很多人在做不必要的事情,但行動如此之快,以至於他們沒有時間擔心生產力。 但這是在問題直接出現之前。 通常99%的時間沒人關心我在做什麼,直到一件重要的事情出現在沒人關心的關鍵路徑上的那一刻。 在這裡,每個人都開始向你嘮叨「為什麼它從一開始就不完美」。 一般來說,性能總是有需要改進的地方。 但 99% 的情況你都沒有線索! 你只是想把事情做好,在這個過程中你會發現什麼是重要的。 你永遠不可能提前知道這件作品需要完美,所以,事實上,你必須在一切方面都做到完美。 但這是不可能的,你不這麼做。 總是有很多事情需要解決——這是完全正常的。

如何進行大重構

安德魯: 你是如何進行表演的? 這是一個交叉問題。 例如,您是否曾經必須解決因許多現有功能的交叉而產生的問題?

懸崖: 我盡量避免。 如果我知道效能會成為一個問題,我會在開始編碼之前考慮它,尤其是資料結構。 但通常你很晚才發現這一切。 然後你必須採取極端措施,做我所說的「重寫和征服」:你需要抓住足夠大的一塊。 由於效能問題或其他原因,一些程式碼仍然需要重寫。 無論重寫程式碼的原因是什麼,重寫較大的程式碼幾乎總是比重寫較小的程式碼更好。 這一刻,每個人都開始害怕地發抖:“天哪,你不能碰這麼多代碼!” 但事實上,這種方法幾乎總是效果更好。 你需要立即解決一個大問題,在它周圍畫一個大圓圈並說:我將重寫圓圈內的所有內容。 邊框比其內部需要替換的內容小得多。 如果這樣的界線劃分可以讓你完美地完成裡面的工作,那麼你的雙手就得到了自由,可以做你想做的事。 一旦你了解問題,重寫過程就會容易得多,所以咬緊牙關吧!
同時,當您進行大量重寫並意識到效能將成為一個問題時,您可以立即開始擔心它。 這通常會變成簡單的事情,例如「不要複製數據,盡可能簡單地管理數據,使其變小」。 在大型重寫中,有一些標準方法可以提高效能。 它們幾乎總是圍繞著數據。

成本模型

安德魯:在一個播客中,您談到了生產力背景下的成本模型。 你能解釋一下你這是什麼意思嗎?

懸崖: 當然。 我出生在一個處理器效能極為重要的時代。 而這個時代再次回歸——命運不無諷刺。 我開始生活在 256 位元機器的時代;我的第一台電腦使用 XNUMX 位元組。 確切地說是位元組。 一切都非常小。 必須對指令進行計數,並且隨著我們開始在程式語言堆疊中向上移動,語言的數量也越來越多。 先是組譯器,然後是 Basic,然後是 C,C 負責處理很多細節,例如暫存器分配和指令選擇。 但那裡的一切都很清楚,如果我創建一個指向變數實例的指針,那麼我就會得到負載,並且該指令的成本是已知的。 硬體產生一定數量的機器週期,因此只需將要執行的所有指令相加即可計算出不同事物的執行速度。 每個比較/測試/分支/呼叫/載入/儲存都可以相加並表示:這就是您的執行時間。 在致力於提高性能時,您肯定會注意哪些數字對應於小熱循環。 
但一旦你轉向 Java、Python 和類似的東西,你很快就會脫離低階硬體。 在 Java 中呼叫 getter 的成本是多少? HotSpot 中的 JIT 是否正確 內聯的,它會加載,但如果它沒有這樣做,它將是一個函數呼叫。 由於呼叫處於熱循環中,因此它將覆蓋該循環中的所有其他最佳化。 因此,實際成本會高很多。 您立即無法查看一段程式碼並了解我們應該根據所使用的處理器時脈速度、記憶體和快取來執行它。 只有真正投入表演中,這一切才會變得有趣。
現在我們發現自己處於處理器速度十年來幾乎沒有提高的境地。 舊時光又回來了! 您不能再指望良好的單線程效能。 但如果你突然進入並行計算,那就非常困難了,每個人都像詹姆斯龐德一樣看著你。 這裡的十倍加速通常發生在有人搞砸了事情的地方。 並發需要做很多工作。 要獲得 XNUMX 倍的加速,您需要了解成本模型。 費用是多少? 為此,您需要了解舌頭如何安裝在底層硬體上。
馬丁湯普森為他的部落格選擇了一個很棒的詞 機械同情! 您需要了解硬體將要做什麼、它到底如何做以及為什麼它首先要做它所做的事情。 使用它,可以很容易地開始計算指令數併計算出執行時間的去向。 如果你沒有接受過適當的培訓,你只是在黑暗的房間裡尋找一隻黑貓。 我看到人們一直在優化效能,但他們不知道自己到底在做什麼。 他們遭受了許多痛苦,卻沒有取得太多進展。 當我使用同一段程式碼,加入一些小技巧並獲得五倍或十倍的加速時,他們會說:好吧,這不公平,我們已經知道你更好了。 驚人的。 我在說什麼......成本模型是關於您編寫的程式碼類型以及它在總體上的平均運行速度。

安德魯:你怎麼能在腦子裡記住這麼大的容量呢? 這是透過更多的經驗實現的,還是? 這樣的經驗從何而來?

懸崖:嗯,我沒有以最簡單的方式獲得經驗。 在你可以理解每一指令的時代,我用組合語言進行程式設計。 這聽起來很愚蠢,但從那時起,Z80指令集就一直留在我的腦海裡、我的記憶裡。 我一說話就記不住別人的名字,但我記得 40 年前寫的程式碼。 很有趣,看起來像是一種綜合症”白痴科學家“。

低階優化訓練

安德魯: 有沒有更方便的方法?

懸崖: 是也不是。 隨著時間的推移,我們使用的硬體並沒有太大變化。 除了 Arm 智慧型手機之外,每個人都使用 x86。 如果您沒有進行某種硬核嵌入,那麼您就會做同樣的事情。 好的,接下來。 這些說明幾個世紀以來也沒有改變。 你需要去彙編中寫一些東西。 雖然不多,但足以開始理解。 你在微笑,但我卻在認真地說。 你需要了解語言和硬體之間的對應關係。 之後,您需要編寫一些程式碼,為一種小玩具語言製作一個小玩具編譯器。 像玩具一樣意味著它需要在合理的時間內製作完成。 它可以非常簡單,但它必須產生指令。 產生指令的行為將幫助您了解每個人編寫的高級程式碼與在硬體上運行的機器碼之間的橋樑的成本模型。 這種對應關係會在編譯器寫的時候就烙進大腦。 即使是最簡單的編譯器。 之後,您可以開始研究 Java,事實上它的語義鴻溝要深得多,並且在其上架起橋樑要困難得多。 在 Java 中,要理解我們的橋是好還是壞、什麼會導致它崩潰、什麼不會導致它崩潰要困難得多。 但是您需要某種起點,讓您查看程式碼並理解:“是的,這個 getter 每次都應該內聯。” 然後事實證明,有時會發生這種情況,除了方法變得太大並且 JIT 開始內聯所有內容的情況。 這些地方的表現可以立即預測。 通常 getter 工作得很好,但是當你查看大型熱循環時,你會意識到那裡有一些函數調用,它們不知道它們在做什麼。 這就是 getter 廣泛使用的問題,之所以沒有內聯,是因為不清楚它們是否是 getter。 如果你的程式碼庫非常小,你可以簡單地記住它,然後說:這是一個 getter,這是一個 setter。 在大型程式碼庫中,每個函數都有自己的歷史,一般來說,任何人都不知道。 探查器顯示,我們在某個循環上損失了 24% 的時間,要了解該循環在做什麼,我們需要查看內部的每個函數。 如果不研究函數就不可能理解這一點,這嚴重減慢了理解的進程。 這就是為什麼我不使用 getter 和 setter,我已經達到了一個新的水平!
哪裡可以獲得成本模型? 嗯,當然,你可以讀點東西……但我認為最好的方法是採取行動。 製作一個小型編譯器將是理解成本模型並將其融入您自己的頭腦的最佳方式。 一個適合微波爐程式設計的小型編譯器是初學者的任務。 嗯,我的意思是,如果您已經具備程式設計技能,那麼這就足夠了。 所有這些事情,例如將您擁有的字串解析為某種代數表達式,以正確的順序從那裡提取數學運算指令,從寄存器中獲取正確的值 - 所有這些都是一次性完成的。 當你這樣做的時候,它就會印在你的大腦裡。 我想大家都知道編譯器的作用。 這將有助於理解成本模型。

績效改善的實際例子

安德魯:在提高生產力的同時,還需要注意什麼?

懸崖: 資料結構。 順便說一句,是的,我已經很久沒有教過這些課程了… 火箭學校。 很有趣,但是需要付出很多努力,而且我也有生活! 好的。 因此,在一門大而有趣的課程「你的表現在哪裡」中,我給學生們舉了一個例子:從 CSV 檔案中讀取了 70 GB 的金融科技數據,然後他們必須計算銷售的產品數量。 定期報價市場數據。 自 1 年代以來,UDP 封包轉換為文字格式。 芝加哥商業交易所——各種各樣的東西,例如奶油、玉米、大豆等等。 需要統計這些產品、交易筆數、資金和貨物的平均流動量等。 這是非常簡單的交易數學:找到產品代碼(哈希表中的 2-XNUMX 個字元),獲取金額,將其添加到交易集之一,增加交易量,增加價值,以及其他一些事情。 非常簡單的數學。 這個玩具實作非常簡單:所有內容都在一個文件中,我讀取該文件並瀏覽它,將各個記錄劃分為 Java 字串,在其中查找必要的內容並根據上述數學將它們相加。 而且它在低速下工作。

透過這種方法,發生的事情很明顯,並行計算也無濟於事,對嗎? 事實證明,只需選擇正確的資料結構即可將效能提高五倍。 這甚至讓經驗豐富的程式設計師感到驚訝! 在我的特定情況下,訣竅是您不應該在熱循環中進行記憶體分配。 好吧,這不是全部事實,但總的來說 - 當 X 足夠大時,您不應該突出顯示“once in X”。 當 X 是兩個半千兆位元組時,您不應該分配任何「每個字母一次」、「每行一次」或「每個欄位一次」或類似的內容。 這就是時間花費的地方。 這是如何運作的? 想像我打電話 String.split()BufferedReader.readLine(). Readline 從透過網路傳輸的一組位元組產生一個字串,對於數億行中的每一行,每行一次。 我拿起這條線,解析它並把它扔掉。 為什麼我要把它扔掉——好吧,我已經處理過了,僅此而已。 因此,對於從這 2.7G 讀取的每個字節,將在該行中寫入兩個字符,即已經是 5.4G,並且我不再需要它們,因此將它們丟棄。 如果你看一下記憶體頻寬,我們會載入2.7G,透過處理器中的記憶體和記憶體匯流排,然後將兩倍的記憶體傳送到位於記憶體中的線路,並且在建立每個新線路時,所有這些都會磨損。 但我需要讀取它,硬體讀取它,即使後來一切都磨損了。 我必須把它寫下來,因為我創建了一行並且快取已滿 - 快取無法容納 2.7G。 因此,對於我讀取的每個字節,我會再讀取兩個位元組並再寫入兩個字節,最終它們的比率為 4:1 - 在這個比率中,我們浪費了記憶體頻寬。 然後事實證明如果我這樣做 String.split() – 這不是我最後一次這樣做,裡面可能還有另外 6-7 個欄位。 因此,讀取 CSV 然後解析字串的經典程式碼會導致記憶體頻寬浪費,與您實際想要的相比約為 14:1。 如果你放棄這些選擇,你可以獲得五倍的加速。

這並不那麼困難。 如果你從正確的角度看程式碼,一旦你意識到問題,一切都會變得非常簡單。 你不應該完全停止分配記憶體:唯一的問題是你分配了一些東西,它會立即死亡,並且在此過程中它會消耗重要的資源,在本例中是記憶體頻寬。 所有這些都會導致生產力下降。 在 x86 上,您通常需要主動消耗處理器週期,但在這裡您會更早消耗掉所有記憶體。 解決辦法是減少排放量。 
問題的另一部分是,如果您在記憶體條帶耗盡時運行探查器,那麼您通常會等待快取返回,因為它充滿了您剛剛產生的垃圾,所有這些行。 因此,每個載入或儲存操作都會變得很慢,因為它們會導致快取未命中 - 整個快取變得很慢,等待垃圾離開它。 因此,分析器只會顯示整個循環中塗抹的溫暖隨機噪音 - 程式碼中不會有單獨的熱指令或位置。 只有噪音。 如果您查看 GC 週期,您會發現它們都是年輕代並且超快 - 最多微秒或毫秒。 畢竟,所有這些記憶都會立即消失。 你分配了數十億字節,他把它們削減、削減、再削減。 這一切發生得非常快。 事實證明,GC 週期很便宜,整個週期都有溫暖的噪音,但我們希望獲得 5 倍的加速。 這時,你的腦海中應該有什麼東西關閉並響起:“這是為什麼?!” 經典偵錯器中不會顯示記憶體條溢位;您需要執行硬體效能計數器偵錯器並親自直接查看。 但這並不能從這三個症狀直接懷疑。 第三個症狀是,當您查看突出顯示的內容時,請詢問分析器,他回答:“您創建了 XNUMX 億行,但 GC 是免費工作的。” 一旦發生這種情況,您就會意識到您創建了太多物件並燒毀了整個記憶體通道。 有一種方法可以解決這個問題,但並不明顯。 

問題出在資料結構:所有發生的事情背後的裸結構,它太大了,在磁碟上有2.7G,所以製作這個東西的副本是非常不可取的——你想立即從網絡字節緩衝區載入它寫入暫存器,以免對該行來回讀寫五次。 不幸的是,預設情況下,Java 並沒有提供您這樣的程式庫作為 JDK 的一部分。 但這是微不足道的,對吧? 本質上,這些 5-10 行程式碼將用於實現您自己的緩衝字串載入器,它重複字串類別的行為,同時作為底層位元組緩衝區的包裝器。 結果,事實證明,您幾乎就像處理字串一樣,但實際上指向緩衝區的指標正在移動到那裡,原始位元組不會複製到任何地方,因此相同的緩衝區會一遍又一遍地重複使用,並且作業系統很樂意承擔其設計目的,例如這些位元組緩衝區的隱藏雙緩衝,並且您不再需要處理無休止的不必要資料流。 順便問一下,您是否明白,在使用 GC 時,可以保證在最後一個 GC 週期之後每次記憶體分配對處理器來說都是不可見的? 因此,所有這些都不可能在快取中,然後就會發生 100% 保證未命中的情況。 當使用指標時,在 x86 上,從記憶體中減去暫存器需要 1-2 個時脈週期,一旦發生這種情況,你就得付費,付費,付費,因為記憶體全部開啟了 九個緩存 – 這就是記憶體分配的成本。 真正的價值。

換句話說,資料結構是最難改變的。 一旦你意識到你選擇了錯誤的資料結構,這會在以後影響效能,通常還有很多工作要做,但如果你不這樣做,事情會變得更糟。 首先,你需要考慮資料結構,這很重要。 這裡的主要成本落在胖資料結構上,它們開始以「我將資料結構 X 複製到資料結構 Y 中,因為我更喜歡 Y 的形狀」的方式使用。 但複製操作(看起來很便宜)實際上浪費了記憶體頻寬,這就是所有浪費的執行時間被埋葬的地方。 如果我有一個巨大的JSON 字串,並且想將其轉換為POJO 的結構化DOM 樹或其他內容,則解析該字串並構建POJO,然後稍後再次訪問POJO 的操作將導致不必要的成本-這是不便宜。 除非您在 POJO 上運行的次數比在字串上運行的次數多得多。 您可以暫時嘗試解密該字串並僅從其中提取您需要的內容,而不將其轉換為任何 POJO。 如果這一切都發生在需要最大效能的路徑上,沒有 POJO 適合您,您需要以某種方式直接深入該行。

為什麼要創建自己的程式語言

安德魯:你說為了理解成本模型,你需要寫自己的小語言......

懸崖:不是一種語言,而是一個編譯器。 語言和編譯器是兩個不同的東西。 最重要的差別在於你的頭腦。 

安德魯:順便說一句,據我所知,您正在嘗試創建自己的語言。 為了什麼?

懸崖: 因為我可以! 我處於半退休狀態,所以這是我的愛好。 我一生都在實現別人的語言。 我還在我的編碼風格上做了很多工作。 還因為我看到其他語言的問題。 我發現有更好的方法來做熟悉的事情。 我會使用它們。 我只是厭倦了在自己、Java、Python、任何其他語言中看到問題。 我現在用 React Native、JavaScript 和 Elm 寫作作為一種愛好,這不是為了退休,而是為了積極的工作。 我還使用 Python 進行編寫,並且很可能會繼續從事 Java 後端的機器學習工作。 有許多流行的語言,它們都有有趣的功能。 每個人都有自己的優點,你可以嘗試將所有這些功能結合在一起。 所以,我正在研究我感興趣的東西,語言的行為,試圖提出合理的語義。 到目前為止我成功了! 目前我正在努力解決記憶體語義問題,因為我希望像 C 和 Java 一樣擁有它,並獲得強大的記憶體模型以及用於載入和儲存的記憶體語義。 同時,具有像 Haskell 中那樣的自動類型推斷。 在這裡,我嘗試將類似 Haskell 的類型推斷與 C 和 Java 中的記憶體工作混合。 例如,這就是我過去 2-3 個月一直在做的事情。

安德魯:如果你建構一種吸收其他語言更好的方面的語言,你認為有人會做相反的事情:採納你的想法並使用它們嗎?

懸崖:新語言就是這樣出現的! 為什麼Java與C相似? 因為C有一個很好的語法,每個人都能理解,而Java受到了這個語法的啟發,添加了類型安全、數組邊界檢查、GC,他們還改進了C的一些東西。他們添加了自己的東西。 但他們受到了很多啟發,對嗎? 每個人都站在前人的肩膀上——這就是進步的方式。

安德魯:據我了解,您的語言將是記憶體安全的。 您是否考慮過實作 Rust 的借用檢查器之類的東西? 你看過他嗎,你對他有什麼看法?

懸崖:嗯,我已經編寫 C 語言很多年了,使用所有這些 malloc 和 free,並手動管理生命週期。 要知道,90-95%的手動控制壽命都具有相同的結構。 而且手動操作非常非常痛苦。 我希望編譯器簡單地告訴您那裡發生了什麼以及您透過操作取得了什麼成果。 對於某些事情,借用檢查器可以開箱即用地執行此操作。 它應該自動顯示訊息,理解一切,甚至不會給我帶來呈現這種理解的負擔。 它必須至少進行本地轉義分析,並且只有在失敗時,才需要添加描述生命週期的類型註釋 - 這種方案比借用檢查器或實際上任何現有的內存檢查器要復雜得多。 「一切都很好」和「我什麼都不懂」之間的選擇——不,一定有更好的東西。 
因此,作為一個用 C 編寫了大量程式碼的人,我認為支援自動生命週期控制是最重要的。 我也厭倦了 Java 使用多少內存,主要抱怨的是 GC。 當你在Java中分配記憶體時,你不會取回上一次GC週期時本地的記憶體。 在記憶體管理更精確的語言中並非如此。 如果呼叫 malloc,您會立即獲得通常剛剛使用的記憶體。 通常你會用記憶做一些臨時的事情,然後立即將其返回。 並且它立即返回malloc池,下一個malloc循環又將其拉出。 因此,實際記憶體使用量會減少為給定時間的活動物件集加上洩漏。 如果一切都沒有以完全不雅的方式洩漏,那麼大部分記憶體最終都會進入快取和處理器,並且運行速度很快。 但需要在正確的位置以正確的順序調用 malloc 和 free 進行大量手動記憶體管理。 Rust 可以自行正確處理這個問題,並且在許多情況下可以提供更好的效能,因為記憶體消耗縮小到僅當前計算 - 而不是等待下一個 GC 週期來釋放記憶體。 結果,我們得到了一種非常有趣的方法來提高性能。 而且相當強大——我的意思是,我在處理金融科技數據時做了這樣的事情,這讓我獲得了大約五倍的加速。 這是一個相當大的提升,特別是在處理器沒有變得更快並且我們仍在等待改進的世界中。

性能工程師職業生涯

安德魯: 我也想問一般的職業。 您憑藉在 HotSpot 的 JIT 工作而嶄露頭角,然後轉到 Azul,這也是一家 JVM 公司。 但我們在硬體上的投入已經多於軟體。 然後他們突然轉向大數據和機器學習,然後轉向詐欺偵測。 這怎麼發生的? 這些都是非常不同的發展領域。

懸崖:我已經編程很長時間了,並且已經成功地參加了很多不同的課程。 當人們說:「哦,你是為 Java 進行 JIT 的人!」時,這總是很有趣。 但在此之前,我正在研究 PostScript 的克隆——Apple 曾經將這種語言用於其雷射印表機。 在此之前我做了一個 Forth 語言的實作。 我認為對我來說共同的主題是工具開發。 我一生都在製作工具,讓其他人可以編寫很酷的程式。 但我也參與了作業系統、驅動程式、核心級偵錯器、作業系統開發語言的開發,這些一開始很簡單,但隨著時間的推移變得越來越複雜。 但主要話題仍然是工具的開發。 我生命中的很大一部分時間是在 Azul 和 Sun 之間度過的,而且都是關於 Java 的。 但當我進入大數據和機器學習領域時,我重新戴上我的奇特帽子並說道:「哦,現在我們遇到了一個不平凡的問題,並且有很多有趣的事情正在發生,人們也在做事情。” 這是一條偉大的發展之路。

是的,我真的很喜歡分散式運算。 我的第一份工作是 C 專業的學生,從事廣告專案。 這是 Zilog Z80 晶片上的分散式運算,收集由真實模擬分析儀產生的模擬 OCR 資料。 這是一個很酷且完全瘋狂的話題。 但是存在問題,某些部分沒有被正確識別,所以你必須拿出一張圖片並將其展示給一個已經可以用眼睛閱讀並報告其內容的人,因此出現了帶有數據的工作,這些工作有自己的語言。 有一個後端處理所有這些 - Z80s 與 vt100 終端並行運行 - 每人一個,並且 Z80 上有一個並行編程模型。 星型配置中所有 Z80 共享的一些通用記憶體; 背板也是共享的,一半的 RAM 在網路內共享,另一半是私有的或用於其他用途。 具有共享...半共享記憶體的有意義的複雜平行分散式系統。 這是什麼時候的事……我甚至記不清了,大概是八十年代中期吧。 很久以前了。 
是的,假設30年已經是很久以前的事了,分散式運算的問題已經存在了很長一段時間,人們長期處於戰爭狀態。 貝奧武夫-簇。 這樣的集群看起來像......例如:有以太網,你的快速x86連接到這個以太網,現在你想要獲得假共享內存,因為當時沒有人可以做分佈式計算編碼,這太困難了,因此有是x86 上帶有保護內存頁面的假共享內存,如果您寫入此頁面,那麼我們告訴其他處理器,如果它們訪問相同的共享內存,則需要從您那裡加載它,因此類似於支援協定快取一致性和相關軟體出現了。 有趣的概念。 當然,真正的問題是別的。 所有這些都有效,但您很快就遇到了性能問題,因為沒有人在足夠好的級別上理解性能模型 - 存在哪些內存訪問模式,如何確保節點不會無休止地相互 ping,等等。

我在 H2O 中想到的是,開發人員自己負責確定並行性在哪裡隱藏和不隱藏。 我提出了一種編碼模型,使編寫高效能程式碼變得輕鬆簡單。 但編寫運行緩慢的程式碼很困難,看起來很糟糕。 你需要認真嘗試編寫緩慢的程式碼,你將不得不使用非標準的方法。 煞車代碼一目了然。 因此,您通常會編寫運行速度很快的程式碼,但您必須弄清楚在共享記憶體的情況下該怎麼做。 所有這些都與大型數組相關,其行為類似於並行 Java 中的非揮發性大型數組。 我的意思是,假設兩個線程寫入一個並行數組,其中一個線程獲勝,而另一個線程相應地失敗,並且您不知道哪個線程是哪個線程。 如果它們不是不穩定的,那麼順序可以是任何你想要的 - 這非常有效。 人們真正關心操作的順序,他們將易失性放在正確的位置,並且他們期望在正確的位置出現與記憶體相關的效能問題。 否則,他們會簡單地以從 1 到 N 的循環形式編寫程式碼,其中 N 約為數萬億,希望所有複雜的情況都會自動變得並行 - 但這在那裡行不通。 但在 H2O 中,這既不是 Java 也不是 Scala;如果您願意,您可以將其視為「Java minus minus」。 這是一種非常清晰的程式設計風格,類似於使用循環和陣列編寫簡單的 C 或 Java 程式碼。 但同時,記憶體可以以 TB 為單位進行處理。 我仍然使用H2O。 我不時在不同的項目中使用它——它仍然是最快的東西,比競爭對手快幾十倍。 如果您使用柱狀資料處理大數據,那麼 H2O 很難被擊敗。

技術挑戰

安德魯:您整個職業生涯中最大的挑戰是什麼?

懸崖:我們正在討論問題的技術部分還是非技術部分? 我想說最大的挑戰不是技術挑戰。 
至於技術挑戰。 我簡直打敗了他們。 我什至不知道最大的一個是什麼,但有一些非常有趣的,花了相當多的時間和精神鬥爭。 當我去Sun的時候,我確信我會做出一個快速的編譯器,而一群前輩回應說我永遠不會成功。 但我沿著這條路,寫了一個編譯器到暫存器分配器,而且速度相當快。 它和現代 C1 一樣快,但當時的分配器要慢得多,事後看來這是一個很大的資料結構問題。 我需要它來編寫一個圖形寄存器分配器,但我不理解程式碼表現力和速度之間的困境,這在那個時代存在並且非常重要。 事實證明,資料結構通常超過當時 x86 上的快取大小,因此,如果我最初假設暫存器分配器將計算出總抖動時間的 5-10%,那麼實際上結果是50%。

隨著時間的推移,編譯器變得更乾淨、更有高效,在更多情況下不再產生糟糕的程式碼,並且效能越來越開始類似於C 編譯器產生的結果。當然,除非你寫了一些連C 都無法加速的廢話。 如果您編寫像 C 一樣的程式碼,那麼在更多情況下您將獲得像 C 一樣的效能。 你走得越遠,你就越經常得到漸近與 C 級一致的代碼,寄存器分配器開始看起來像是完整的東西......無論你的代碼運行得快還是慢。 我繼續研究分配器以使其做出更好的選擇。 他變得越來越慢,但在其他人無法應付的情況下,他的表現卻越來越好。 我可以深入研究寄存器分配器,在那裡埋下一個月的工作,突然整個程式碼的執行速度會加快 5%。 這種情況一次又一次地發生,寄存器分配器變成了一件藝術品——每個人都喜歡它或討厭它,學院的人就「為什麼一切都是這樣做」這個主題提出問題,為什麼不呢? 線掃描,有什麼差別。 答案仍然是一樣的:基於圖形繪製的分配器加上對緩衝區代碼的非常仔細的處理等於勝利的武器,是任何人都無法擊敗的最佳組合。 這是一件相當不明顯的事。 編譯器所做的所有其他事情都經過了相當深入的研究,儘管它們也達到藝術水平。 我總是做一些應該將編譯器變成藝術品的事情。 但這些都沒什麼特別的——除了暫存器分配器。 訣竅是要小心 降低 在負載下,如果發生這種情況(如果您有興趣,我可以更詳細地解釋),這意味著您可以更積極地內聯,而不會有在效能計劃中陷入困境的風險。 在那些日子裡,有一堆全規模的編譯器,掛著小玩意和口哨,有寄存器分配器,但沒有其他人能做到。

問題是,如果你添加需要內聯的方法,增加和增加內聯區域,使用的值集立即超過寄存器的數量,你必須削減它們。 當分配者放棄時,通常會出現臨界水平,並且一個好的溢出候選人值得另一個,你將出售一些通常瘋狂的東西。 這裡內聯的價值在於你損失了一部分開銷,呼叫和保存的開銷,你可以看到裡面的值並且可以進一步優化它們。 內聯的代價是形成大量的即時值,如果你的暫存器分配器消耗的太多,你就會立即失敗。 因此,大多數分配器都會遇到一個問題:當內聯跨越某條線時,世界上的一切都開始被削減,生產力就會被沖入馬桶。 那些實作編譯器的人添加了一些啟發式方法:例如,停止內聯,從足夠大的大小開始,因為分配會毀掉一切。 這就是性能圖中的一個扭結是如何形成的——你內聯,內聯,性能慢慢增長——然後繁榮! – 它像一個快速的千斤頂一樣掉下來,因為你的線太多了。 這就是 Java 出現之前一切的運作方式。 Java 需要更多的內聯,所以我必須讓我的分配器更加積極,這樣它才能平穩而不是崩潰,如果內聯太多,它就會開始溢出,但「不再溢出」的時刻仍然會到來。 這是一個有趣的觀察,我突然想到了這一點,並不明顯,但它得到了很好的回報。 我採用了正向的內聯,它把我帶到了 Java 和 C 效能並存的地方。 它們非常接近——我可以編寫比 C 程式碼和類似程式碼快得多的 Java 程式碼,但平均而言,從整體來看,它們大致相當。 我認為這個優點的一部分是暫存器分配器,它允許我盡可能愚蠢地內聯。 我只是內聯我看到的所有內容。 這裡的問題是分配器是否運作良好,結果是否是智慧工作的程式碼。 這是一個巨大的挑戰:理解這一切並使其發揮作用。

關於暫存器分配和多核心的一些知識

弗拉基米爾:像寄存器分配這樣的問題似乎是某種永恆的、無盡的話題。 我想知道是否有過一個看起來很有前途但在實踐中卻失敗了的想法?

懸崖: 當然! 寄存器分配是您嘗試尋找一些啟發式方法來解決 NP 完全問題的領域。 而且你永遠無法實現完美的解決方案,對吧? 這根本不可能。 看,提前編譯 - 它的效果也很差。 這裡的對話是關於一些普通情況的。 關於典型性能,因此您可以去測量您認為良好的典型性能 - 畢竟,您正在努力改進它! 寄存器分配是一個與效能有關的主題。 一旦你有了第一個原型,它就會工作並繪製出所需的內容,性能工作就開始了。 你需要學會很好地衡量。 為什麼它如此重要? 如果你有明確的數據,你可以查看不同的區域並看到:是的,它在這裡有所幫助,但這就是一切崩潰的地方! 一些好的想法出現了,你添加了新的啟發式方法,突然之間,平均而言,一切都開始變得更好了。 或者它沒有啟動。 我遇到過很多案例,我們正在為 XNUMX% 的性能而奮鬥,這使得我們的開發與之前的分配器有所不同。 每次看起來都是這樣:在某個地方你贏了,在某個地方你輸了。 如果您擁有良好的績效分析工具,您可以找到失敗的想法並了解它們失敗的原因。 也許值得讓一切保持原樣,或者採取更認真的方法進行微調,或者出去修復其他問題。 這是一大堆東西! 我做了這個很酷的黑客,但我還需要這個,這個,還有這個 - 他們的總組合提供了一些改進。 孤獨的人可能會失敗。 這就是 NP 完全問題的性能工作的本質。

弗拉基米爾:人們會感覺到分配器中的繪畫之類的問題已經解決了。 好吧,從你所說的來看,這已經為你決定了,那麼這還值得嗎…

懸崖: 沒有這樣解決。 你必須把它變成「解決」。 有困難的問題需要解決。 完成此操作後,就該提高生產力了。 您需要相應地處理這項工作 - 進行基準測試,收集指標,解釋當您回滾到以前的版本時,您的舊黑客再次開始工作(反之亦然,停止)的情況。 並且在取得某些成就之前不要放棄。 正如我已經說過的,如果有一些很酷的想法不起作用,但在想法暫存器分配領域,它幾乎是無窮無盡的。 例如,您可以閱讀科學出版物。 儘管現在這個區域已經開始比年輕時移動得慢得多,並且變得更加清晰。 然而,有無數的人在這個領域工作,他們的所有想法都值得嘗試,他們都在等待。 除非您親自嘗試,否則您無法判斷它們有多好。 它們與分配器中的其他所有內容整合得有多好,因為分配器可以做很多事情,並且有些想法在您的特定分配器中不起作用,但在另一個分配器中它們很容易。 分配器獲勝的主要方法是將慢速內容拉到主路徑之外,並迫使其沿著慢速路徑的邊界分裂。 因此,如果您想運行 GC,請採取慢速路徑、去優化、拋出異常,所有這些東西 - 您知道這些事情相對較少。 我查了一下,它們確實很罕見。 你做了額外的工作,它消除了這些慢速路徑的許多限制,但這並不重要,因為它們很慢且很少有人行走。 例如,空指標 - 它永遠不會發生,對吧? 對於不同的事情,你需要有幾條路徑,但它們不應該幹擾主要路徑。 

弗拉基米爾:當同時有數千個核心時,您如何看待多核心? 這是一個有用的東西嗎?

懸崖:GPU的成功說明它相當有用!

弗拉基米爾: 他們很專業。 通用處理器怎麼樣?

懸崖:嗯,這就是 Azul 的商業模式。 答案出現在人們真正喜歡可預測性能的時代。 那時編寫平行程式碼很困難。 H2O 編碼模型具有高度可擴展性,但它不是通用模型。 也許比使用 GPU 更通用。 我們是在談論開發這樣一個東西的複雜性還是使用它的複雜性? 例如,Azul 教我一個有趣的教訓,一個相當不明顯的教訓:小型快取是正常的。 

人生最大的挑戰

弗拉基米爾:非技術挑戰呢?

懸崖:最大的挑戰是不……對人友善和友善。 結果,我經常發現自己處於極度衝突的境地。 那些我知道事情出了問題,但不知道如何解決這些問題並且無法處理它們的人。 許多持續數十年的長期問題就是這樣產生的。 Java 擁有 C1 和 C2 編譯器的事實就是這一點的直接結果。 Java連續十年沒有多層編譯也是直接後果。 顯然我們需要這樣一個系統,但它不存在的原因尚不清楚。 我與一名工程師或一組工程師之間存在問題。 曾幾何時,當我開始在 Sun 工作時,我是……好吧,不僅如此,我通常對所有事情都有自己的看法。 我認為你確實可以接受你的這個真相並正面講述它。 尤其是因為我大多數時候都是正確的。 如果你不喜歡這種方式……特別是如果你明顯錯了並且胡言亂語……一般來說,很少有人能容忍這種形式的溝通。 雖然有些人可以,像我。 我的一生都建立在精英原則之上。 如果你給我看有什麼不對的地方,我會立刻轉身說:你胡說八道。 當然,我同時表示歉意,如果有的話,我會記下優點,並採取其他正確的行動。 另一方面,我對總時間中很大一部分時間的預測是正確的。 而且在人與人的關係上也不太管用。 我並不是想表現得友善,但我只是直率地問這個問題。 “這永遠行不通,因為一、二、三。” 他們就像,“哦!” 還有其他後果可能最好忽略:例如,那些導致我與妻子離婚以及此後十年抑鬱症的後果。

挑戰是與人們的鬥爭,與他們對你能做什麼或不能做什麼、什麼是重要的和什麼不重要的看法的鬥爭。 程式設計風格面臨許多挑戰。 我仍然寫了很多程式碼,在那些日子裡我甚至不得不放慢速度,因為我做了太多並行任務並且做得很糟糕,而不是專注於一項任務。 回想起來,我寫了一半Java JIT指令的程式碼,也就是C2指令。 第二快的程式設計師寫得慢一半,下一個慢一半,就是指數下降。 這排第七個人的速度非常非常慢──這種情況總是會發生! 我接觸了很多程式碼。 我看著誰寫了什麼,無一例外,我盯著他們的程式碼,審查了他們每個人,並且仍然繼續自己寫的比他們中的任何人都多。 這種方法對人來說效果不太好。 有些人不喜歡這樣。 當他們無法處理時,各種抱怨就開始了。 例如,我曾經被告知停止編碼,因為我寫了太多代碼,這危及了團隊,這對我來說聽起來就像一個笑話:夥計,如果團隊的其他成員消失了而我繼續寫代碼,你只會損失一半球隊。 另一方面,如果我繼續編寫程式碼,而你失去了一半的團隊,這聽起來像是非常糟糕的管理。 我從來沒有真正想過它,也沒有談論過它,但它仍然在我腦海中的某個地方。 這個念頭在我腦海中盤旋:“你們在開玩笑嗎?” 所以,最大的問題是我和我與人的關係。 現在我更了解自己了,我長期擔任程式設計師的團隊領導,現在我直接告訴人們:你知道,我就是我,你將不得不對付我 - 如果我站起來可以嗎?這裡? 當他們開始處理這個問題時,一切都順利了。 事實上,我不壞也不好,我沒有任何惡意或自私的願望,這只是我的本質,我需要以某種方式接受它。

安德魯:就在最近,每個人都開始談論內向者的自我意識和一般軟技能。 對此您有什麼想說的嗎?

懸崖:是的,這就是我從與妻子離婚中學到的見解和教訓。 我從離婚學到的是了解自己。 這就是我開始理解別人的方式。 了解這種互動是如何運作的。 這導致了一個又一個的發現。 人們意識到我是誰以及我代表什麼。 我在做什麼:要么我全神貫注於任務,要么我在避免衝突,或者其他什麼——這種程度的自我意識確實有助於保持自我控制。 在此之後一切都會變得容易得多。 我不僅在我自己身上發現了一件事,而且在其他程式設計師身上也發現了這一點:當你處於情緒壓力狀態時,無法用語言表達想法。 例如,你坐在那裡編碼,處於心流狀態,然後他們跑向你,開始歇斯底里地尖叫,說有東西壞了,現在將對你採取極端措施。 而且你不能說一句話,因為你處於情緒緊張的狀態。 所獲得的知識可以讓你為這一刻做好準備,度過它並繼續執行撤退計劃,之後你可以做一些事情。 所以,是的,當你開始意識到這一切是如何運作時,這是一個改變生活的巨大事件。 
我自己找不到合適的詞語,但我記得動作的順序。 關鍵是,這種反應既是身體上的反應,也是言語上的反應,而且你需要空間。 這樣的空間,在禪宗意義上。 這正是需要解釋的,然後立即退到一邊——純粹的身體退開。 當我保持口頭沉默時,我可以在情感上處理這種情況。 當腎上腺素到達你的大腦,將你切換到戰鬥或逃跑模式時,你再也不能說什麼,不- 現在你是一個白痴,一個鞭打工程師,無法做出適當的反應,甚至無法停止攻擊,而攻擊者是自由的一次又一次地攻擊。 你必須先重新做回自己,重新獲得控制,擺脫「戰鬥或逃跑」模式。

為此,我們需要言語空間。 只是自由空間。 如果你要說什麼,那麼你可以準確地說出來,然後去真正為自己找到「空間」:去公園散步,把自己鎖在淋浴間——這並不重要。 最重要的是暫時脫離這種情況。 一旦你關閉至少幾秒鐘,控制權就會恢復,你就會開始清醒地思考。 “好吧,我不是什麼白痴,我不做蠢事,我是一個非常有用的人。” 一旦您能夠說服自己,就該進入下一階段:了解發生了什麼。 你遭到攻擊,攻擊來自你意想不到的地方,這是一次不誠實、卑鄙的伏擊。 這不好。 下一步是了解攻擊者為什麼需要這個。 真的,為什麼? 也許是因為他自己很憤怒? 他為什麼生氣? 例如,因為他把自己搞砸了,無法承擔責任? 這才是謹慎處理整個局面的方法。 但這需要迴旋餘地,也就是言語空間。 第一步是中斷言語接觸。 避免用言語討論。 取消它,盡快走開。 如果是電話交談,就掛掉──這是我從與前妻溝通中學到的技巧。 如果談話進展不順利,就說「再見」然後掛斷電話。 電話那頭:“等等等等”,你回答:“是的,再見!” 然後掛斷電話。 你就結束談話吧。 五分鐘後,當你恢復理智思考的能力時,你已經冷靜了一點,可以思考一切,發生了什麼以及接下來會發生什麼。 並開始製定深思熟慮的回應,而不是僅僅出於情緒做出反應。 對我來說,自我意識的突破正是在情緒緊張時無法說話。 擺脫這種狀態,思考和計劃如何應對和彌補問題 - 這些是當您無法說話時的正確步驟。 最簡單的方法就是逃離出現情緒壓力的情況,停止參與這種壓力。 之後你就變得能夠思考,當你能夠思考時,你就變得能夠說話,等等。

順便說一下,在法庭上,對方律師試圖對你這樣做——現在原因很清楚了。 因為他有能力把你壓製到一個地步,例如你連名字都叫不出來。 從非常現實的意義上來說,你將無法說話。 如果這種情況發生在您身上,並且您知道您會發現自己處於一個激烈口水戰的地方,例如法庭,那麼您可以和您的律師一起去。 律師會為你出面,停止言語攻擊,並且會以完全合法的方式去做,失去的禪宗空間也會歸還給你。 例如,我給家人打了幾次電話,法官對此很友善,但對方律師卻對我大喊大叫,我甚至插不上話。 在這些情況下,使用調解員最適合我。 調解者停止了所有這些源源不斷地傾注在你身上的壓力,你找到了必要的禪宗空間,隨之而來的是說話的能力回歸。 這是一個完整的知識領域,其中有很多東西需要學習,有很多東西需要在你自己身上發現,所有這些都會變成對不同人來說不同的高階策略決策。 有些人不會有上述問題;通常專業銷售人員不會有這些問題。 所有這些以文字為生的人——著名歌手、詩人、宗教領袖和政治家,他們總是有話要說。 他們沒有這樣的問題,但我有。

安德魯: 這真是……出乎意料。 太好了,我們已經聊了很多,是時候結束這篇訪談了。 我們一定會在這次會議上見面,並且能夠繼續這種對話。 九頭蛇見!

您可以在 Hydra 2019 會議上繼續與 Cliff 對話,該會議將於 11 年 12 月 2019 日至 XNUMX 日在聖彼得堡舉行。 他會帶著一份報告來 “Azul 硬體事務記憶體體驗”. 可以購買門票 在官方網站上.

來源: www.habr.com

添加評論