Bjarne訪談:抽象與效率

提升抽象的層面 

Bill Venners(以下簡稱Bill):我最初是從Borland的一個教學錄像“World of C++"”開始學習C++的。在該錄像開頭的簡短片斷中,你說你正致力於C++方面的工作是提升編程過程中的抽象層面。 

Bjarne Stroustrup(以下簡稱BS):是的。 

Bill提升抽象層面意味着什麼呢,一個高的抽象層面爲什麼會有良好 的表現呢? 

BS較高的抽象層面不僅僅在C++中表現良好,在普遍情形下都具有良好表現。我們希望能夠在考慮問題的層面上來處理這些問題。如果我們能夠以此種方式來處理這些問題,那麼在理解這些問題的方式與實現其解決方案的方式之間將不存在鴻溝。我們能夠比較容易地理解別人的代碼,而不必使自己猶如編譯器 一般。    

抽象是一種理解事物的機制。例如,用數學方式來表達一個解決方案則意味着我們真正理解了這一問題。我們不是推出大量的方法針對於特定的情形進行試驗。人們經常會僅僅針對一個特定的問題提出解決方案。但是,除非我們試着對問題進行歸納概括並將一個問題看作一類普遍問題的範例,那麼我們將會遺漏掉“針對於我們特定問題的解決方案中的”重要部分,而且不可能找到將來對我們具有幫助的概念和一般性 的解決方案。如果某些人具有一條理論,例如有關矩陣操作的一條理論,那麼你就可工作於這些概念的層面之上,而且你的代碼將變得簡潔、清晰且正確的可能性會更大,只需編寫少量的代碼,而且便於維護。 

我相信提升抽象層面對於所有實際的科學探究都具有基礎性的作用。儘管我不認爲提升抽象層面是一個有爭議的觀點,但是由於認爲較高抽象層面的代碼呈現出不必要的低效率,所有人們有時會認爲這是一個存在爭議的觀點。例如,我兩天前收到了一個聽過我報告的人 發來的電子郵件,在這次報告中我倡導使用一個具有適當線性代數支持的矩陣庫。他說,“使用矩陣庫與直接使用數組相比所耗用的資源要多多少?我不敢確定我能承受得起這一耗用。”令他非常驚異的是,我的答案是,“如果你想達到我所指出的效率,你不能直接使用數組。” 

比最快的代碼還要快的代碼就是根本沒有代碼。通過對矩陣處理操作的抽象,你可爲編譯器提供足夠的類型信息,從而能使它免除許多操作。如果你正在數組的層面編寫代碼,除非你比所有的人都聰明,否則你將無法免除這些操作。所以如果你使用的是數組而不是矩陣庫,那麼你不僅要編寫多出十倍的代碼,而且還不得不接受一個運行得非常緩慢的程序。通過在我們理解事物的層面進行操作,有時你也可以在我們分析代碼的層面執行操作 (在第二種情形下我們是作爲編譯器,並得到更好的代碼)。 

針對於該現象我最鍾愛的兩個例子爲:矩陣乘以向量的操作,其中C++有機會擊敗Fortran;簡單排序 ,其中C++有機會擊敗C。這兩種情形的原因都是因爲你已經將程序表達得如此直接、如此清晰,以至於類型系統能夠幫助生成更好的代碼 。除非你擁有一個適當的抽象層面,否則你將不能實現這一效果。你將遇到其中你的代碼變得更加清晰、更加簡潔、更加快速的美妙情形。當然這種情形不是時時刻刻都會發生, 不過一旦發生它將是非常得優美。

編程即是理解 

Bill在有關靜態類型 和動態類型的爭論中,強類型的支持者通常聲稱,儘管一個動態類型語言能夠幫助你快速地檢查一個原型,但是爲了創建一個健壯的系統你還需一個靜態類型語言。相反,我從你的談論和書籍中所得到有關靜態類型的主要消息卻是靜態類型能夠幫助一個優化器更加有效地工作。那麼在你的觀點中,在C++中以及一般性問題中,靜態類型的益處是什麼呢? 

BS存在兩個益處。首先,我認爲在靜態類型程序中你能夠更好地理解程序。如果我們能夠說出“你可執行於一個整數上的特定操作”而且這就是一個整數,那麼我們就能夠確切獲悉現在所發生的一切。 

Bill當你說我們可以獲悉正在發生的一切時,你指的是程序員還是編譯器? 

BS程序員。 我傾向於更人性化一些。 

Bill使程序員 更人性化一些? 

BS使編譯器 更人性化一些。我願意這麼做的部分原因在於它具有誘惑力,部分原因在於我已經編寫過編譯器。所以作爲程序員,我感覺我們能夠更好理解一個靜態類型語言所發生的事情。 

