10個編寫快速運行的Mathematica代碼的小訣竅

當我聽到人們說Mathematica不夠快的時候,我通常會提出想要看一下這段令他們煩惱的代碼,然后會發現,其實并不是Mathematica本身的表現不夠好,而是Mathematica沒有被最優使用。我覺得我應該和大家分享一下我在優化Mathematica代碼時首先會看的一些內容。

01

如果可以的話盡量盡早使用浮點數



我最常看到的導致代碼變慢的問題是,程序員會不經意地讓Mathematica做超出需要的細致的事情。沒必要的代數精確是其中最常見的問題。

在多數與數字相關的軟件中,是不需要這么精確的代數的。1/3和0.33333333333333是一樣的。當你碰到特別嚴重的在數字上不穩定的問題時這個差異可能會被放大的特別明顯,但是,在大多數情況中,浮點數已經足夠使用了,而且最重要的是,浮點數運算更快。Mathematica中,任何小于16位的小數都被看作是機器浮點數,所以如果更想要速度而可以舍棄一些精確性的時候,記得用小數(比如,三分之一輸入為1./3.)。以下是一個例子,可以看到使用浮點數是精確數運行速度的50.6倍。在這個例子中,兩個數字的使用得到的是同一個結果。

10個編寫快速運行的Mathematica代碼的小訣竅的圖1

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

10個編寫快速運行的Mathematica代碼的小訣竅的圖2

10個編寫快速運行的Mathematica代碼的小訣竅的圖3

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

10個編寫快速運行的Mathematica代碼的小訣竅的圖4

10個編寫快速運行的Mathematica代碼的小訣竅的圖5

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

10個編寫快速運行的Mathematica代碼的小訣竅的圖6

02
學會Compile


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

下面是一個例子:

10個編寫快速運行的Mathematica代碼的小訣竅的圖7

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

10個編寫快速運行的Mathematica代碼的小訣竅的圖8

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

10個編寫快速運行的Mathematica代碼的小訣竅的圖9

在我的雙核處理器電腦上,我的運行結果比原本快150倍,如果是多核處理器那么效果會更加明顯。

但是要注意,很多Mathematica函數比如Table、Plot、NIntegrate等會自動編譯它們的參數,這樣的話你使用上述方法可能不會看到任何速度上的提升。

02.5
使用Compile生成C代碼


另外,如果你的代碼可編譯,你還可以使用選項CompilationTarget->“C”來生成C代碼,調用你的C編碼器并將其匯編成一個DLL,并把這個DLL鏈接回Mathematica,都是自動操作的。在編譯階段,DLL直接在CPU上運行而非Mathematica的虛擬器,所以會更快得到結果。

10個編寫快速運行的Mathematica代碼的小訣竅的圖10

03
使用內置函數


Mathematica有很多函數。起碼半數以上的人可能不會坐下來學習所有函數。所以當我看見有些人會寫一些代碼而沒有意識到其實Mathematica知道怎么做這些操作的時候,我一點也不意外。這種重復操作不僅是浪費時間,而且公司是花錢請程序員來開發研究運行這些操作的最有效方法,所以內置的函數一般是非??斓?。

如果你發現有些結果很接近了但是不完全對的時候,此時可以檢查選項和參數,通常它們會概括可以覆蓋很多特殊用法或者專有應用的函數。

下面舉一個這樣的例子。如果我有一個一百萬2x2矩陣的列表,我想把該列表轉換成一百萬個包含四個元素的列表,概念上來說最簡單的方法是用Map把已經用Flatten扁平化過的數據進行映射即可。

10個編寫快速運行的Mathematica代碼的小訣竅的圖11

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

10個編寫快速運行的Mathematica代碼的小訣竅的圖12

所以記?。涸谶\行代碼之前在幫助菜單里先搜索一遍。

04
使用Wolfram Workbench


Mathematica對于某些種類的編程錯誤容忍度很高——如果你忘記在正確的時候初始化一個變量,Mathematica會以符號的模式順利運行,而并不會有循環計算或者預料之外的數據類型出現。如果你只想要一個答案的話這個功能是很棒的,但是這也會讓你沒有得到最優的解答。

Workbench會在幾個方面幫助你。首先它會幫你排除程序問題,并把大型的代碼項目組織得更好,整齊易讀的代碼會讓程序員更好地寫優秀的代碼。但是最關鍵的功能在于分析器會告訴你是哪一行代碼用光了時間,而且會告訴你調用這些代碼用了多少時間。

看下這個例子,一個很可怕的執行斐波那契數的方法。如果你沒有考慮到數列的雙重遞歸,你可能會驚訝計算fib[35]怎么會需要22秒鐘(大約和內置函數計算Fibonacci[1000000000]所有208,987,639位數字需要的時間一樣)(請看訣竅3)。

10個編寫快速運行的Mathematica代碼的小訣竅的圖13

