vatt'ghern jaskier's ballads
本文 1 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

作者的工作習慣是兩個顯示器各開一個一模一樣的 IDE,同時處理同一個 workspace 裡的一批專案——兩份 rust-analyzer 加起來吃掉了 16GB 記憶體,作者自己形容這個用法是「roughly 2N」,換算成單一個 instance 大概是一半。原話是「that I, ugh, would prefer to have available」——這句抱怨後來變成一支新 LSP 的第一行 commit,目標把常駐記憶體壓到 100MB 以下。

同樣做 Rust LSP,記憶體差了 100 倍

Rust Glancer 是作者已經拿來當日常編輯器用了大約一個半月、直到 2026 年 8 月才貼出第一篇公開部落格文章「Hello, world!」的 Rust language server,作者宣稱常駐記憶體可以壓到遠低於 rust-analyzer 的水準——他自己給的目標是「target <100mb for reasonable projects」。對照組是 rust-analyzer:目前幾乎所有 Rust 編輯器整合預設用的 LSP,用 salsa 做增量查詢資料庫、rowan 建構語法樹,逐鍵重新分析的精確度是它的招牌。兩者要解決的是同一個問題——給 Rust 提供 goto definition、hover、補全、inlay hints——差別在於要不要把整個索引結果留在記憶體裡。

這類比較不只是效能極客的話題。同一支 LSP 在你筆電上多吃幾 GB,代表你能同時開的分頁、容器、其他工具就少幾個;對維運大型 monorepo 的團隊來說,這筆記憶體帳甚至會反映在開發機規格與雲端開發環境的成本上。

資料來源:Rust Glancer 部落格「Hello, world!」(2026-08-19)與 rust-analyzer 官方架構文件。
比較項 Rust Glancer rust-analyzer
常駐記憶體 作者的目標:「target <100mb for reasonable projects」 作者「兩個顯示器各開一份 IDE」同時工作時,兩份加總觀察到的個案:「16GB of memory」,自陳是「roughly 2N」,單一個 instance 約一半(非官方基準)
Base indexing(M4 Max 36GB) 5 秒 6 秒
Full indexing(M4 Max 36GB) 8 秒 13 秒
Base indexing(M1 8GB) 6 秒 7 秒
Full indexing(M1 8GB) 9 秒 14 秒
打字當下重新分析的範圍 只對目前函式本體做淺層分析,沿用上一次存檔的完整索引 salsa 增量重算被觸及的查詢,但整個查詢圖常駐記憶體
新符號何時對 IDE 可見 存檔後才索引 逐鍵幾乎即時
作者自己的定位 「not a complete LSP yet」,有已知 bug 「the default choice for projects that care about completeness and keystroke accuracy」
索引時間兩邊差距不大(都在個位數到十幾秒之間),常駐記憶體與新符號可見時機才是真正分岔的地方。

把四組時間數字攤開來看:索引速度其實差距不大——Full indexing 在 M4 Max 上是 8 秒對 13 秒,M1 上是 9 秒對 14 秒,Rust Glancer 每一組都略快,但差距最多不到兩倍;記憶體宣稱的落差卻是兩個數量級。這個落差來自兩邊在「要不要把分析結果留在記憶體裡」這個問題上做出的相反決定,「時間差距溫和、記憶體差距誇張」正是這個決定造成的結果。

換算一下就知道「差了 100 倍」這個標題怎麼來的:作者說的 16GB 是兩份 rust-analyzer 同時開著的加總,換算回單一個 instance 大概是一半、8GB 左右,8GB 除以 100MB 大約是 80 倍,取整數量級一樣落在兩個數量級、口語說法就是「百倍」量級。這個換算本身沒有問題,問題在於分子分母不是同一次量測——這點下面會拆得更細。

常駐記憶體:一個目標數字對一個個案數字

