看看在科學計算方面,Fortran語言在科學和工程領域經久不衰,討論的最熱烈的一個主題就是性能。
Fortran語言至今依然非常重要的一個最主要原因是速度快。在Fortran中搗弄數字的方式比在其他語言中使用的別的方式要快。能在這個領域和Fortran競爭的——C和C++,被廣泛使用的原因也是因為他們性能的競爭力極強。
問題是,為什么?是什么讓C++和Fortran這么快?又是為什么,他們能勝過像Java或者Python這些其他語言?
解釋VS編譯
對不同編程語言分類的方法有很多,像是依據編程語言偏好的風格分類,或者它們所支持的特性分類等等。側重性能的話,可以分為編譯型語言和解釋型語言。
這兩種類型的區分并不困難;相反,就像是在一條線上的兩端。在一端,是傳統的編譯型語言,這一類包括Fortran,C/C++等。在這些語言中,有一個單獨的編譯平臺,專門負責將源代碼編譯成處理器可以執行的程序。
編譯的過程分為以下幾步。首先是分析和解析源代碼。基本的拼寫錯誤和筆下誤會在此時被檢測出來。檢查過的源代碼將用來產生存放在存儲器中的表達式,作用同樣是檢查錯誤——這一次檢查的是語法錯誤,例如調用了不存在的函數,或者在字符串或文本上進行了算數運算。
然后,存儲器中的表達式驅動代碼生成器,這一步會生成可執行的代碼。為了提高可執行代碼的性能,代碼優化會遵循一以下的過程來進行:在代碼的表達式上進行高水平的優化,在代碼生成器的輸出上進行低水平的優化。
事實上,隨后就可以執行生成的代碼。完整的編譯過程就是這樣。
在對面的一端,就是解釋型語言。解釋型語言也有類似于編譯器那樣的解析平臺,但是它的代碼直接用來驅動程序的運行。
最簡單的解釋器會將源代碼和各種各樣編程語言所支持的特性進行匹配——所以解釋型語言將會負責添加數字,添加到字符串等函數。隨著代碼被解析,解釋器會匹配到相應的函數并且執行它。程序中產生的變量將存儲在表中,通過名字和值的匹配來提供查詢。
解釋型語言的風格最極端的例子是批處理文件和shell script。在這些語言中,可執行的代碼通常不會構建到解釋器中,而是獨立的運行。
那么,到底是什么造成了這種性能的不同呢?通常來說,每一次底層間接的調用都會降低性能。例如,對兩個數字做加法最快的方式,是將其存放在處理器的寄存器中,然后使用處理器的加法指令。編譯器可以這樣做,可以將變量往寄存器里面存并利用處理器的指令。但是解釋器卻不行,相同的添加動作需要這樣來完成:聲明兩個變量的名稱與值,然后調用函數執行加法動作。相應的函數可能對處理器調用了一樣的指令做加法,所有這些在調用處理器指令之前的這些工作都讓性能變慢了。
模糊的界限
在這兩個極端之間還存在著別的選擇。例如,很多優秀的解釋器表現得像編譯器一樣:它們執行類似于編譯器的步驟,包括生成可以直接執行的代碼,但是它們緊接著就將代碼執行了(而不是存儲在硬盤上供以后執行)。在程序的運行期間,解釋器會保留這些可執行的代碼,這樣當需要特定代碼的時候就不用重新編譯了,但是程序執行完的時候,可執行代碼就被刪除了,如果你想再運行這個程序,就不得不重新生成可執行代碼。
這種在程序執行時候編譯帶啊的方法叫做即時編譯(Just-in-time,JIT),IE,火狐,Chrome瀏覽器的JavaScript引擎都使用了這項技術來提高它們的腳本性能。
即時編譯一般比傳統的解釋要高效。然而,卻比不上提前編譯(Ahead-of-time,AOT)。提前編譯可能會慢,因為編譯器要花大量的時間來,盡最大的努力來優化代碼。它們之所以能夠這樣做是因為人們大可不必在這段時間苦等著它們完成這項工作。然而,即時編譯卻是在運行時期,人們守在鍵盤前面等待程序運行。這就限制了優化代碼占用的時間。像是在后臺進程優化添加過程或者現在的多核處理器技術在這方面擁有廣泛前景。
原則上,即時編譯在改變編譯環境方面會表現出優勢。常規的編譯方式必須在某些方面非常保守。例如,微軟不能輕易地使用新一代英特爾和AMD上最新的AVX來編譯Windows,因為Windows必須要保證能夠在不支持AVX的處理器上運行。然而,一個即時編譯型的程序就可以,因為它能適應所處的硬件環境,并最大化的利用它。
歷史上,即時編譯并沒有很好地利用現代處理器提供的復雜指令集。的確,就算拋開時間的限制不說,用好像SSE或者AVX指令集對于提前編譯來說都是一個很大的挑戰。好在這種情況正在改善,比如Oracle的HotSpot Java虛擬機就有對這些指令集的早期支持。
另一種應用廣泛的技術是使用字節碼。基于字節碼的平臺有Java,.NET,他們也有傳統上的編譯,不過編譯器不生成可執行文件,而是生成字節碼,一種類型的字節碼并不是基于硬件環境而設計的,而是基于理想的虛擬機。程序運行的時候,字節碼可以被解釋或者即時編譯。
總體上說,這種基于字節碼系統的平臺介于編譯型和解釋型之間。字節碼即時編譯起來非常簡單并且易于優化,相比于解釋型語言是一個進步的地方,但優化代碼的效果仍比不上編譯型。
各種各種介于解釋型和編譯型中間的選擇,提供了廣泛的介于解釋型和編譯型兩種階段之間的選項。
技術上講,使用編譯器和解釋器并不是一個語言自身的特點。有一些項目,例如,有人為通常使用編譯器的C語言制作一個解釋器;JavaScript也正從簡單解釋器向復雜的即時編譯器過度以便強化其性能。
然而,主流的預編并不會在這兩種形式之間來回轉換。C++本質上說是提前編譯的,Fortran也是。C#和Java大多時候是編譯成字節碼,運行的時候再即使編譯。Python和Ruby通常是解釋型。這就產生了一個性能的分級:C++和Fortran比Java和C#快,Java和C#又比Python和Ruby快。
語言本身的特點
不同的語言種類之間依然存在著很大的性能差異,其中一個主要原因就是它們的受重視度。拿JavaScript和Python來說,例如:這兩個流行的腳本語言都是解釋型的。但實際上,JavaScript卻比Python要快的多,這并不是因為語言自身的特點——它們的表達式和能力方面不相上下——而是因為像微軟,谷歌,莫斯拉這些公司更加重視JavaScript。
幾種類似的語言,投資的不同(或者發展的優先級別不同)是一種語言性能的決定因素。寫一個優秀的編譯器或者解釋器需要花很多精力,而不是每一個語言都值得去這樣做。
然而,語言的不同也會對性能有所影響。Fortran語言的長壽就是一個很好的例子。很長一段時間,相同的兩個程序在Fortran和C(或者C++)中運行,Fortran會快一些,因為Fortran的優化做的更好。這是真的,就算C語言和Fortran的編譯器用了相同的代碼生成器也是一樣。這個不同不是因為Fortran的某種特性,事實上恰恰相反,是因為Fortran不具備的特性。
數據處理程序經常在很大的數量級上操作數據。內存中的數字會表示某個點在3D空間中或者其他的東西。運算通常會在這數據上對每一個元素迭代重復的操作。例如,有這樣一個函數:將兩個數組中的每一個元素添加對對方的數組中,需要三步:添加兩個數組,聲明第三個數組,然后進行運算獲得結果。
更好的添加數組可以用別的指令來完成,這允許這種各樣的優化。例如,可以使用SSE或者AVX這樣的指令集。不再是一個一個地添加元素,而是每一個添加四個,同時,使用SSE或者AVX指令集來做一個簡單的提升四倍性能的優化。這個函數可以應用到多線程:如果你有四核處理器,那么一個核心可以完成分四分之一的添加。這種基于數組的函數提供了一些強力的編譯的優化,使得代碼在運行很多次的時候,效率有了明顯的提升。
C語言其實并沒有讓數組作為函數的輸入(或者,在這種情況下,輸出)。而是使用了指針。指針或多或少地代表了地址,這是C語言的內嵌特點:讀出或者寫入儲存在內存中的地址值。在很多時候,C語言使用指針和數組可以互相轉換。對于數組,指針只是代表了元素的首地址。數組的其他元素就在下一個連續的內存中。C語言用內嵌的可以操作內存中的地址值的功能可以獲取數組。
指針非常復雜,C程序員使用它們來構建復雜的程序結構,以及緊湊有序的數組。但是這種復雜卻造成了編譯器優化的麻煩。依然以添加兩個數組的函數為例。在C中,可能不需要三個數組作為輸入,但是卻需要三個指針:兩個輸入,一個輸出。
問題就來了。這些指針可以代替任何內存地址。更重要的是,他們可以重疊。輸出數組的內存地址也可以同時是輸入數組的。甚至可以部分重疊,輸出數組可以覆蓋一個輸入數組的一半。
這對編譯器優化來說是個大問題,因為之前基于數組的優化不再適用。特別的,添加元素的順序也成問題,如果輸出重疊的數組,計算的結果會變得不確定,取決于輸出元素的動作是發生在元素被覆蓋之前還是之后。
這就意味著我們之前的理想的優化——使用多線程和迭代指令——行不通了。編譯器不知道這樣做是不是安全的,它處理器源碼的時候只好遵從原來的順序,沒有別的。編譯器不再有整理代碼使之更快的自由了。
這個問題叫做混淆現象,像傳統的Fortran語言就沒有這種問題。因為傳統Fortran沒有指針,它只有非重疊數組。很長時間以來,都允許Fortran的編譯器(或者程序員)對程序進行強有力的優化,不像在C。鞏固了Fortran在數據處理方面的霸主地位。
顯然對于這種功能的函數,這種指針的靈活性并不是很有用。如果數組重疊,就沒有合適的方式來處理數據,所以很不幸,優化在這里就毫無用武之地了。在1999年規定的C語言標準(C99),給出了這個問題的答案。在C99中,指針可以被指定為不重疊。這時編譯器就可以做所有的優化,C99的這個特性使得C語言(和C++,,因為大多數編譯器供應商給了一個類似的功能)成為了像Fortran那樣的可以優化的語言。
這個混淆現象表明語言的特點會跟優化相關,尤其是很大的、變革性的優化。例如,自動將單線程代碼轉換成多線程的代碼。然而,它也表明這種差異不是永久性的。開發人員希望能夠使用C和c++來處理數據,如果一些小的改變能夠使它像Fortran語言那樣快,開發人員就會做這樣一些改變。
原文鏈接: Why are some programming languages faster than others?