當我聽到人們說Mathematica不夠快的時候,我通常會提出想要看一下這段令他們煩惱的代碼,然后會發(fā)現(xiàn),其實并不是Mathematica本身的表現(xiàn)不夠好,而是Mathematica沒有被最優(yōu)使用。我覺得我應(yīng)該和大家分享一下我在優(yōu)化Mathematica代碼時首先會看的一些內(nèi)容。
如果可以的話盡量盡早使用浮點數(shù)
我最??吹降膶?dǎo)致代碼變慢的問題是,程序員會不經(jīng)意地讓Mathematica做超出需要的細致的事情。沒必要的代數(shù)精確是其中最常見的問題。
在多數(shù)與數(shù)字相關(guān)的軟件中,是不需要這么精確的代數(shù)的。1/3和0.33333333333333是一樣的。當你碰到特別嚴重的在數(shù)字上不穩(wěn)定的問題時這個差異可能會被放大的特別明顯,但是,在大多數(shù)情況中,浮點數(shù)已經(jīng)足夠使用了,而且最重要的是,浮點數(shù)運算更快。Mathematica中,任何小于16位的小數(shù)都被看作是機器浮點數(shù),所以如果更想要速度而可以舍棄一些精確性的時候,記得用小數(shù)(比如,三分之一輸入為1./3.)。以下是一個例子,可以看到使用浮點數(shù)是精確數(shù)運行速度的50.6倍。在這個例子中,兩個數(shù)字的使用得到的是同一個結(jié)果。

在符號運算中也是這樣。如果你不是很在意符號式的結(jié)果,并且計算的穩(wěn)定性也不是問題的話,那么盡快使用數(shù)值作為替代。比如,求解下面的二項式符號計算時,在使用數(shù)值作為替代之前,這個代碼可能會讓Mathematica生成長達五頁的中間符號表達式。

但是如果先用數(shù)值替代,那么Solve會使用更快的數(shù)值方法。

當用數(shù)據(jù)列表工作時,使用實數(shù)的方法必須保持一致。只要一個精確的數(shù)值就可以讓整個數(shù)據(jù)組處于一個更靈活但是缺乏效率的形式中。

Compile函數(shù)接受Mathematica的代碼,并讓你預(yù)先聲明輸入?yún)?shù)的類型(比如實數(shù)、復(fù)數(shù)等)和結(jié)構(gòu)(如數(shù)值、列表、矩陣等)。這雖然失去了Mathematica語言靈活性的優(yōu)勢,但是可以免于擔心類似于“如果參數(shù)是符號怎么辦?”的問題,Mathematica也可以最優(yōu)化程序并創(chuàng)建一個字節(jié)碼在虛擬器上運行。并不是所有東西都可以被編譯,且簡單的代碼可能不會有太大效果,但是那種復(fù)雜的低階數(shù)字代碼速度可以得到大大的提升。
下面是一個例子:

使用Compile可以比Function的運行速度提高80倍。

但是我們可以在Compile函數(shù)中加入一些代碼的可并行性質(zhì),這樣可以生成更好的結(jié)果。

在我的雙核處理器電腦上,我的運行結(jié)果比原本快150倍,如果是多核處理器那么效果會更加明顯。
但是要注意,很多Mathematica函數(shù)比如Table、Plot、NIntegrate等會自動編譯它們的參數(shù),這樣的話你使用上述方法可能不會看到任何速度上的提升。
另外,如果你的代碼可編譯,你還可以使用選項CompilationTarget->“C”來生成C代碼,調(diào)用你的C編碼器并將其匯編成一個DLL,并把這個DLL鏈接回Mathematica,都是自動操作的。在編譯階段,DLL直接在CPU上運行而非Mathematica的虛擬器,所以會更快得到結(jié)果。

Mathematica有很多函數(shù)。起碼半數(shù)以上的人可能不會坐下來學(xué)習(xí)所有函數(shù)。所以當我看見有些人會寫一些代碼而沒有意識到其實Mathematica知道怎么做這些操作的時候,我一點也不意外。這種重復(fù)操作不僅是浪費時間,而且公司是花錢請程序員來開發(fā)研究運行這些操作的最有效方法,所以內(nèi)置的函數(shù)一般是非常快的。
如果你發(fā)現(xiàn)有些結(jié)果很接近了但是不完全對的時候,此時可以檢查選項和參數(shù),通常它們會概括可以覆蓋很多特殊用法或者專有應(yīng)用的函數(shù)。
下面舉一個這樣的例子。如果我有一個一百萬2x2矩陣的列表,我想把該列表轉(zhuǎn)換成一百萬個包含四個元素的列表,概念上來說最簡單的方法是用Map把已經(jīng)用Flatten扁平化過的數(shù)據(jù)進行映射即可。

