限制管理
本文討論模組分解的策略問題。
想起要寫它,是上週寫這個推演時引入的概念:in nek:從CPU和TPU的不同語言抽象看抽象原則 。當時討論到這樣一個現象:TPU/NPU的程式設計師給了一個請求序列給TPU或者NPU,如果要求指定得太多,編譯器就失去了優化的機會了。這個問題無處不在,也不需要一定是TPU,我們用給一些更簡單的例子來類比。比如說,你有這樣一個執行序列:
def add_all(a, b, c): x=a x+=b x+=c return x;
其實你關心的是把三個輸入引數加在一起,但你的執行序列要求比這個要求多出了很多東西,比如:
- 你指定了臨時變數:
- 你指定了執行順序(先加a,b,再加c)
這些可能不是並非你的需求,但執行者不知道,它不一定可以自作主張幫你自動把臨時變數去掉,或者把你的執行順序打亂——說不定你別有深意呢?比如x是個暫存器變數,這樣加比直接把兩個記憶體的值相加快很多——反正只要體系結構足夠奇葩,什麼考量都有可能的。
從這裡可以看出來,需求提出方提供的“指定”越多,提得越精確,執行方的效率就越低。如果軟體給編譯器提供的執行要求僅僅是add_all(a, b, c),編譯器就有餘量,只保證a, b, c相加的結果就可以了,如果軟體直接提供了“如何計算這個a, b, c的方法”,編譯器(或者說CPU),就不能怎麼樣了,只能一步步計算,也許明明CPU可以一條指令就搞定3個數相加的,也只能變成兩個兩個來完成了。
一般模組也普遍存在這種情況,比如你要壓縮一個檔案,你直接給我整個檔案的路徑,壓縮模組說不定可以直接從OS內部把資料餵給硬體加速器,然後流式回寫到磁碟上。但你非要把它從檔案系統讀到使用者態,再分成一塊一塊餵給壓縮模組,壓縮模組就只好用CPU一塊一塊給你單獨壓(由於哈夫曼樹在塊之間不能共享,這個壓縮率也會下降),這個效率就低了。
所以,我們首先有第一個認識,高層設計,並非越精確越好的,精確意味著更多的限制,更多的限制破壞了下一層設計的自由度,導致下一級無法進行優化。
所以,“極致的最自由的”高層設計是僅提需求。比如我要做一個壓縮軟體,收集了需求,直接設計成:“實現命令compress,把stdin的資料流壓縮到stdout”。這樣,壓縮命令的實現者就有最大的自由度,根據最好的方法實現整個壓縮功能。
問題是下一層設計怎麼辦?你不可能一次就實現到具體的程式碼,也不可能仍告訴下層“實現命令compress...”,那麼下一層的切分在哪裡?這是本文主要想討論的問題。
我們從另一個層面考量這個問題:每層設計,本質上都是邏輯鏈(“先XX,再XX,遇到XX則XX……”),而邏輯鏈是沒有根的,整個邏輯鏈最前面的節點(“因為”),其實都是“限制”:因為使用者要求壓縮stdin,所以我們必須讀入stdin的資料。讀入資料需要緩衝區,所以我們需要緩衝區,但因為我們使用的平臺的緩衝區是有限的,所以一次讀入的資料不能超過4M,因為使用者的提供的輸出可能超過4M,所以每次讀到的資料超過4M後,就要啟動壓縮演算法……”。你看,其實我們的邏輯鏈全部都是建立在“限制”上的。
而前面提到的下層軟體的優化,其實也是限制:本來我把要相加的3個數都給你了,你不能三個一起加,非要兩個兩個地加,這是你下層模組的“限制”。
好了,現在的問題是,限制其實一直在那裡,我們是在上層認知這個限制呢?還是在下層認知這個限制?答案就回到原來的高內聚低耦合這個問題了,也就是說:相關的限制,一體處理,介面承載介面雙方的共同限制。
所以呢,我推演了半天,看來是什麼結論都沒有。但它有幾個邊界效應,它讓我們認識到:
- 我們聚合一個模組的關注重點不是聚合它的實現,而是聚合它的限制
- 我們決定一個邏輯在某個模組中實現,不是因為它“名義上”屬於這個模組,而是它的邏輯鏈根植於這個模組所管理的限制。只有這樣,我們才不為整個系統引入多餘的“限制”,導致系統最終自相矛盾,無法再加入新的需求(本質也是“限制”)
- 當我們進行介面設計的時候,介面製造的“限制”,必須是介面雙方邏輯鏈中的限制
這樣,我們就可以解釋上一個文件中提到的那些介面問題,例如為什麼我們不能接受直接把執行緒作為TPU的輸入——因為TPU內部記憶體如何使用,是TPU內部的限制,是TPU的靈活性的範圍,CPU本身提交需求的時候,並沒有這個限制,所以,在介面上引入這個限制,是不利於介面發展的。