本文來(lái)自微信公眾號(hào):CSDN(ID:CSDNNews)
2018 年年底,C++ 標(biāo)準(zhǔn)委員會(huì)歷史上規(guī)模最大的一次會(huì)議在美國(guó) San Diego 召開(kāi),討論了哪些特性要加入到 C++20 中。其中,Modules 便是可能進(jìn)入 C++ 20 的一大重要特性:
“一直以來(lái) C++ 一直通過(guò)引用頭文件方式使用庫(kù),而其他90年代以后的語(yǔ)言比如 Java、C#、Go 等語(yǔ)言都是通過(guò) import 包的方式來(lái)使用庫(kù)?,F(xiàn)在 C++ 決定改變這種情況了,在 C++20 中將引入 Modules,它和 Java、Go 等語(yǔ)言的包的概念是類(lèi)似的,直接通過(guò) import 包來(lái)使用庫(kù),再也看不到頭文件了?!?/p>
然而就是這一特性,前段時(shí)間在 Twitter 上引發(fā)了不小的討論。再加上諸多其他問(wèn)題,“C++ 20 還未發(fā)布就已涼涼”的論調(diào)也早有苗頭。C++ 模塊化,究竟是問(wèn)題多多的無(wú)用嘗試,還是如期待般能帶來(lái)其承諾的性能升級(jí)呢?
以下為譯文:
C++ Modules(模塊化)被視作 C++ 自誕生以來(lái)最大的變化,其設(shè)計(jì)有幾個(gè)基本目標(biāo):
1. 自頂向下隔離:模塊的“導(dǎo)入程序”不能影響正在導(dǎo)入的模塊的內(nèi)容。導(dǎo)入源中編譯器(預(yù)處理器)的狀態(tài)與導(dǎo)入代碼的處理無(wú)關(guān)。
2. 自下而上隔離:模塊的內(nèi)容不會(huì)影響導(dǎo)入代碼中預(yù)處理器的狀態(tài)。
3. 橫向隔離:如果兩個(gè)模塊由同一個(gè)文件導(dǎo)入,則它們之間不會(huì)“串?dāng)_”。導(dǎo)入語(yǔ)句的順序無(wú)關(guān)緊要。
4. 物理封裝:只有模塊顯式聲明為導(dǎo)出的實(shí)體才會(huì)對(duì)使用者可見(jiàn)。模塊中未導(dǎo)出的實(shí)體不會(huì)影響其他模塊中的名稱查找(除了 ADL 可能有一些不同之處(依賴實(shí)參的名字查找),但這就說(shuō)來(lái)話長(zhǎng)了)。
5. 模塊化接口:強(qiáng)制任何給定模塊的公共接口在稱為“模塊接口單元”(MIU)的單個(gè) TU 中聲明。模塊接口子集的實(shí)現(xiàn)可以在稱為“分區(qū)”的不同 TU 中定義。
如果你期望 Modules 可以像 C++ 的許多其它功能一樣經(jīng)久不衰,那么你會(huì)注意到上面這個(gè)列表中缺少了“編譯速度”。然而,這是 C++ Modules 模塊最大的承諾之一。模塊帶來(lái)的速度提升可能就是歸功于上面的設(shè)計(jì)。
下面我列出從 Modules 設(shè)計(jì)中受益匪淺的 C++ 編譯的幾個(gè)方面,按照從最明顯到最不明顯的順序:
1. 標(biāo)記化緩存(Tokenization Caching):由于 TU 的隔離,當(dāng)模塊后面導(dǎo)入另一個(gè) TU 時(shí),可以緩存已經(jīng)標(biāo)記化的 TU。
2. 解析樹(shù)緩存(Parse-tree Caching):和標(biāo)記化緩存一樣。標(biāo)記化和解析是 C++ 編譯中開(kāi)銷(xiāo)最大的操作之一。我自己的測(cè)試顯示,對(duì)于具有大量預(yù)處理輸出的文件,解析可能會(huì)占用高達(dá) 30% 的編譯時(shí)間。
3. 延遲重編譯(Lazy Re-generation):如果 foo 導(dǎo)入了bar,然后我們修改了 bar 的實(shí)現(xiàn),我們可以不需要對(duì) foo 立即重新編譯。只有對(duì) bar 接口修改后才需要重新編譯 foo。
4. 模板專(zhuān)門(mén)化:這一點(diǎn)比較微妙,可能需要更多的工作來(lái)實(shí)現(xiàn),但潛在的加速是巨大的。簡(jiǎn)而言之,模塊接口單元中出現(xiàn)的類(lèi)或函數(shù)模板在經(jīng)過(guò)專(zhuān)門(mén)化處理后可以在磁盤(pán)上緩存并供后續(xù)需要時(shí)加載。
5. 內(nèi)聯(lián)函數(shù)代碼復(fù)制緩存:內(nèi)聯(lián)函數(shù)(包括函數(shù)模板和類(lèi)模板的成員函數(shù))的代碼復(fù)制結(jié)果可以緩存,然后由編譯器后端重新加載。
看上去模塊設(shè)計(jì)相當(dāng)不錯(cuò),不是嗎?
但是我們都忽略了一個(gè)非??膳虑覙O為糟糕的缺陷。
還記得…… Fortran 嗎?
FORTRAN 實(shí)現(xiàn)了與 C++ 的設(shè)計(jì)有點(diǎn)相似的模塊系統(tǒng)。幾個(gè)月前,SG15 工具研究小組在圣地亞哥提交了一篇文章(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1300r0.pdf),據(jù)我所知,這篇文章迄今為止沒(méi)有得到任何相關(guān)人士的討論和評(píng)論。
文章要點(diǎn)摘錄如下:
1. 我們有模塊 foo 和 bar,分別由 foo.cpp 和 bar.cpp 定義。
2. bar.cpp 里有 import foo; 語(yǔ)句。
3. 在編譯 bar.cpp 時(shí),如何確保 import foo 被解析?當(dāng)前的設(shè)計(jì)和實(shí)現(xiàn)有一個(gè)為 foo 定義的所謂“二進(jìn)制模塊接口”(簡(jiǎn)稱BMI)。這個(gè) BMI 是文件系統(tǒng)中描述模塊 foo 導(dǎo)出接口的文件。我就叫它 foo.bmi, 文件擴(kuò)展名在這里無(wú)所謂。
4. foo.bmi 是編譯 foo.cpp 的副產(chǎn)品。編譯 foo.cpp 時(shí),編譯器將生成 foo.o 和 foo.bmi。因此,必須在 bar.cpp 之前編譯 foo.cpp!
趁著警鈴還沒(méi)有拉響,我們來(lái)討論一下我們目前使用頭文件的工作方式:
1. 我們有一個(gè)模塊 foo,由 foo.cpp 和 foo.hpp 定義; 和另一個(gè)模塊 bar,由 bar.cpp 和 bar.hpp 定義。
2. bar.cpp 中有 #include
3. 在編譯 bar.cpp 時(shí),如何確保 #include
4. 對(duì)模塊 foo 和 bar 的編譯沒(méi)有次序要求,可以并行處理。
并行化可能是提高 build 性能最重要的方面。優(yōu)化 build 時(shí),你無(wú)需再考慮并行化,因?yàn)樗呀?jīng)存在了。
模塊改變了這一點(diǎn)。模塊的導(dǎo)入導(dǎo)致了一個(gè)編譯時(shí)間的依賴項(xiàng),這在 #include 語(yǔ)句中并沒(méi)有體現(xiàn)。(關(guān)于模塊編譯的次序問(wèn)題,可參考:https://vector-of-bool.github.io/2018/12/20/build-like-ninja-1.html)。
Rene Rivera 最近在《Are modules fast?》(https://bfgroup.github.io/cpp_tooling_stats/modules/modules_perf_D1441R1.html)一文中探討了這種設(shè)計(jì)的后果。
劇透一下 Rene 文章的結(jié)論:答案是否定的,或者更準(zhǔn)確一點(diǎn)來(lái)講,這很微妙,但大多數(shù)情況下答案仍然是不。這篇文章中使用的當(dāng)前模塊實(shí)現(xiàn)是非常原始的,但仍然在了解哪些模塊看上去對(duì)性能有幫助這方面有一定的參考價(jià)值??梢云诖?,隨著硬件并行性的提升,header 的引導(dǎo)模塊變得越來(lái)越重要,而且與 DAG 深度(即互相導(dǎo)入的模塊鏈的長(zhǎng)度)也有關(guān)系。隨著 DAG 深度的增加,模塊會(huì)越來(lái)越慢,而 header 則保持相當(dāng)穩(wěn)定,即使是對(duì)于接近 300 的“極端”深度。
一個(gè)徒勞的掃描任務(wù)
假設(shè)我有下面的源文件:
這很簡(jiǎn)單。因?yàn)槲覀儗?dǎo)入了一些模塊,所以我們需要先編譯 greetings 和 std.iostream,然后才能編譯這個(gè)文件。
那么,讓我們來(lái)……
emmm……
怎么啦?
我們只有一個(gè)包含兩個(gè) import 的源文件,僅此而已,別無(wú)他物。我們不知道 greetings 是在哪里定義的,我們需要找到這個(gè)包含 module greetings; 語(yǔ)句的文件。
在銀河系另一側(cè)的 talk.cpp 文件看起來(lái)很可能是:
它定義了我們想要的 greeting::english 函數(shù)。但是我們?cè)趺粗肋@是正確的文件呢?它并沒(méi)有 module greetings; 這一行!
但它某些時(shí)候確實(shí)是我們要的。當(dāng)我們使用 -DFROMBULATE 編譯時(shí),文件 hello.h 會(huì)被粘貼到源文件中。讓我們看看 hello.h 里面有什么?
Oh no!
好吧好吧……別擔(dān)心。我們需要做的就是……運(yùn)行預(yù)處理器來(lái)檢查文件中是否出現(xiàn) module salutations 或 module greetings。
這是可以的,但是有 4201 個(gè)文件可以定義可以被導(dǎo)入的模塊,其中任何一個(gè)都可能有 module greetings;。
另外,我們還不能使用自己的預(yù)處理器實(shí)現(xiàn),需要精確地運(yùn)行編譯這段代碼的預(yù)處理器??吹?__SOME_BUILTIN_MACRO__ 了嗎?我們不知道那是什么。如果我們沒(méi)有正確地對(duì)它進(jìn)行編譯,編譯就會(huì)失敗。更糟的是,我們甚至可能會(huì)錯(cuò)誤地編譯此文件。
那么我們能做什么呢?我們可以在預(yù)處理完所有文件后緩存所有模塊的名稱,對(duì)嗎?那么,我們?cè)谀睦锎鎯?chǔ)這個(gè)映射表呢?當(dāng)我們想用一個(gè)不同的編譯器編譯,生成不同的映射表時(shí)會(huì)發(fā)生什么?如果我們添加需要掃描的新文件怎么辦?為了檢查任何模塊是否添加、刪除或重命名了,我們是否需要在每次構(gòu)建時(shí)搜索這些包含了數(shù)千個(gè)源文件的所有目錄?在那些啟動(dòng)進(jìn)程和/或訪問(wèn)文件需要較大開(kāi)銷(xiāo)的系統(tǒng)上,這些成本也將會(huì)疊加上去。
可能的解決方案
這兩個(gè)問(wèn)題雖然不同,但卻是相關(guān)的,我(和許多其他人)認(rèn)為模塊設(shè)計(jì)的一個(gè)改變可以解決這兩個(gè)問(wèn)題, 那就是模塊接口單元的位置必須是確定的。
有兩種備選方案可以實(shí)施:
1. 強(qiáng)制從模塊名稱派生 MIU 文件名。這模擬了頭文件名的設(shè)計(jì),它與如何從 #include 指令中找到頭文件名直接相關(guān)。
2. 提供一個(gè)“manifest”或“mapping”文件,描述基于模塊名的 MIU 文件路徑。此文件需要用戶提供,否則我們將同樣遇到上文描述的掃描問(wèn)題。
有了確定且易于定義的 MIU lookup(查詢),我們就可以進(jìn)入下一個(gè)必要步驟:必須延遲生成模塊的 BMI。
TU 之間的編譯順序?qū)⒍髿?module adoption 的進(jìn)程。即使是相對(duì)較淺的 DAG 深度也比與頭文件相同的深度慢得多。唯一的答案是 TU 編譯必須是可并行的,即使是導(dǎo)入其他 TU 時(shí)。
在這方面,C++ 最好模仿 Python 的導(dǎo)入實(shí)現(xiàn):當(dāng)遇到新的導(dǎo)入語(yǔ)句時(shí),Python 將首先找到對(duì)應(yīng)于該模塊的源文件,然后以確定性的方式查找預(yù)編譯的版本。如果預(yù)編譯版本已經(jīng)存在并且是最新的,就使用它;如果不存在預(yù)編譯版本,則將編譯源文件,并將生成的字節(jié)碼寫(xiě)入磁盤(pán)。然后加載此字節(jié)碼。如果兩個(gè)解釋器實(shí)例同時(shí)遇到同一個(gè)未編譯的源文件,它們將競(jìng)爭(zhēng)寫(xiě)字節(jié)碼。不過(guò),競(jìng)爭(zhēng)并不重要,它們都會(huì)得出相同的結(jié)論,并將相同的文件寫(xiě)入磁盤(pán)。
為了方便 DAG 中 TU 的并行編譯,C++ 模塊必須以相同的方式實(shí)現(xiàn)。提前編譯 BMI 是不可能的。相反,當(dāng)編譯器第一次遇到有關(guān)模塊的 import 語(yǔ)句時(shí),應(yīng)該延時(shí)生成 BMI。Build 系統(tǒng)根本不應(yīng)該與 BMI 有關(guān)。
只有當(dāng)一個(gè) MIU 的位置對(duì)于編譯器是確定的時(shí)候,以上這些才能實(shí)現(xiàn)。
前景渺茫
前段時(shí)間,Twitter 上發(fā)生的事讓人心煩意亂。Kona 會(huì)議前的郵件列表在 1 月 25 日開(kāi)放了。在發(fā)布的許多文章中,有一篇《關(guān)注模塊的工具能力(Concerns about module toolability)》(http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1427r0.pdf),其作者和貢獻(xiàn)者名單中很多是來(lái)自業(yè)界的系統(tǒng)和工具構(gòu)建工程師。我想呼吁權(quán)威人士的關(guān)注,但我覺(jué)得這份名單中的人才是最有資格提供 module toolability 反饋的人。
SG15 之外的人一直熱衷于反駁關(guān)于 module toolability 問(wèn)題的討論,他們聲稱 SG15 缺乏必要的實(shí)現(xiàn)經(jīng)驗(yàn),無(wú)法對(duì)模塊這個(gè)話題提出有用的建議。
SG15 只搞過(guò)面對(duì)面的會(huì)議,上次在圣地亞哥的會(huì)議也沒(méi)起到什么作用,因?yàn)橹飨辉?,而且大家急急忙忙參?huì),沒(méi)時(shí)間進(jìn)行任何有用的討論。由于在官方的 WG21 會(huì)議之外沒(méi)有安排 SG15 會(huì)議,因此其成員很難保證更新并協(xié)同工作。此外,SG15 曾多次嘗試重提已經(jīng)被拒絕的問(wèn)題,被拒絕的原因是因?yàn)樗麄兲岢龅膯?wèn)題被認(rèn)為“超出了 C++ 語(yǔ)言范圍”。
關(guān)于 Kona 會(huì)議前郵件列表的推文催生了關(guān)于 C++ 模塊化的討論:關(guān)于 module toolability,該相信誰(shuí)?(https://twitter.com/horenmar_ctu/status/1089542882783084549)。
這場(chǎng)討論最終以要求 SG15 “他媽的閉嘴”而告終,除非 SG15 能夠提供代碼示例來(lái)證明它們所提到的問(wèn)題。但是這個(gè)示例代碼,無(wú)法在當(dāng)前的任何編譯器中實(shí)現(xiàn),也不能在任何當(dāng)前的構(gòu)建系統(tǒng)中實(shí)現(xiàn)。所以即使這些問(wèn)題確實(shí)存在,這個(gè)要求也只能得出一個(gè)否定的結(jié)論,因?yàn)檫@是一個(gè)無(wú)法憑經(jīng)驗(yàn)完成的任務(wù)。也就是說(shuō),要求 SG15 提供代碼根本是一個(gè)無(wú)法永遠(yuǎn)完成的任務(wù)。
這些問(wèn)題沒(méi)有繼續(xù)討論下去,也沒(méi)有被推翻。甚至沒(méi)有人再提到 《關(guān)注模塊的工具能力》中列出的問(wèn)題。我們只是被簡(jiǎn)單地告知要相信一些大人物比我們更了解 C++ 模塊(這里我要再次呼吁權(quán)威人士介入)。
支持目前模塊設(shè)計(jì)的人尚未證明模塊能適應(yīng)大規(guī)模生產(chǎn)環(huán)境,但是他們卻要求 SG15 提供模塊不能滿足大規(guī)模生產(chǎn)的證據(jù)。盡管已有的模塊部署并沒(méi)有使用當(dāng)前的設(shè)計(jì),也沒(méi)有使用真實(shí)環(huán)境中構(gòu)建實(shí)際系統(tǒng)所需的自動(dòng)模塊掃描。
如果模塊被合并,結(jié)果發(fā)現(xiàn)它們不能以良好的性能和靈活的方式實(shí)現(xiàn),那么人們就不會(huì)使用模塊。如果一個(gè) broken module 建議被合并到 C++ 中,后果可能是不可彌補(bǔ)恢復(fù)的,C++ 也將永遠(yuǎn)得不到模塊設(shè)計(jì)承諾帶來(lái)的好處。
至于針對(duì)當(dāng)前模塊設(shè)計(jì)的改進(jìn)方案能成功解決這些問(wèn)題呢?我不能給出確定的答案,但我和許多人都認(rèn)為 C++ Modules 有重大問(wèn)題需要解決。
然而,從其他人的做法來(lái)看,SG15 怎么想似乎并不重要,他們的提議總是被缺乏 C++ 工具經(jīng)驗(yàn)的人否決, 他們?cè)谡麄€(gè)討論中沒(méi)有任何發(fā)言權(quán),提出的任何問(wèn)題都被認(rèn)定為“未經(jīng)證實(shí)”和“超出范圍”而不予考慮。
我不太敢指責(zé)這種行為的后果,我也并不熱衷“人際沖突”。然而,我更擔(dān)心 C++ 這個(gè)無(wú)用的模塊設(shè)計(jì)最終會(huì)害死自己。
原文:https://vector-of-bool.github.io/2019/01/27/modules-doa.html
本文來(lái)自微信公眾號(hào):CSDN(ID:CSDNNews)