兩邊唯一直接對得上的數字,其實只有作者自己給的兩個:Rust Glancer 的目標「target <100mb for reasonable projects」,以及他動手寫這支 LSP 之前,親眼看著自己兩個顯示器各開一份 IDE、同時跑著的兩份 rust-analyzer 加起來吃掉「16GB of memory that I, ugh, would prefer to have available」——作者自己形容這個用法是「roughly 2N」,單一個 instance 大概是一半、8GB 上下。把「<100mb」跟「單一 instance 大概 8GB」放在一起看,落差還是接近兩個數量級——這也是「記憶體差了 100 倍」這個說法的來源。但要說清楚:這不是同一份 benchmark 跑出來的兩個讀數,一個是設計目標、一個是單一個案的自述換算,兩邊都沒有在同一台機器、同一個 workspace 上直接對照過。

「reasonable projects」這個限定詞本身沒有被進一步定義——多大的 crate 數量、多少行程式碼算「合理」,作者沒有寫死一個門檻。這跟 rust-analyzer 那句「16GB」一樣,都是留給讀者自行對號入座的模糊地帶:兩個數字背後的 workspace 規模、crate 數量、開了幾個檔案、同時開幾份 IDE,作者都沒有公開到可以互相核對的程度。

作者也真的示範過比目標宣告更具體一層的東西——影片裡「Rust Glancer memory usage remaining below 100 MB」,測試全程維持這條線以下。這代表至少在他選定的測試場景裡,這個數字被跑出來過,不是純粹的規劃書。

兩台測試機本身就是一個訊號——M4 Max 是 2025 年最新的頂規機種,M1、8GB 則是 2020 年的入門款。合理的推測是,多數 LSP 的效能宣傳習慣只挑最新款機器做展示;Rust Glancer 特地把五年前的低階機器也放進同一張表,呼應了它整篇文章的重點——資源有限的機器上能不能正常開發,才是這篇文章真正要驗證的事,比頂規機器上誰更快更值得測。

記憶體落差說到底是兩邊決定把什麼東西留在記憶體、什麼丟到別處的結果,比「Rust Glancer 比較會省」這句籠統的說法具體得多。下面把雙方各自常駐(或不常駐)的四塊拆開看:

誰把什麼留在哪裡

  • L1 · Salsa 查詢圖rust-analyzer · 常駐記憶體

    整個增量查詢結果的鍵值圖常駐記憶體;官方文件裡「keeping everything in memory is OK」講的是原始碼這個輸入端,衍生查詢結果也一併常駐則是作者自己觀察到的效果。

  • L2 · Rowan 語法樹rust-analyzer · 常駐+易碎片化

    每個檔案的語法樹用 rowan 建構,樹狀結構讓局部失效變得可能,但作者認為「the tree-like representation inside of it can cause heavy memory fragmentation」。

  • L3 · Frozen 索引快照Rust Glancer · 磁碟優先

    整個 workspace 索引一次,結果寫進檔案系統,「loaded to memory only when they are actually needed」;重新啟動編輯器也不用重新索引。

  • L4 · 目前函式本體的淺層分析Rust Glancer · 常駐但很小

    打字當下只對「目前函式本體」做淺層分析並沿用先前完整索引,新符號要等存檔才真正入庫。

L1 · Salsa 查詢圖 整個增量查詢結果的鍵值圖常駐記憶體,官方文件的判斷只針對原始碼輸入端 rust-analyzer · 常駐記憶體 L2 · Rowan 語法樹 樹狀結構方便局部失效,但作者認為長期編輯會造成 heavy memory fragmentation rust-analyzer · 常駐+易碎片化 L3 · Frozen 索引快照 整包索引結果寫進檔案系統,查詢時才局部載入,重新啟動編輯器不必重新索引 Rust Glancer · 磁碟優先 L4 · 目前函式本體的淺層分析 只碰目前函式本體,沿用上次存檔的完整索引;新符號存檔後才入庫

工程師平常很難直接感知到記憶體碎片化——作業系統回報的「已用記憶體」數字不會告訴你這些記憶體有多少是真正被用到、多少是配置器切碎之後留下的縫隙。這也是為什麼「16GB」這種第一人稱觀察格外有參考價值:那是實際跑起來、實際被工作系統回報的數字,而不是理論上的資料結構大小估算。

這四塊攤開來看還有一個隱藏的假設:rust-analyzer 假設「記憶體夠用」,Rust Glancer 假設「磁碟 I/O 夠快」。合理的推測是,這個假設能成立,某種程度上仰賴這幾年 SSD 讀取速度的進步——但兩份來源都沒有談到磁碟延遲、HDD 或這類時機上的考量,這一段純屬個人推論。