但是Flatten本身知道怎么把整個步驟完成,你只要說明數(shù)據(jù)結(jié)構(gòu)的第二層和第三層應(yīng)該被合并而第一層不動就可以了。說明這種細節(jié)的內(nèi)容可能相對來說是比較細致的工作,但是只需要使用Flatten就能完成整個扁平化工作可以讓整個進程比你自己手動做這些程序要快將近4倍。

所以記住:在運行代碼之前在幫助菜單里先搜索一遍。
Mathematica對于某些種類的編程錯誤容忍度很高——如果你忘記在正確的時候初始化一個變量,Mathematica會以符號的模式順利運行,而并不會有循環(huán)計算或者預(yù)料之外的數(shù)據(jù)類型出現(xiàn)。如果你只想要一個答案的話這個功能是很棒的,但是這也會讓你沒有得到最優(yōu)的解答。
Workbench會在幾個方面幫助你。首先它會幫你排除程序問題,并把大型的代碼項目組織得更好,整齊易讀的代碼會讓程序員更好地寫優(yōu)秀的代碼。但是最關(guān)鍵的功能在于分析器會告訴你是哪一行代碼用光了時間,而且會告訴你調(diào)用這些代碼用了多少時間。
看下這個例子,一個很可怕的執(zhí)行斐波那契數(shù)的方法。如果你沒有考慮到數(shù)列的雙重遞歸,你可能會驚訝計算fib[35]怎么會需要22秒鐘(大約和內(nèi)置函數(shù)計算Fibonacci[1000000000]所有208,987,639位數(shù)字需要的時間一樣)(請看訣竅3)。

在分析器中運行這個代碼可以解釋這個現(xiàn)象的原因。主要規(guī)則被援引9,227,464次,fib[1]的值被請求18,454,929次。
學(xué)習(xí)代碼能做什么,而不是想當然,會讓你眼界大開。
這個編程訣竅對任何語言都管用。Mathematica認為你想知道的是這個:

這省去了用任何值調(diào)用 f 的結(jié)果,這樣的話如果再用相同數(shù)值調(diào)用 f,Mathematica不需要再算一遍。這里你就是用內(nèi)存換取計算速度,所以如果你的函數(shù)要用大量不同數(shù)值調(diào)用而不太重復(fù)的時候這個方法可能不合適。但是如果輸入的范圍有限,那么這個方法就很有用了。以下就是如何拯救我剛才提到的來解釋訣竅3的例子的方法??梢园训谝粭l規(guī)則改成這樣:

然后速度立刻就可以提升,因為fib[35]現(xiàn)在只需要用主要的規(guī)則運算33次。查詢之前的結(jié)果可以防止循環(huán)遞歸fib[1]的問題。
有很多Mathematica的操作都會自動在本地核中并行運行(大部分是代數(shù)、圖像處理和統(tǒng)計),如果需要手動的話,Compile也可以。但是對于其他操作來說,或者如果你想在遠程硬件上并行操作,你可以試用內(nèi)置的并行編程架構(gòu)來完成。
有一個這樣工具的集合,但是都是為非常獨立的任務(wù)服務(wù)的,比如ParallelTable, ParallelMap,ParallelTry,還有很多。每個這樣的小工具都可以自動進行通信、工作管理和收集結(jié)果。發(fā)送任務(wù)和回收結(jié)果需要一點時間,所以在減少時間和增加時間上會有需要一個取舍。你的Mathematica有四個計算內(nèi)核,如果你有額外的CPU可使用的話,還可以通過gridMathematica在此基礎(chǔ)上提高這一性能。這里由于我用的是雙核電腦,ParallelTable實際將我的運算時間縮少了一半。如果有更多CPU則會得到更好的結(jié)果。

