前幾天,我看到一則瘋傳的推文,聲稱那些認為大型語言模型(LLM)導致程式碼緩慢、臃腫的人,將會在一切都用高度優化的組合語言重寫後被打臉。我們還沒到需要用組合語言編寫一切的階段,但諾蘭·勞森(Nolan Lawson)關於測試的說法,即「你可以選擇你想要的錯誤數量」,正變得越來越適用於效能優化。我曾較不精確地在此提及過。

我上一篇文章中有一則評論提到,過去專門的效能工作成本已大幅降低,過去需要稀有技能的效能工作,現在任何人只要能打幾個字就能完成。這意味著過去只有最大規模或最賺錢的專案才值得進行的各種優化,現在都變得可行。對此,馬克·布魯克(Marc Brooker)回應道:

我完全同意你最後的觀點。針對特定工作負載而非一類工作負載的動態客製化軟體,似乎是一個非常可能出現的結果。(這本身也帶來了各種有趣的風險和機會。)有點讓我想起 FFTW。還有大量古怪的舊式 demoscene 技術,它們都致力於在非常特定的問題(通常是特定硬體)上實現超快速和小型化。例如,我記得有一個 demo 重複使用其程式碼作為紋理,以獲得極佳的快取局部性。

網路上流傳著一個迷因,認為 AI 沒有幫助,因為「程式碼從來都不是難點」。我認為在某些領域這是真的,但在其他領域,編寫程式碼絕對是難點。JIT 編譯器就是一個很好的例子。對於許多軟體來說,JIT 編譯器將極大地幫助加速程式碼。JIT 編譯器之所以罕見,讓我認為在歷史上實現 JIT 編譯器太困難,以至於不值得。LLM 降低了進入門檻,使得編寫 JIT 編譯器變得容易得多。這也是 pgrust 背後的理論。資料庫在歷史上一直是建造最困難的軟體,並因此受到限制。現在,有了 AI,我們可以對我們建造的軟體類型更加雄心勃勃。

讓我們用 FRE 來試試,這是我們上一篇文章中建立的正規表達式引擎。回想一下,它是透過讓一個代理人花一個月時間,在 rebar 正規表達式基準測試套件的幫助下,不斷改進正規表達式引擎的效能而創建的。這導致 FRE 過度擬合 rebar,直到我們警告代理人有一個獨立的基準測試,才促使代理人將優化泛化到足夠的程度,使其在獨立測試上的效能尚可。沒有特別的理由去使用一個在獨立測試上無法勝過經過良好測試的正規表達式引擎的「軟體工廠」正規表達式引擎,但 FRE 的一個顯著特點是,原生 AOT 編譯版本在較長的搜尋中表現相當不錯。我們注意到,可以想像在 ripgrep 運行其正常匹配器時,在另一個執行緒中運行原生程式碼編譯器,然後在編譯完成後切換到原生程式碼,通常可以獲得更好的效能。當然,這通常會導致短查詢的效能變差,因為我們失去了一個執行緒用於編譯,但我更關心 ripgrep 運行數秒或數分鐘時的耗時,而不是運行數秒時的耗時,所以我對這種權衡感到滿意。

就像我們可以在幾分鐘的人工時間內建立一個正規表達式引擎一樣,我們也可以在幾分鐘的人工時間內進行這個實驗。我打了一些字,一個代理人就完成了工作,使得這一切成為可能(這對人類來說將是一大塊程式碼手術),並且它在我的程式碼歷史記錄中的實際 ripgrep 查詢上運行了基準測試。對於較長的查詢,我們在這裡看到了一些非常簡單的查詢有 2-4 倍的效能提升。但大多數查詢更複雜,當我們在代表性的獨立查詢上運行時,對於應該啟用 AOT 的查詢 2,我們獲得了約 7% 的加速。這不是一個驚天動地的結果,但對於花幾分鐘打字到 codex(它仍在進行更多優化,並有望進一步加速)來說,這也不是一個壞結果。

這可以說是一件很傻的事,因為如果我們在電腦上重複搜尋文字,顯然的加速方法不是為正規表達式匹配編寫原生程式碼編譯器,而是建立一個索引。但重點是,這種過去需要大量時間和專業知識的技術工作,現在可以輕而易舉地完成。而且,如果我們想建立一個文字索引,碰巧我曾參與 BitFunnel 的開發,這是 Bing 的搜尋索引,專門用於快速的文字攝取,並贏得了 SIGIR 的最佳論文獎,所以我可以想到一些實驗,如果我們要建立一個快速的本地索引,涵蓋我們整個機器(我見過的專案似乎旨在索引你的程式碼目錄,但真正拖垮我機器效能的是當 codex 決定針對大量生成檔案的巨大臨時目錄運行 ripgrep,然後在錯失時擴展到查看我的整個機器時,所以我想要一個涵蓋我整個磁碟的索引,而不僅僅是某些專案的程式碼)。