前兩塊都在 rust-analyzer 那側,後兩塊都在 Rust Glancer 那側。差別很直白:rust-analyzer 選擇把分析結果整包留在記憶體,Rust Glancer 選擇把分析結果凍結成快照丟到硬碟,需要的時候才載入一小塊。

索引與增量運算模型:常駐查詢圖 vs 凍結快照

要理解這個分岔怎麼發生,得先弄清楚幾個關鍵字在講什麼。

點底線的字看定義與出處 · 4 個詞彙

這篇文章反覆出現 salsarust-analyzer 用的增量查詢資料庫。官方文件形容它「a key-value store, but it can also compute derived values using specified functions」。rowanrust-analyzer 用來建構語法樹的函式庫,樹狀結構支援局部失效。chalkRust 的 trait solver。rust-analyzer 用它做型別推論,Rust Glancer 作者原本想自己寫,後來也改用它。frozen 索引快照Rust Glancer 把整個 workspace 索引一次後寫進檔案系統的做法,查詢時才局部載回記憶體。 這四個詞——先弄清楚各自的角色,後面的機制比較才看得懂。

rust-analyzer 官方架構文件很直接地解釋了為什麼把原始碼這個輸入端整個留在記憶體:「Because the input data is source code, which typically measures in tens of megabytes at most, keeping everything in memory is OK.」這是團隊自己寫在文件裡明確做出的判斷,不是事後才發現的技術債——原始碼本身通常只有數十 MB,帳面上不算貴。文件本身沒有直接說「衍生出來的所有查詢結果也一併常駐」;這一點其實是 Rust Glancer 作者自己的觀察——查詢圖被設計成整包常駐,難以把部分資料搬到記憶體以外,下一段會完整引用他的原話。文件裡還有一條核心不變量:「typing inside a function's body never invalidates global derived data」,正是這條規則讓 rust-analyzer 能做到逐鍵級的增量分析而不必整包重算。

具體一點說,salsa 的「derived value」指的是像「這個函式的回傳型別是什麼」「這個變數在哪裡被宣告」這類查詢的計算結果。每一次 hover 或 goto definition,背後都是對 salsa 資料庫發一次查詢;查詢背後可能疊了好幾層別的查詢,全部算完就快取起來,下次同樣的問題直接回傳快取值,不必重算。這也是為什麼 rust-analyzer 在同一個檔案裡重複觸發同一個功能時,第二次通常比第一次快得多。

Rust Glancer 的作者對「整包常駐」這個決定有不同的評價:「It is a very cool approach, but it's inherently tied to memory, which makes it hard to move parts of data from memory elsewhere.」salsa 的查詢圖被設計成常駐,難以把部分資料挪到記憶體以外;再加上 rust-analyzer 用 rowan 建構語法樹,「the tree-like representation inside of it can cause heavy memory fragmentation」——樹狀結構方便局部失效,但長時間編輯下來記憶體配置容易變得零碎。作者自己也承認這不是設計失誤:「rust-analyzer chose them to make the LSP faster, and it does work for that purpose」,用記憶體換來的是逐鍵級的反應速度。

Rust Glancer 反過來把「常駐」整個拿掉。核心想法是把索引結果「offloaded to the filesystem and loaded to memory only when they are actually needed」——workspace 只整包索引一次,「It indexes the workspace once and preserves results in the filesystem」,往後每次查詢才去磁碟撈需要的那一小塊。附帶的好處是編輯器重新啟動也不用重新索引,因為「saved analysis is reusable, and since it's already offloaded to the filesystem, it can be reused after the editor restart」。

這套凍結索引的設計還有一個容易被忽略的角落:讓 AI agent 在背景直接修改程式碼時,inlay hints 容易跟著跑位、跟實際程式碼對不上——作者特別提到這個情境下 rust-analyzer 也有同樣的毛病,不是 Rust Glancer 獨有的問題。作者的解法是額外寫了一個檔案監控機制,「it was resolved by implementing a custom file watcher」,專門補 agent 編輯這條路徑上編輯器原本偵測不到的洞。