在一個動態類型語言中,你在執行一個操作時基本上可認爲對象的類型在操作處是有意義的,否則你必須在運行期間來處理這一問題。現在,如果你的程序正在運行,而你正坐在一個終端前調試你的代碼,那麼這可能就是一個查找bugs的好方法。這將具有精確的快速反應時間,而且如果你發現一個操作不能運行,就會發現自己正猶如調試器一樣。一切情形將是良好的。如果程序員在工作時,能找到所有的bugs,這一切將非常良好 ,但對於大量的實際程序,你並不能以這種方式找到所有bugs。如果當程序員不在場時bugs顯露出來,那麼將遇到一個難題。我已經做了大量關於“應當運行在諸如電話交換機 之類的地點中”的程序的工作。在這些環境中,不發生意想不到的事情是至關重要的。在嵌入式系統中同樣也是如此。在這些環境中,如果一個bug使人們置身於一個調試器狀態,那麼沒人能夠知道該如何應對。 

利用靜態類型,我發現能夠更容易編寫代碼,更容易理解代碼,更容易理解別人的代碼,因爲他們想要說明的事情是用語言中具有良好定義的語義進行表達的。例如,如果我指定我的函數帶有一個Temperature_reading類型的參數,那麼用戶就無須再來查看我的代碼以便決定我需要何種類型的對象,只要查看一下接口就可以解決問題。如果用戶給我一個錯誤類型的對象,那麼我也無需檢查,因爲編譯器將會拒絕Temperature_reading類型以外的任何參數。無需進行任何轉型操作我就可直接將我的參數用作一個Temperature_reading。另外 我還發現開發靜態類型接口是一個很好的習慣。如果一定要使我考慮什麼是本質的東西,而不是僅僅將所有東西用作參數和返回值,從而變得似是而非,那就是希望調用者和被調用者能夠達成一致並都編寫必要的運行期檢查。 

正如Kristen Nygaard所說的那樣,編程即是理解(programming is understanding),意思是,如果你對一些事情不理解,你就不能對其進行編程,對其進行編程就應當獲取對其的理解。這也是我的《The C++ Programming Language》第三版的前言。這是非常基本的東西,在你知道你擁有一個整數vector而不是一個指向對象的指針的地方,將會更容易讀懂一塊代碼。當然,你可詢問對象是否爲一個vector,如果它是一個vector,你可詢問它是否容納一些整數、一些字符串 或一些圖形。如果你需要此類容器,你可創建它們,但是我認爲你應當選擇同類的vectors,即容納某一特定類型的vectors而不是一個容納常規對象的常規集合。爲什麼呢?它實際上是那些需要靜態檢查接口的參數的一個變體。如果我擁有一個vector<Apple>,那麼我就知道它的元素是Apples。我無需將一個Object轉型爲一個Apple來使用它,而且我也不用擔心你將我的vector用作一個vector<Fruit>,並將一個Pear塞入其中,或將其用作一個vector<Object>,並將一個HydraulicPumpInterface塞入其中。我認爲到現在爲止這都已相當容易理解。即使JAVAC#都準備開始提供支持這一功能的 通用機制。 

另一方面,你不能創建完全是靜態類型的系統,因爲這樣一來你必須將所編譯的整個系統部署爲一個從不發生變化的單元。諸如虛擬函數的更多動態 技術的益處是你能夠鏈接那些“爲了完成靜態類型檢查,你所知信息還不充分的”東西。然後,你可使用你所知的任何初始接口來檢查這一系統所擁有的接口。你可詢問一個對象一些問題,然後在基於答案的基礎上開始使用它。問題是遵循某一系列的,“你是遵循Shape接口的東西嗎?”如果你得到的答案是yes,你就可以將Shape的有關操作運用在它上面。 如果你得到的答案是no,你會說“哎呀,”並對其進行處理。有關與此的C++機制是dynamic_cast。使用dynamic_cast的“提問方式”與動態類型語言的相反,在後一種情形中,你傾向於馬上就開始運用操作,如果它不運作的話,你就說“哎呀。”通常這一令人驚訝的情形發生於計算與對象爲你所知那一刻的中間。後面一種令人驚訝的情形則比較難以應對。 

另外,針對於編譯器優化方面的益處也是巨大的。動態類型、靜態類型以及決議操作之間的差異很容易達到50倍。當我談論到效率時,我喜歡談論倍數,因爲由此你能夠很容易地看出差異所在。 

Bill係數? 

BS當你接觸到百分數,10%、50%等時,你可能會爭論效率是否重要,相應的解決方案也許是今後更先進的計算機而不再是優化。但是針對動態和靜態,我們是在討論倍數:3倍、5倍、10倍、50倍。我認爲那些需要在巨型計算機上處理的實時問題的一點點區別都很重要,其中一個爲10的係數甚至是一個2倍的係數都關係着成功和失敗的差異。 

Bill你 並不僅僅在討論有關動態與靜態方法調用,你還討論了優化,對嗎?優化器具備更多的信息並能夠執行一項更好的任務。 

BS是的。 