如果我在一家 AI 實驗室工作,並且能夠使用運行在 Cerebras 晶片或其他加速器上的 SOTA 模型,這些模型極大地提高了 tok/s,從而增加了搜尋的負載/需求,我可能會實際調查現有的索引器,看看它們是否足夠快,或者我是否想自己建立一些客製化的東西。雖然 BitFunnel 的開源版本「僅」包含一個位元組碼解釋器和一個 JIT,但 Bing 版本包含多個 JIT 編譯器。一個進行如此大規模優化的專案過去是一項重大工程,但「我可以在週末完成」現在對某些這類專案來說是真的。憑藉我每月 200 美元的低廉帳戶,我認為一個稍微快一點的 ripgrep 加上任何現成的索引就足夠了,所以也許這個快速攝取全機器索引專案可以留給「給在 AI 實驗室工作的讀者作為練習」。

優化成本的大幅降低可以追溯到 2025 年 11 月,甚至可能在此之前,隨著公開模型的出現(我確信在此之前,AI 實驗室的人員已經能夠接觸到)。以 GPT-5.1 或 5.2 時期的例子來說,我對遊戲 AI 一無所知,試圖建立一個 Azul AI。結果它在該遊戲中成為了最強大的 AI,優勢相當明顯。從描述第二強 AI 的論文來看,我認為我的 AI 在「AI」方面可能稍微好一些,但它獲勝的主要地方在於優化。例如,那個 AI 是單線程的,而我的 AI 是多線程的。由於我有一個原生程式碼版本,以及一個惡劣的 wasm 共用記憶體 + JavaScript 版本,還有兩個針對不同版本的不同搜尋架構,這「需要」完全不同的多線程演算法(用於非常小且快速的網路的 minimax,以及用於較大網路的 MCTS),如果手工完成,這將是一項相當大的工程。而且,由於我曾幾次讓 LLM 根據其自身的(錯誤)推理來選擇多線程演算法,然後才花 30 分鐘閱讀遊戲 AI 的多線程演算法,我最終(讓 codex 重寫)多次重寫了多線程演算法。

有很多標準的步驟可以幫助調試和驗證像這樣的多線程演算法,例如實現從調試日誌重播,即使演算法是非確定性的也能重現錯誤。僅僅這一步,如果我手工完成,可能就需要幾天到一週的時間,但這正是代理人可以輕鬆地在迴圈中完成的事情(只需讓它每次嘗試重播日誌並在無法完美重播時插入日誌以處理非確定性)。許多過去為了讓這種棘手的優化奏效而必須付出的繁瑣工作都消失了。

這也適用於許多其他棘手的優化。從我曾編寫 CPU 微碼、進行 CPU 驗證、優化搜尋引擎索引等經驗來看,我有很多時間在查看優化時思考「嗯,這會提高 2% 的效能,但需要 N 個人天來驗證這個棘手的優化是否有效」,然後根據是否值得花費時間來決定是否繼續。現在,這個 N 的值已經大大降低(變數很大,但以人力時間計算,通常是 1000 倍 / 10000 倍 / 1000000 倍,如果將代幣成本與 Bing 工程師編寫搜尋索引所使用的 JIT 編譯器進行比較,以美元成本計算可能更接近 1000 倍),因此有意義的這類優化數量大大增加。對於那些你不確定是否會加速的優化也是如此。我過去有時會看一個我不確定是否會加速的優化,然後想「這需要 M 小時才能實現到我們可以獲得足夠好的測量來猜測效能影響的程度」。現在,嘗試這些優化變得更有意義了。

回到遊戲 AI 的案例,至少對於我嘗試的 AI 來說,似乎每速度加倍就能獲得約 100 Elo 的提升(比西洋棋多,我懷疑是因為平局非常罕見)。僅僅增加多線程就足以讓一個在其他方面相當的 AI 在大型機器上望塵莫及。如果你堆疊 10-20 個大多數人覺得太麻煩而不願手動進行的優化,那麼差異是巨大的,試圖跟上手寫 AI 3 是不合理的。

遊戲 AI 的案例比大多數軟體都稍微麻煩一些,因為你想要進行的許多優化實際上會改變結果,而且沒有便宜、簡單的方法可以判斷速度的增加加上結果的變化是否在實際中產生更好或更差的結果。而且,正如我們之前注意到的,目前公開的 SOTA 模型在實驗設計方面相當糟糕,所以我不得不設置他們用來確定優化是否良好的框架,但一旦框架建立起來,它就像任何其他優化問題一樣。我猜測從事 LLM 優化的人也必須處理這類問題,但大多數優化問題要簡單得多。