這個差別在單一時間點的截圖裡看不太出來——兩邊都能回答 goto definition,兩邊都有一個數字叫「記憶體用量」。真正拉開差距的是「隨時間累積」這件事:開的檔案越多、待機時間越長,兩條曲線會往哪個方向走。下面是把這個機制畫成的示意模擬——不是實測曲線,兩條線的量級校準分別對到上面提過的「16GB」個案與「<100mb」目標:

按播放看記憶體隨「開檔案/打字/存檔」事件變化 · 2 條線

t = 0.0s
rust-analyzer(salsa 常駐查詢圖,示意) Rust Glancer(frozen 快照 + 淺層分析,示意)
示意模擬,非實測曲線;虛線是作者自陳的「<100mb」目標線。曲線只會往上走的那條,反映的是查詢圖難以把資料搬到記憶體以外這件事。

示意模擬,非實測曲線;虛線是作者自陳的「<100mb」目標線

rust-analyzer 把整個查詢圖留在記憶體裡只增不減;Glancer 打字時只碰目前函式本體,記憶體幾乎不動,存檔才有短暫尖峰。

模擬裡 rust-analyzer 那條線只會往上、不太會往下——每開一個新檔案,salsa 的查詢圖就多記一批衍生資料,而且難以把這些資料搬到記憶體以外;Rust Glancer 那條線大部分時間貼著底部,因為大部分索引結果根本沒被搬進記憶體,只有存檔時才會有一次短暫的尖峰,把新結果寫回磁碟快照後很快又降下來。

打字與存檔:鍵盤敲下去的瞬間,兩邊各自在算什麼

記憶體策略的另一面,是「打字的時候會發生什麼運算」。這裡兩邊的答案差得比記憶體數字更直接影響手感:

切換分頁比較三個時刻各自的運算範圍 · 3 個分頁

rust-analyzer · 每一鍵都重算

靠的是官方文件寫的不變量:「typing inside a function's body never invalidates global derived data」。所以每次按鍵只需要重算被觸及的那個函式本體對應的衍生查詢,其餘全部沿用 salsa 快取——因為整個查詢圖本來就在記憶體裡,重算完立刻可查,不用等任何 I/O。

Rust Glancer · 只碰目前函式本體

「when you type, it doesn't perform full blown analysis on each keystroke, it instead attempts shallow analysis of the current body and reuses the previous complete index」。新的 import、struct、trait 這時候「are not indexed until you save the document」——對 LSP 來說它們還不存在。

Rust Glancer · 存檔才真正入庫

存檔觸發一次完整的重新索引,把新符號寫回 frozen 快照。成本比逐鍵重算高:作者自己承認「frozen workspace analysis is slower than lazy incremental by definition, since loading and deserializing data from filesystem is slower than loading from memory」。

三個分頁裡最容易被忽略的是中間那個——「previous complete index」這幾個字才是關鍵:淺層分析永遠是疊在上一次存檔的完整索引之上,不是憑空分析出來的。這也是為什麼新增的符號要等存檔才進得去:它們還沒有機會被寫進那份「previous complete index」。

換成實際工作流程來想:在 rust-analyzer 底下重新命名一個 struct,合理的推測是 IDE 會很快在所有引用它的地方標紅未更新的呼叫點——這是根據 salsa 增量重算機制推論出來的效果,官方文件那條不變量本身只保證函式本體內部的編輯不會拖累全域衍生資料,並沒有直接對「跨檔案 rename 多快能反映」下結論;在 Rust Glancer 底下,這個 struct 改名之後,除非按下存檔,否則其他檔案裡對它的呼叫還停留在改名前的索引狀態——IDE 顯示的是尚未更新的舊索引,容易誤判「沒有壞掉的地方」。

這種差異不會出現在任何 benchmark 報表裡,卻會出現在工程師的直覺裡:習慣 rust-analyzer 的人,看到補全或跳轉結果會直接當真;換到 Rust Glancer,可能得養成「改完記得先存檔再相信 IDE」的新習慣。

代價:一個「不完整」的 LSP 換來了什麼

作者自己把代價列得很清楚,沒有回避:「Rust Glancer is not a complete LSP yet, it has a lot of missing functionality」,加上「it has some known bugs, and it has a lot of things I want to improve」。這比行銷用語裡常見的「還在打磨細節」直白得多:功能缺、有 bug,講明白。

