理解 Roslyn 中的紅綠樹(Red-Green Trees)

Roslyn 的 API 是非常易用的。即便如此複雜的 C# 語法,建立的複雜的 C# 語法樹,還有其複雜的樹遍歷和修改過程,也都被其 API 包裝得乾淨簡潔。

而這背後是它的重要設計思路 —— 紅綠樹。


紅綠樹的影子

如果你是通過搜索找到這篇文章的,那麼至少證明你調試過 Roslyn API 的使用,或者閱讀過 Roslyn 的源碼。因爲正常使用 Roslyn 的 API 時你是看不到紅綠樹的,這是 Roslyn 的實現細節。但你在調試的時候可能會看到 Green 屬性,或者在閱讀源碼時看到 GetRed 方法。

調試時看到的綠樹
▲ 調試時看到的綠樹

protected T GetRed<T>(ref T field, int slot) where T : SyntaxNode
{
    var result = field;

    if (result == null)
    {
        var green = this.Green.GetSlot(slot);
        if (green != null)
        {
            Interlocked.CompareExchange(ref field, (T)green.CreateRed(this, this.GetChildPosition(slot)), null);
            result = field;
        }
    }

    return result;
}

▲ Roslyn 中獲取紅樹的源代碼

源代碼摘抄自:roslyn/SyntaxNode.cs at master · dotnet/roslyn

Roslyn 的設計理念

Roslyn 一開始就將漂亮的 API 作爲目標的一部分,同時還要非常高的性能;所以 Roslyn 的開發團隊需要找到一種特殊的數據結構來描述語言(如 C#)的語法。這種數據結構要滿足這些期望的要求:

  • 不可變(Immutable)
  • 樹的形式
  • 可以容易地訪問父節點和子節點
  • 可以非常容易地將任何一個節點對應到源代碼文件的一段文本區間
  • 可重用(Persistent)

最後一個的英文說法是 Persistent,單詞的原本意思是“可持久的,連續的”,我把它翻譯爲“可重用”(Reusable)。Roslyn 的設計中有一個重要的業務需求,希望能夠分析源代碼文件並在開發者編輯的過程中不斷提供建議。也就是說,當我們連續不斷地去修改源代碼中的文本內容時,Roslyn 也需要具備很高的性能。如果每次編輯代碼都去重新解析一次整份源代碼,然後全部重新生成整個數據結構,那將是大量的性能浪費;更不可能實時去分析開發者編輯的源碼。所以,在 Roslyn 的設計中,希望源代碼文本改變時,整棵樹中的大多數節點都是能夠重複使用的(無需重新生成)。

而如果將數據結構設計成不可變的(Immutable),那麼重用這些節點將會非常容易。當然不止對於 Roslyn,對其它數據來說,不可變也一樣有各種好處;比如可以隨時重用這份數據的實例而不用擔心可能被各個不同的業務模塊意外修改,比如天然是線程安全的。

那麼問題來了,到底什麼樣的數據結構能夠在同時滿足以上所有的特點的前提下,同時還能設計出簡單易用的 API 呢?

  • 既然要容易地訪問到父節點和子節點,那麼我們是先構造父節點還是子節點呢?如果先構造父節點,那子節點還沒有創建出來;而先構造子節點,那父節點就沒構造出來。我們要求這樣的數據結構具有不可變性,所以我們不可能先把它們都構造出來再去修改它們的父子關係。
  • 還有,我們也不能隨意地去爲任何子節點指定新的父節點,因爲子節點是不可變的。然而我們同時有希望能夠在連續修改的情況下具備較高的性能,如果連修改父節點都不能辦到,那也很難重用之前的節點,最終不得不再次重新生成所有的子節點。
  • 另外,如果你在源代碼文件中插入了一個字符,那麼這個字符後面的每一個節點對應的源代碼區間都需要改變。然而這非常不利於連續修改,因爲隨便一個字符的插入都將導致更新大量節點中的文本區間信息。而由於不可變性,我們只能重新生成這些節點而沒法兒重用它們。

於是 Roslyn 團隊就折騰出了“紅綠樹”(Red-Green Trees)。

紅綠樹

紅綠樹並不是一棵樹,而是兩棵樹。

樹(the green tree)是不可變的,可重用的,沒有父節點的引用。綠樹的構建是自下而上的,每一個節點都保存它在文本區間中的字符個數(說通用點是寬度)。如果源代碼的內容被編輯,我們只需要重新創建受編輯影響的綠樹的部分;相比於重新分析整棵樹,其時間複雜度只有 O(log n)。

樹(the red tree)也是不可變的,是圍繞綠樹而建的外觀(參見 外觀模式)。紅樹的構建是自上而下的,但紅樹只在需要時纔會創建,而一旦編輯了源代碼文件,紅樹就直接丟棄不用了。如果有需要,紅樹就會開始創建;它會根據綠樹自上而下計算最新的父節點引用,計算節點最新對應的文本區間。

這兩棵樹設計起來協同工作,前者負責解決 Roslyn 語法分析的性能問題,後者負責對開發人員提供友好的 API 調用。由於最開始 Roslyn 團隊的大佬們在會議室討論時,前者是用紅筆畫的,後者是用綠筆畫的,於是就合在一起稱作“紅綠樹”。

自此,Roslyn 團隊設計出的這種數據結構滿足了以上所有的要求。不過,如果紅樹太大,每次重新生成依然會耗費比較多的性能。


參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章