再舉一個例子,作為準備效能面試的一部分,傑米·布蘭登(Jamie Brandon)嘗試了 Anthropic 現在公開的效能測驗題。嘗試之後,他讓 Claude 接手了他的工作,並取得了更好的結果。當他看到 Claude 做了他還沒做的事情時,他說很多優化都是他想到的,但他還沒來得及做,而「有些是瘋狂的事情,除非我花幾週時間研究,否則我永遠不會嘗試」4。他是一位合理的效能工程師,並獲得了他想要的效能工作的職位,但在一個定義明確的優化問題上,他完全無法與一個不錯的模型競爭(我還沒有自己嘗試過這個問題,但我懷疑在時間限制大致相同的情況下,我也無法競爭)。

回到馬克·布魯克評論的這部分:

針對特定工作負載而非一類工作負載的動態客製化軟體,似乎是一個非常可能出現的結果。

這似乎是不可避免的。在我文章的另一則回覆中,pgrust 的麥可·馬利斯(Michael Malis)說了類似的話:

[討論 pgrust 優化]... 我認為創建這些優化足夠容易,我們可以查看客戶的工作負載並根據需要添加它們

在沒有任何框架或設置的情況下,就在我開始寫這篇文章之前,我讓一個代理人為我的 ripgrep 查詢進行了工作負載特定優化(不是原生程式碼編譯器切換,而是基於一組基準測試對通用 FRE 引擎的優化),這花了我大約 2 分鐘啟動。優化在查詢集上運行,然後有一個後續的獨立查詢集來運行。這仍在進行中,但初步結果似乎很有希望。經過一輪優化後,工作負載優化版本在獨立測試上的速度比標準 ripgrep 快 2%,而且還在持續加速。對於我本地的 ripgrep 使用來說,2% 並不算什麼大事,但考慮到這只花了幾分鐘時間,而且這些優化在我開始寫這篇文章時就已經開始了,並且還在不斷改進,我會接受這裡的 2% 的提升(請注意,這還沒有與原生程式碼編譯器結合,如果正確結合,整體提升會更大)。而且請記住,這利用了 FRE 正規表達式引擎 5,該引擎在獨立測試上比 Rust 正規表達式引擎慢得多,並且由於我對正規表達式工作負載一無所知,而 SOTA LLM 在實驗設計方面不夠好,無法進行無指導的開放式自我改進迴圈,我們沒有好的方法來提高其在獨立測試上的效能。但如果我關心的是我自身工作負載的效能,我有足夠的數據並且一直在生成更多數據。正如馬克·布魯克上面指出的,我們必須小心過度擬合,如果出現舊數據中沒有的運行時變化等情況,但我們仍然處於比以前更好的境地。

在更普遍的情況下,如果你是亞馬遜的馬克·布魯克或 pgrust 的麥可·馬利斯這樣的人,那麼不僅僅是做一次性的事情,而是與客戶合作試點一個項目,利用他們的數據為他們優化,然後找出如何將其擴展到一般客戶。我沒有在一家公司工作,那裡這是最有效利用我時間的方式 6,但你可以看到這對大型公司來說是可行的,而且鑑於它只需要幾分鐘的時間來運行這些實驗,對於個人專案來說,嘗試這種事情是相當合理的。

感謝 Jamie Brandon、Michael Malis 和 Max Bittker 的評論/修正/討論。

P.S. 如我在過去幾篇文章中提到的,對於編碼代理人,運行實驗和看到足夠的結果來滿足我的好奇心所需的時間大大減少,而使結果真正嚴謹所需的時間沒有改變或有所增加,因此像我過去那樣寫文章意味著相對於我擁有的頻寬,我只能運行很少的實驗。結果是,我一直在運行這些實驗並與幾個朋友分享結果。作為一個實驗,我正在嘗試以一種非常快速且不嚴謹的方式寫下這些內容,而不是讓這些實驗多年來只被少數朋友知道。就像上一篇文章一樣,我設定了一個目標,在半小時內寫完這篇文章並完成所有清理工作,雖然沒有計時,但我很確定我錯過了這個目標一點點。