淺層分析加上凍結索引的組合,最容易踩到的應該是「索引與現實脫節」這一類 bug:某個型別已經在別的分支被改掉,但因為索引是存檔時才刷新,IDE 給出的補全建議或型別提示還停留在舊版本。這類 bug 不容易在單元測試裡抓到,因為它跟「程式碼本身對不對」無關,跟「IDE 有沒有看到最新版本的程式碼」有關——這是合理的推測,作者的部落格文章沒有具體列出 bug 清單。

效能上也有一個作者自己承認的天花板:「frozen workspace analysis is slower than lazy incremental by definition, since loading and deserializing data from filesystem is slower than loading from memory」。換句話說,省下來的記憶體,換的是每次查詢都可能要多付一次「從磁碟讀、反序列化」的成本——這筆帳在小 workspace 上不明顯,workspace 越大、查詢越頻繁,代價會越清楚。合理的推測是:這個代價會隨 workspace 規模拉大而變得更容易被察覺,但作者自己的部落格文章並沒有給出這條曲線的實測數字。

這個代價會在多大的 workspace 上被放大,作者沒有給出對照組——他自己的 benchmark 只測了索引時間,沒有測「打字延遲隨 workspace 大小變化」這條曲線。另一個該放進天平的變數是成熟度:rust-analyzer 是目前在意完整性與逐鍵正確性的專案的預設選擇;Rust Glancer 雖然作者已經拿來當日常編輯器用了大約一個半月,但公開發部落格文章、接受外界檢視才剛開始,兩者的踩坑歷史完全不對等。

有意思的是,兩邊在型別推論這塊其實沒有分道揚鑣——作者一開始想自己寫 trait 解析,後來「I gave up and integrated Chalk, which turned out to be significantly simpler」,chalk 正是 rust-analyzer 本身也在用的 trait solver。這代表記憶體與延遲上的差距,幾乎全部來自「資料放哪裡、什麼時候重算」這兩個架構決定,跟型別系統本身的複雜度關係不大。

怎麼選

作者自己把界線劃得很清楚,不打模糊仗:「It is highly unlikely that Rust Glancer will ever become just like rust-analyzer, but better.」他給出的建議是:「rust-analyzer will remain the default choice for projects that care about completeness and keystroke accuracy」。

對應到實際場景:如果你在意的是大型 workspace 上逐鍵級的補全與重構準確度——重構之後改的型別要馬上能被 goto definition 找到——照作者自己的預期,rust-analyzer 應該會繼續是這類專案的預設選擇。如果你面臨的問題是在小記憶體機器(8GB 等級的舊筆電、資源吃緊的容器裡)跑一個中大型 Rust workspace,願意接受「新符號要存檔後才進索引」這個限制換取記憶體降到一個數量級以下,Rust Glancer 值得先在旁邊跑一輪試試,再決定要不要換掉編輯器裡的 LSP。

具體一點分三種人看:長期維護大型 Rust monorepo、天天靠重構與跨模組補全過日子的團隊,rust-analyzer 沒有值得冒險替換的理由;獨立開發者在一台舊筆電上寫個人專案、常常同時開著瀏覽器與好幾個容器,記憶體吃緊比補全慢一點更痛,Rust Glancer 值得一試;如果專案本身就在頻繁重構、型別滿天飛的階段,「新符號要存檔才可見」這個限制會被放大到很煩,這種階段最好還是先留著 rust-analyzer。

把兩支 LSP 放在一起看,真正的分歧點在於「把 workspace 的分析結果算是快取還是算是狀態」,跟哪邊程式碼寫得比較好沒有關係——當成快取,就可以隨時丟掉、隨時重建,換來的是可以卸到磁碟;當成狀態,就必須維持一致性,隨時可能被查詢用到,也就只能留在記憶體裡。

Take-away:照 Rust Glancer 作者自己的判斷,多數在意重構準確度與逐鍵正確性的專案,rust-analyzer 依然是預設答案——常駐記憶體是它官方文件自己寫明、經過權衡的取捨;只有在記憶體真的吃緊、且能接受新符號存檔後才可見這個限制時,Rust Glancer 才值得換。