Bill這是如何運作的呢?優化器如何使用類型信息來執行更好的優化操作呢? 

BS讓我們看一個非常簡單的情形。C++具有靜態和動態綁定成員函數。如果你執行了一個虛擬函數調用,那麼即爲一個間接函數調用。如果是一個靜態綁定,那麼將是一個極爲普通的函數調用。如今一個間接函數調用耗費的資源要比直接調用多出25%。這不是一筆很大的開銷。如果它是一個“在整數上執行小於比較一類操作的”極小函數,那麼一個函數的相對耗費將是巨大的,因爲有更多的代碼需要被執行。你不得不去執行函數的前奏內容、執行操作、執行後續內容(如果存在此類事情的話),你不得不將更多的指令加載到機器中。你分裂了傳遞路徑,尤其當它是一個間接函數調用時。所以針對於如何執行小於比較你將得到一個1030的係數。如果這一差異發生於一個關鍵的內部循環中,那麼該差異將變得至關重要。這就是C++排序如何擊敗C排序的。C排序將一個函數傳遞爲間接調用。C++版本則傳遞一個函數對象,其中你能夠擁有一個退化爲“小於比較”的靜態綁定內聯函數。 

未成熟的優化或是謹慎的優化 

BillC++文化總是與效率相連。是否存在大量未成熟的優化呢?我們如何知曉早期未成熟的優化與早期謹慎的優化之間的差異呢? 

BSC++社羣的某些部分 關心效率,我認爲其中一些具有充分的理由,而其他的則僅僅是因爲他們不是很理解。他們對於不是十分確切的低效感到擔心。但是,肯定存在對效率的關注,我認爲有兩種看此問題的方法。我看待效率的方法是這樣的:我希望知道我的抽象能夠以一種合理的方式與機器相匹配,而且我希望擁有我自己能夠理解的抽象。 

如果我想進行線性代數運算,我需要一個矩陣類,如果我想進行繪圖,那麼我需要一個繪圖類,如果我想進行字符串操作,我就需要一個字符串類。我首先要做的事情就是將抽象的層面提升到一個合適的層面。我使用這些非常簡單的例子,是因爲它們是最爲常見也最容易討論的。下面需要注意的事情就是在我不需要的地點不擁有N2N3算法。如果我擁有本地化的信息,我就不會再到Web上搜尋信息。如果我在內存中擁有緩存的信息,那麼我就不用到硬盤上去找。我看到人們使用建模工具最終以寫兩次硬盤而將兩個字段全部寫入到一個記錄中告終。爲了避免此類算法,我認爲這是一個謹慎的前端設計層面的優化,即你應當關注的一類事情。

現在,一旦你擁有一個位於適當高度的抽象層面的適當模型世界,那麼你就可以開始進行優化,而且此類後續的優化也是適當的。我不喜歡的是,那些擔憂高級特性和擔憂抽象的人們開始時就使用語言的一組非常有限的子集 或者爲了支持自己手寫的代碼而避免使用設計良好的庫。他們在能夠處理對象的地方處理字節。由於擔心一個vector或是一個map類過於昂貴而採用數組。那麼,他們最終只能編寫更多的代碼,而這些代碼後來還不能被人理解。因爲在任何大型系統中你都要對其進行分析並找出你使其出錯的地點,所以這就成了一個問題。 

你另外也會試着去擁有較高層面的抽象以便你能夠檢測具體的事情。如果你使用一個map,可能會發現它 代價過於高昂,這是極有可能的。如果你擁有一個帶有一百萬個元素的map,那麼很可能它會慢下來。它是一個紅黑樹。在許多情形下,如果你需要進行優化,都可用一個 哈希表來替換一個map。如果你只擁有一百個元素,那麼將不會有任何區別。但如果有一百萬個元素,那麼差別就大了。 

現在,如果你是在最低層面編寫一切代碼,即使一次,那麼你也將不知道自己擁有的是什麼。可能你知道你所擁有的數據結構是一個map,但更爲可能的是它是一個類似map的特殊數據結構。一旦你知道這一特殊數據 結構運作不正確,你怎樣才能知道應該用哪一個數據結構來替換它呢?由於你工作於如此低的層面,所以很難知道該如何操作。而最後,如果你編寫了一個特殊的數據結構,你可能將操作遍佈於你的整個程序。這對於一個隨機數據結構並不是不常見的。並沒有一組你用以對其進行操控的固定操作,有時“爲了追求效率”,數據是直接從用戶代碼進行訪問。在此種情形下,你的編譯器並不會告訴你瓶頸所在的地點,因爲你將代碼散佈於整個程序之中。從概念上講,瓶頸隸屬於某些東西,但你對其並沒有概念,或你不能直接表達這一概念。因此你的工具並不能向你展示是由這一概念引起的問題。如果某一東西不是直接位於代碼中,那麼沒有工具能夠根據其適當的名字來向你說明關於它的信息。

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