在分析器中運行這個代碼可以解釋這個現象的原因。主要規則被援引9,227,464次,fib[1]的值被請求18,454,929次。

學習代碼能做什么,而不是想當然,會讓你眼界大開。

05
記住你將來會需要用到的值


這個編程訣竅對任何語言都管用。Mathematica認為你想知道的是這個:

10個編寫快速運行的Mathematica代碼的小訣竅的圖14

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

10個編寫快速運行的Mathematica代碼的小訣竅的圖15

然后速度立刻就可以提升,因為fib[35]現在只需要用主要的規則運算33次。查詢之前的結果可以防止循環遞歸fib[1]的問題。

06
并行


有很多Mathematica的操作都會自動在本地核中并行運行(大部分是代數、圖像處理和統計),如果需要手動的話,Compile也可以。但是對于其他操作來說,或者如果你想在遠程硬件上并行操作,你可以試用內置的并行編程架構來完成。

有一個這樣工具的集合,但是都是為非常獨立的任務服務的,比如ParallelTable,ParallelMap,ParallelTry,還有很多。每個這樣的小工具都可以自動進行通信、工作管理和收集結果。發送任務和回收結果需要一點時間,所以在減少時間和增加時間上會有需要一個取舍。你的Mathematica有四個計算內核,如果你有額外的CPU可使用的話,還可以通過gridMathematica在此基礎上提高這一性能。這里由于我用的是雙核電腦,ParallelTable實際將我的運算時間縮少了一半。如果有更多CPU則會得到更好的結果。

10個編寫快速運行的Mathematica代碼的小訣竅的圖16

任何Mathematica可以做的事情都可以以并行方法運行。比如,你可以給遠程硬件發送一個并任務集合,每個任務都在CPU或GPU中編譯和運行。

06.5
想想CUDALink和OPENCLLink


如果你有GPU硬件,有一些用批量并行運行方法可以做的非??斓氖虑椤3沁@些最優化CUDA函數恰好可以做你想要它們做的事情,否則你還要做一點額外的工作,但是CUDALink和OpenCLLink 工具可以為你自動化很多繁瑣的細節。

07
使用Sow和Reap累積大量數據(不是AppendTo)


因為Mathematica數據結構的靈活性,AppendTo不會假設你要追加的是一個數字,因為你要追加的可能是一個文件、音頻或者圖像等。所以AppendTo必須為所有數據創建一個新的副本,并重新調整架構以適應新追加的信息。當數據累積的時候這個過程會變得越來越慢。(而且構建data=Append[data,value]與AppendTo一樣。)

嘗試使用Sow和Reap。Sow會舍棄你想要累積的值,而Reap收集它們并一次性在末尾建立一個數據對象。下列范例是等價的:

10個編寫快速運行的Mathematica代碼的小訣竅的圖17

08
使用Block或With而非Module


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

10個編寫快速運行的Mathematica代碼的小訣竅的圖18

09
少用模式匹配


模式匹配很好,可以讓項目中復雜的任務變得簡單一點。但是它有時會很慢,尤其是像BlankNullSequence這種比較復雜的模式(通常寫作“___”)中,可能會花很長時間仔細在你的數據中搜索一些——你作為一個程序員可能已經可以判斷的——不存在的模式。如果想要速度的話,那么選擇范圍更窄的模式,或者不用模式會更好。

比如,下面范例使用了模式,在一行代碼中簡潔地執行了冒泡排序:

10個編寫快速運行的Mathematica代碼的小訣竅的圖19

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

10個編寫快速運行的Mathematica代碼的小訣竅的圖20

當然在這個例子中你可以用內置函數(參見訣竅3),這個內置函數會使用比冒泡排序更好的排序算法。

010
嘗試不同的方法


Mathematica的一個很重要的優點是,它可以用不同的方式處理同一個問題。它允許你按照你自己的想法編程,而不是為了編程語言的風格重構你的問題。但是,概念上簡單和計算效率不是一件事。有時候容易懂的想法可能會需要更多的工作才能實現。

但是另一個問題是,因為Mathematica中最優化和一些絕妙算法都是自動應用的,所以很難預測什么時候Mathematica又會做出另一個絕妙的操作。比如,下例是兩種計算階乘的方法,第二種比第一種快10倍。

10個編寫快速運行的Mathematica代碼的小訣竅的圖21

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

當然最好的方法還是使用內置函數(又說到訣竅3了):

10個編寫快速運行的Mathematica代碼的小訣竅的圖22

Mathematica可以做非常高級的計算,而且有強大的功能和極高的精確性,但是這兩者并不總能兼得。我希望這些訣竅可以在快速編程、快速執行和精確結果的沖突訴求中對你有些許幫助。

登錄后免費查看全文
立即登錄
App下載
技術鄰APP
工程師必備
  • 項目客服
  • 培訓客服
  • 平臺客服

TOP

7
2