這篇是我在知乎的回答,原文在這裏:justjavac: VS Code、ATOM這些開源文本編輯器的代碼實現中有哪些奇技淫巧?html
研究 V8 比較多,也關注了一下 vscode 和 atom 的性能,每次 vscode、atom 的 change log 我都會看一遍。印象最深的是 vscode 1.14 的一次更新日誌,doApplyEdits Lines inserted using splice · Issue #351 · Microsoft/monaco-editor:不要在循環中使用 splice。java
下圖是我一年前跑的測試結果:Inserting an array within an arraynode
300+倍的差距。git
在以前 vscode 還有一次很大的性能提高,在版本 1.9 的時候,改進了語法高亮的算法。程序員
語法高亮的過程一般分爲 2 個階段(tokenization 和 render):先將源碼分割爲 token,而後使用不一樣的主題對分割後的 token 進行着色。github
tokenization 的過程是:從上到下逐行運行。tokenizer 在行的末尾存儲一些狀態,在 tokenize 下一行時會用到這些狀態。這樣,在用戶進行編輯時僅須要從新 tokenize 行的一小部分,而不須要掃描整個文件內容。算法
還有一種狀況是當前行的輸入會影響到後面(甚至是前面)的行,這時會用到結束狀態:sql
在 1.9 以前的版本,vscode 如何 tokenization 呢?數據庫
好比上面的代碼:數組
在 vscode 種這樣存儲:
tokens = [ { startIndex: 0, type: 'keyword.js' }, { startIndex: 8, type: '' }, { startIndex: 9, type: 'identifier.js' }, { startIndex: 11, type: 'delimiter.paren.js' }, { startIndex: 12, type: 'delimiter.paren.js' }, { startIndex: 13, type: '' }, { startIndex: 14, type: 'delimiter.curly.js' }, ]
{ startIndex: 0, type: 'keyword.js' } 表示從 0 開始的 token 是一個 keyword。
VSCode 團隊在博客種指出這在 Chrome 中佔據 648 個字節,所以存儲這樣的對象在內存方面的代價很是高(每一個對象實例必須保留指向其原型的空間,以及其屬性列表等)。爲 15 個字符存儲 648 字節是不可接受的。
因此,vscode 使用二進制來存儲token:
// 0 1 2 3 4 map = ['', 'keyword.js', 'identifier.js', 'delimiter.paren.js', 'delimiter.curly.js']; tokens = [ { startIndex: 0, type: 1 }, { startIndex: 8, type: 0 }, { startIndex: 9, type: 2 }, { startIndex: 11, type: 3 }, { startIndex: 12, type: 3 }, { startIndex: 13, type: 0 }, { startIndex: 14, type: 4 }, ]
和上面的表示法相比,只是把 type 由字符串變成了數字,本質上並無節約太多的內存。可是彆着急,vscode 還有黑科技。
咱們都知道 JavaScript 使用 IEEE-754 標準存儲雙精度浮點數,尾數爲 53bit。可以在不丟失精度的狀況下處理的最大整數爲 2^53-1。所以 vscode 使用其中的 48big 進行編碼:使用 32bit 來存儲 startIndex,16bit 來存儲type。 因而上面的對象在 vscode 種被存儲爲:
tokens = [ // type startIndex 4294967296, // 0000000000000001 00000000000000000000000000000000 8, // 0000000000000000 00000000000000000000000000001000 8589934601, // 0000000000000010 00000000000000000000000000001001 12884901899, // 0000000000000011 00000000000000000000000000001011 12884901900, // 0000000000000011 00000000000000000000000000001100 13, // 0000000000000000 00000000000000000000000000001101 17179869198, // 0000000000000100 00000000000000000000000000001110 ]
每一個數字是 64bit(8字節),一共是 7 個數字,存儲這些元素一共須要 7*8 = 56 字節,再加上數組的額外開銷共須要 104 個字節,只有以前的 648 字節的 1/6。
而主題的渲染則用到了 Trie 數據結構。
這個學過《數據結構》的都懂,算不上奇技淫巧,就不展開了。
這一切都是 2017 年 3 月發佈的 vscode 1.9。
而今年 3 月,vscode 又重寫了 Text Buffer。用戶使用編輯器,大部分時間就是寫新代碼,改舊代碼,說到底仍是對 text 進行編輯。
對於高性能的文本操做,vscode 最初嘗試使用 C++ 進行編寫,畢竟 C++ 的性能要比 JavaScript 高出很多,可是事實卻不夠理想,使用 C++ 確實節約了內存,可是在使用 C++ 模塊時,須要在 JavaScript 和 C++ 之間往返數次,這大大減慢了 vscode 的性能。
vscode 團隊從 Vyacheslav Egorov 的一篇文章 Maybe you don't need Rust and WASM to speed up your JS 收到了啓發,如何充分壓榨 V8 引擎的性能。mrale.ph 的博客我幾乎每篇都看,很是經典,也很是難懂 。
大多編輯器都是基於行的。程序員逐行編寫代碼,編譯器提供基於行的反饋信息,堆棧跟蹤包含行號,tokenization 引擎逐行運行…… 在 vscode 的早期版本中也是直接把每行代碼做爲字符串存儲在數組中。
可是這種方式存在一些問題:
因而 vscode 開始尋找新的數據結果,最終選擇了 Piece table。不知道爲何這麼晚才選擇 piece table,要知道在微軟的 office word 中早就已經使用了 piece table。我也是在一次 Java 讀取 word 的 jar 包源碼中第一次知道的 piece table 數據結構。
推薦幾篇延伸閱讀的文章:
目前主要的三種編輯方式有 gap buffer, rope, piece table。
最近用 Atom 少了。
上一次讓我興奮的地方是:The State of Atom's Performance。在2017年6月 Atom 使用了 piece table 數據結構,使用 C++ 從新實現了 text buffer:Atom's new concurrency-friendly buffer implementation。比 vscode 還要早半年,可是爲何仍是這麼慢呢???
Atom 使用 V8 的自定義快照(snapshot)提高啓動性能,最終刪除了影響性能的 jQuery 和自定義 element。就連 V8 的
Atom 還更新了 DOM 渲染的方式:A new approach to text rendering,而這個新算法包括一個相似 React 的 vdom,從 issue 來看這是一個大工程啊,包含了近 100 個 task
通過一系列優化,官方說道:
we made loading Atom almost 50% faster and snapshots were a crucial tool that enabled some otherwise impossible optimizations.
咱們使 Atom 快了 50%,snapshot 功不可沒。(PS:我必定是使用了假的 Atom)
不過 snapshot 確實是 V8 的神器,Nodejs 也看到了 Atom 的成果,於 2017-11-16 開了 issue :speeding up Node.js startup using V8 snapshot · Issue #17058 · nodejs/node。這在我以前的專欄裏面有介紹:Node.js 新計劃:使用 V8 snapshot 將啓動速度提高 8 倍。
最近一次關注 Atom 是 atom/xray。知乎上也有相關的討論,atom 開發的下一代編輯器(莫非已經定義 atom 爲上一代編輯器了嗎)。大概就是一種「大號廢了,開小號重練」的感受。
值得學習的地方是 text 處理使用 copy-on-write CRDT:
若是一直關注 Atom,對於 CRDT 應該不會陌生。Atom 的多人實時共同編輯插件 https://teletype.atom.io/ 就是使用的 CRDT。
CRDT 全稱:Conflict-Free Replicated Data Types,強行翻譯過來就是「無衝突可複製數據類型」。
CAP定理:在分佈式系統中,最多隻能同時知足一致性(Consistency)、可用性(Availability)和分區容錯性(Partition tolerance)這三項中的兩項。
不少分佈式系統都捨棄了C(一致性):容許能夠在某些時刻不一致,轉而求其次要求系統知足最終一致性。這也是目前不少 nosql 數據庫追求的方式(另外一種是傳統的符合 ACID 特性的數據庫系統,放棄了A(可用性),這種系統稱爲強一致性)。
而在最終一致性分佈式系統中,一個最基本的問題就是,應該採用什麼樣的數據結構來保證最終一致性? 答案就是 CRDT。
這篇文章只是一個提綱,裏面的每一個知識點均可以展開了講上三天三夜。