任何Mathematica可以做的事情都可以以并行方法運行。比如,你可以給遠程硬件發(fā)送一個并任務(wù)集合,每個任務(wù)都在CPU或GPU中編譯和運行。
如果你有GPU硬件,有一些用批量并行運行方法可以做的非??斓氖虑椤3沁@些最優(yōu)化CUDA函數(shù)恰好可以做你想要它們做的事情,否則你還要做一點額外的工作,但是CUDALink和OpenCLLink 工具可以為你自動化很多繁瑣的細節(jié)。
因為Mathematica數(shù)據(jù)結(jié)構(gòu)的靈活性,AppendTo不會假設(shè)你要追加的是一個數(shù)字,因為你要追加的可能是一個文件、音頻或者圖像等。所以AppendTo必須為所有數(shù)據(jù)創(chuàng)建一個新的副本,并重新調(diào)整架構(gòu)以適應(yīng)新追加的信息。當數(shù)據(jù)累積的時候這個過程會變得越來越慢。(而且構(gòu)建data=Append[data,value]與AppendTo一樣。)
嘗試使用Sow和Reap。Sow會舍棄你想要累積的值,而Reap收集它們并一次性在末尾建立一個數(shù)據(jù)對象。下列范例是等價的:

Block,With和Module都是本地化構(gòu)建的工具,但是屬性上有些小區(qū)別。根據(jù)我的經(jīng)驗,95%以上的幾率在我寫的代碼中Block和Module是可以互相替換的,但是Block通??煲稽c,而在另一些例子中(Block的變量在只讀狀態(tài)的情況下)With會快一些。

模式匹配很好,可以讓項目中復(fù)雜的任務(wù)變得簡單一點。但是它有時會很慢,尤其是像BlankNullSequence這種比較復(fù)雜的模式(通常寫作“___”)中,可能會花很長時間仔細在你的數(shù)據(jù)中搜索一些——你作為一個程序員可能已經(jīng)可以判斷的——不存在的模式。如果想要速度的話,那么選擇范圍更窄的模式,或者不用模式會更好。
比如,下面范例使用了模式,在一行代碼中簡潔地執(zhí)行了冒泡排序:

上例概念上很簡單,但是比起這個我最開始學(xué)習(xí)編程的時候就學(xué)過的列出步驟的方法來說還是要慢很多:

當然在這個例子中你可以用內(nèi)置函數(shù)(參見訣竅3),這個內(nèi)置函數(shù)會使用比冒泡排序更好的排序算法。
Mathematica的一個很重要的優(yōu)點是,它可以用不同的方式處理同一個問題。它允許你按照你自己的想法編程,而不是為了編程語言的風(fēng)格重構(gòu)你的問題。但是,概念上簡單和計算效率不是一件事。有時候容易懂的想法可能會需要更多的工作才能實現(xiàn)。
但是另一個問題是,因為Mathematica中最優(yōu)化和一些絕妙算法都是自動應(yīng)用的,所以很難預(yù)測什么時候Mathematica又會做出另一個絕妙的操作。比如,下例是兩種計算階乘的方法,第二種比第一種快10倍。

為什么?你可能會猜可能Do的循環(huán)很慢,或者所有這些任務(wù)緩存都需要時間,或者可能第一次執(zhí)行的時候有什么東西出了問題,但是實際的原因很難預(yù)料到。Time有一個很聰明的二元分離的小技巧,可以在當你有大量整數(shù)參數(shù)的情況下使用,即將循環(huán)將參數(shù)分成兩個更小的乘積(1*2*…*32767)*(32768*…*65536),而不是把這個參數(shù)從第一個用到最后一個。當然要做的乘法數(shù)量還是一樣,但是不會再包括數(shù)值非常大的整數(shù),所以平均來說,運算的速度會更快。在Mathematica中有很多這樣隱藏的小魔法,而且每次新版本發(fā)布都會有更多的小技巧加入。
當然最好的方法還是使用內(nèi)置函數(shù)(又說到訣竅3了):

Mathematica可以做非常高級的計算,而且有強大的功能和極高的精確性,但是這兩者并不總能兼得。我希望這些訣竅可以在快速編程、快速執(zhí)行和精確結(jié)果的沖突訴求中對你有些許幫助。
用可計算文件格式(CDF)文件的形式下載這篇博文。
https://blog.wolfram.com/data/uploads/2011/12/10TipsForMathematicaCode.cdf
京ICP備09015132號-996 | 違法和不良信息舉報電話:4006561155
© Copyright 2000-2026 北京哲想軟件有限公司版權(quán)所有 | 地址:北京市海淀區(qū)西三環(huán)北路50號豪柏大廈C2座11層1105室
北京哲想軟件集團旗下網(wǎng)站:哲想軟件 | 哲想動畫