即使這樣,寫這些文章所需的時間仍然很長,以至於我落後於分享最近的結果,但我還不想(暫時?)轉向 LLM 寫的文章,而且我認為我無法現實地花費足夠的時間來清理數據並寫一篇像這樣的文章,以便在半小時內完成。僅就這篇文章的長度而言,打字應該需要 20-30 分鐘,包括停下來思考我正在寫的內容的時間,然後當我查看數據時,有時會出現一些錯誤,我需要更仔細地檢查以查看是否有需要修復的問題(這裡發生了好幾次,我預計,由於我沒有花費更多時間,還有其他數據問題是我不知道的)。

總之,如果你對這些快速(而且肯定更錯誤)的寫作有任何意見,請讓我知道你的想法( X Bsky Mastodon )!

我長期以來一直強烈反對「X 的開發者寫慢程式碼是壞的,應該感到羞恥」的普遍觀點,因為有各種各樣的程式設計專業知識,不僅大多數程式設計師沒有效能專業知識,而且從業務關心、就業市場等方面來看,這可能甚至沒有意義,所以大多數專案的效能遠不如效能專家所能達到的水平。

以上面的例子來說,Jamie Brandon 獲得了 Anthropic 的錄用通知,你可能負擔不起他或像他這樣的人,除非你是 OpenAI,但你可以負擔得起使用一個編碼代理人,它可以在一個有界優化問題上擊敗他。該代理人沒有他的判斷力,在一個開放式問題上會表現得更糟(回想一下,當我們嘗試建立一個優化的正規表達式引擎並只告訴它不要過度擬合時,它在我們的獨立測試基準上比最好的正規表達式引擎差一個數量級以上,但也要記住,在告訴代理人它在一個它表現不佳的獨立測試後,它加速了正規表達式引擎的效能,使其在效能上普遍能與第二梯隊的正規表達式引擎媲美,這與大多數程式碼中普遍的效能優化水平相比仍然非常出色),但這足以在各種問題上實現合理的效能。這篇文章主要討論了後端效能問題,但如果你想降低 LCP、INP 等指標,代理人在前端效能方面似乎也不差。

這裡有一些關於我機器上 ripgrep 查詢分佈的資訊。我無法聲稱這在任何地方都具有代表性。搜尋模式的長度分佈比我預期的要長得多。p50 是 55 個 Unicode 編碼點(為了簡化,我將它們稱為字元),這已經比我手動 grep 的內容長了,而 p90 是 119 個!

我們也可以看看正規表達式中的交替分支數量,這又比我手動做的複雜得多。

另一種觀點是看看它們之間的關聯性。隨著正規表達式的變長,我們是否會獲得更多的交替分支?是的。

這些真正長的正規表達式到底是什麼?如果我們看看它們,大多數最長的都是關於函數名或測試名的長交替,例如以下正規表達式,它似乎與 FRE 的開發有關。

但有些是有趣的數字結構,例如

這相當於 :(?:1[3-9]|[2-9][0-9])[0-9]{2}: (如果在原始輸入上透過 ripgrep 運行,效能大致相同)。整個管道是

這對人類來說可能是一種奇怪的做法,但代理人似乎總是這樣做。

另一方面,如果我們看看 ripgrep 查詢花了多長時間,有很多查詢很慢,例如 p99 將近 1 分鐘!而 p999 將近 10 分鐘!而在此期間(大約一個月在一個筆記型電腦上;查詢和分佈在 AWS 主機上運行代理人時可能不同,等等,但我還沒檢查過)的最大查詢時間接近 2 小時!

在命令列選項方面,我們看到以下情況。也許不出所料,codex 經常需要行號,而且不知何故,它偶爾會使用 PCRE2 正規表達式。

我不會為這些添加圖表或表格,但另一件值得注意的事情是,搜尋模式的局部性相當低(約 94% 的模式只出現一次),這在考慮到許多查詢的長度時是合理的。然而,搜尋的檔案局部性相當高,一個被搜尋過的檔案相對可能很快再次被搜尋,這表明(對於足夠小的檔案)它們很可能在記憶體中被搜尋。

此外,99% 的查詢是正規表達式查詢(1% 是非正規表達式字串搜尋),99.9% 的搜尋查詢僅為 ASCII,但在搜尋的檔案中,約 45% 僅為 ASCII,55% 包含 Unicode,Unicode 的百分比比我預期的要高。

在上一篇文章的草稿中,Peter Geoghegan 指出:

正規表達式實現也可以透過支援較少的功能來實現更快的速度。有些實現不支援反向引用等。

這在這裡也是真實的。這裡進行的工作負載特定優化相當膚淺,因為我只給了 codex 一些簡短的指令,讓它隨心所欲(這通常不是 codex 最有效的用法),但如果有一個更詳細的計劃,針對我的查詢的常見用例進行更集中的優化,預計可以產生更大的收益。