Reference-Request

julia 相對於 python 的速度優勢對 DSGE 建模有什麼影響嗎?

  • February 21, 2022

與 Python 相比,Julia 的主要賣點是它的速度,正如人們經常爭論的那樣。但是,根據我自己的個人經驗,我從未註意到 Julia 和 Python 之間的速度有任何顯著差異。如果存在微不足道的幾秒鐘差異,那實際上並不重要。這對於使用大數據的模型來說可能是個問題,但是在機器學習之外,是否有任何(甚至是大的)DSGE 模型可以產生影響?

我正在尋找兩者之間顯著差異的一些例子。

PS:儘管參考請求標籤,我很高興接受直接展示一些範例的答案。

Julia 實際上比 Python 快很多,在執行 DSGE 模型時也是如此。

紐約聯儲將他們的 DSGE 模型移至 Julia,因為它允許他們:

  • 估計模型速度快 10 倍
  • 以 11 倍的速度完成“解決”測試
  • 將程式碼行數減半,節省時間,提高可讀性並減少錯誤

原始結果可以在這裡找到。

這些加速比與 FED 之前使用的 Matlab 進行了比較。然而,就速度而言,對於 DSGE 模型(任何矩陣統計、迭代和遞歸等),Matlab 應該比 Python 更快。這也是已接受答案中第一篇建議論文的發現(我只查看了 2018 年修訂版)。順便說一句,論文中的結論如下:

… C++ 是最快的替代品,Julia 在速度和易用性之間取得了很好的平衡,而 Python 太慢了。

下面的截圖直接來自論文。 在此處輸入圖像描述

最慢的 Julia 實現只需要最快的 Python 版本的 1.6%。

第二篇論文(2020 年)基本上測試了一個只執行幾秒鐘(在 Julia 中不到 1)的模型。 在此處輸入圖像描述

儘管如此,只要超過幾分之一秒(Julia 的編譯時間延遲幾乎肯定會成為一個問題),Julia 就會明顯更快。這可能不是您真正感受到的差異或對您來說很重要,因為等待 10 或 15 秒基本上不會破壞交易。

與 Python 不同,Julia 在潛在程式碼實現方面要復雜得多。在 Python 中快速執行的一切都不是 Python,而是 C、Fortran 等。當類型推斷失敗或 JIT 編譯器沒有足夠的資訊進行有效優化時,Julia 也會變慢。如果你注意到 Python 沒有加速,要麼是因為你使用了編譯時間延遲比計算更重要的範例(或以這種方式進行基準測試),要麼是因為它是低效的程式碼。不久前,我在 Julia 的速度和與 Python 的比較方面做了一些工作。我將複製粘貼一些部分,以防有人對 Julia 實際上速度快的一些細節感興趣。

在像 Python 這樣的動態語言中,類可以被子類化。這在一些物件導向的靜態類型語言(如 Java)中也是可能的。出於這個原因,一個整數Python 3.x實際上包含四個部分:

  • ob_refcnt,一個幫助 Python 靜默處理記憶體分配和釋放的引用計數
  • ob_type,它編碼變數的類型
  • ob_size,它指定以下數據成員的大小
  • ob_digit,它包含我們期望 Python 變數表示的實際整數值。

這意味著與 Julia 或 C 中的整數相比,在 Python 中儲存整數存在一些成本。Python 使用 28 個字節來儲存整數,而 Julia 只有 8 個字節。

Julia 的一個重要特性是類型非常輕量級,並且使用者定義的類型與任何內置的類型一樣具有高性能:這與 Python 等語言有很大的不同,後者建構類的新實例非常昂貴。儲存名為 Foo 的結構(類)所需的數據量與儲存指向字元串的指針所需的數據量相同。在編譯時,與對象(比如 Python)在記憶體中的表示方式(包括類型資訊、引用計數器等)相反,Julia(就像 Fortran 和 C 一樣)沒有額外的成本。

朱莉婭:

在此處輸入圖像描述

Python:

在此處輸入圖像描述

您可以在 Python 中看到巨大的成本。說 Julia 不慢(不是說它很快)可能更好,這意味著它不會妨礙硬體,讓硬體高效地完成工作。在數學上,整數 5 與浮點 5.0 完全相同,但它具有不同的按位表示。這就是類型在電腦中很重​​要的原因。慢速程式語言很難提前知道事物是什麼類型。因此,他們測試各種組合以確定使用什麼常式來呼叫正確的 CPU 指令,因為在最低級別,微處理器在處理例如整數或浮點數時必須執行不同的指令。微處理器不知道類型。CPU 需要針對所有不同類型的特殊指令;例如 有符號和無符號整數,單精度或雙精度浮點數,或浮點數和整數的組合,您需要轉換(提升)類型。通常每種可能的類型都會有不同的特殊說明。

只要你的任務是計算性的,Julia 總是能勝過 Python(只要兩者都經過優化)。當您真正使用大量數據和/或複雜模型時,它開始變得重要。

例如看問題為什麼這個任務在 Python 中比 Julia 更快?在stackoverflow上。如果你的 Julia 程式碼很慢,幾乎總是以非性能方式編寫的程式碼。沒有任何優化,只是擺脫了編譯時間延遲,導致 Julia 在 Python 中花費了 8 秒和 30 秒。關於 stackoverflow 問題,Quantinsti對大型數據集的多項操作進行了一些很好的測試。對於約 10,000,000 行的 100 組,Python(pandas 包)和 R(dplyr 包)分別導致內部錯誤和記憶體不足錯誤,而 Julia 第一次耗時 2.4 秒,第二次耗時 1.8 秒。

Julia 速度很快,因為它的設計決策允許編譯器生成高效的程式碼。這是通過將多次調度作為核心設計決策的專業化實現的類型穩定性。為了說明這一點,您可以嘗試計算2^3.

在 Julia 中,您可以執行宏@code_llvm以及@code_native列印 LLVM 位碼以及為執行與給定通用函式和類型匹配的方法而生成的本機彙編指令。

如果您使用@code_llvm exponential(2,3)as well@code_llvm exponential(2,3) 並將其與 2^3 對應項進行比較,您會驚訝於“手動”指數解決方案與 Julia 的內置解決方案相比有多麼混亂。

手動(忽略幾乎一半的輸出):

在此處輸入圖像描述

內置(整個輸出):

在此處輸入圖像描述

內置解決方案是您可以通過一些基本程序集相當快速地破譯的東西。

  • 開頭的行;是註釋,並解釋以下指令來自程式碼的哪一部分。呼叫它debuginfo=:none會刪除彙編程式碼中的註釋
  • pushq, movq,subqaddq用於保存堆棧幀,將內容移入或移出寄存器(後綴q是 的縮寫quad,表示 64 位整數),分配給寄存器並釋放。對於我們的目的來說,兩者都不重要。簡而言之,CPU 僅對存在於寄存器中的數據進行操作。這些是 CPU 本身內部的小型、固定大小的插槽(例如,大小為 8 字節)。寄存器旨在保存一條數據,例如整數或浮點數。
  • movabsq就是所謂的平方取冪。例如,如果您計算 $ x^{15} $ ,它看起來像這樣
x^15 = (x^7)*(x^7)*x 
x^7 = (x^3)*(x^3)*x 
x^3 = x*x*x

或者只是我們上面簡單範例中的最後一行,即 $ x^3 $ 只要。

這樣做的原因是 Julia 的內置函式做出了一些聰明的設計決策。例如,x^-n適用於整數x和文字n,但不適用於表達式,因為 Julia 開發人員定義了文字的特殊句法處理。

  • 好處是快速可靠的求冪,例如 $ x^-3 $ . 優點是文字的權力自動成為類型穩定的,因為它們變成了固定數量的 * 呼叫。這就是為什麼 Julia 現在x^-3降到inv(x)^3.
  • 缺點是 $ p = -3 $ 和 $ x^p $ 不起作用並拋出一個看起來很混亂的錯誤。但是, 與x^literal具有不同的含義x^expression。從本質上講,犧牲了引用透明度,並“擴展”了類型穩定性:這就是為什麼^字面整數冪與提升到具有相同整數值的變數不同的原因。

**注意:**這裡的關鍵是類型穩定性並不意味著函式返回的類型與輸入相同。這意味著可以在每一步都推斷出類型。如果整數的 inv 始終是浮點數,那麼這是類型穩定的。

然而,問題是大多數普通的 Python 使用者會期望 x^p 在 p = -3 時工作。

同樣,範圍也很重要。你可以看看這個簡單的函式。

b = 1.0
function g(a)
   global b
   tmp = a
   for i in 1:1_000_000
       tmp = tmp + a + b
   end
   return tmp
end

b 在這裡是全域的,這就像性能的毒藥,因為全域變數的類型永遠無法確定。

如果你改為寫

function g(a, b)
   tmp = a
   for i in 1:1_000_000
       tmp = tmp + a + b
   end
   return tmp
end

您正在消除全域變數

• 將分配數量減少到零(從 3000001 分配:45.78 MiB)

• 加快執行速度並

• 生成乾淨且相當簡單的機器程式碼。

簡而言之,只要稍加註意,Julia 就可以與最快的語言相提並論。

這些分配非常重要。例如,在我的私人筆記型電腦上,查找單個文件(包括打開和關閉文件)大約需要 1.4 毫秒。從記憶體中訪問 1,000,000 個整數需要 75.7 毫秒。所以我的 RAM 幾乎比我的磁碟快 20,000 倍。我使用相當快的 SSD 執行它,但即使是較新的 Optane 技術磁碟通常也比 RAM 慢數千倍。

我在 C、Python 和 Julia 之間進行了速度測試,該測試基於MIT 數學入門課程中的筆記本。

這是通過實現sum函式來比較 C、Python 和 Julia 之間的速度sum(a),該函式計算

$$ \mathrm{sum}(a) = \sum_{i=1}^n a_i $$

a對於帶有n元素的數組。我比較了 Julia 和 Python 中的內置sum函式以及 C、Python 和 Julia 中的手動編碼實現。如果您是 Windows 使用者並想自己複製它,則需要安裝 MinGW (GCC/G++) 編譯器。

C程式碼是:

C_code = """
#include <stddef.h>
double c_sum(size_t n, double *X) {
   double s = 0.0;
   for (size_t i = 0; i < n; ++i) {
       s += X[i];
   }
   return s;
}
"""

const Clib = tempname()   # make a temporary file

# compile to a shared library by piping C_code to gcc
open(`gcc -fPIC -O3 -msse3 -xc -shared -o $(Clib * "." * Libdl.dlext) -`, "w") do f
   print(f, C_code) 
end

朱莉婭的

function my_sum1(x)
   result = zero(eltype(x))
   for element in x
       result += element
   end
   return result
end

不僅 Julia 程式碼明顯更容易,而且與 C 不同,它在 Julia 中適用於任何類型的數組(任何提供eltype方法的迭代)。

Python(在使用 PyCall 的 Julia 核心中)

py"""
def mysum(a):
   s = 0.0
   for x in a:
       s = s + x
   return s
"""
mysum_py = py"mysum"

結果是

在此處輸入圖像描述

有幾個有趣的方面。

  • 從 C 開始,在數組上測試函式 $ 10^7 $ 中的隨機數$$ 0,1 $$對這些數字求和的結果約為 10 毫秒,這意味著我們每秒有大約 10 億次加法。這聽起來很多,但遠低於我的處理器的 2.5GHZ 時鐘速度。這是因為每次浮點加法需要執行幾個額外的計算才能從記憶體中載入數組的下一個元素,加上訪問記憶體本身所需的時間。
  • 可能最有趣的是 Python 的內置 sum 相當慢,即使 Python 的 sum 函式是用 C 編寫的(Julia 的 sum 僅使用 Julia 建構)。它花費的時間幾乎是等效 C 程式碼的 4 倍,並分配了記憶體。Python 程式碼為通用性和能夠處理任意可迭代資料結構付出了代價。有關 Julia 和 Python 之間的記憶體分配差異,請參見上文。因此,不僅有涉及加法的計算,還有從記憶體中獲取每個項目的成本。
  • 與 Python 列表等效的 Julia 是 Vector{Any}:在內部,這是一個指向可以容納任何類型 (Any) 的“盒子”的指針數組。這讓事情變得更慢:對 Any 值的每個 + 計算都必須動態查找對象的類型,找出要呼叫的 + 函式,並分配一個新的“盒子”來儲存結果。如果你在 Julia 中執行這個邏輯,它甚至比 Python 還要慢。這與手動 Numpy 數組實現的問題相同。AnyPython 處理無類型值更快(更好地優化處理) 。這是因為 Julia 希望您在所有性能關鍵的情況下都使用“具體”類型的數組,以及為什麼您應該始終避免在 Julia 中使用抽像類型。
  • 另一方面,Numpy 數組可以利用所有元素都是同一類型的事實。這實際上比 C 更快!原因是 NumPy 通過利用SIMD 指令獲得了額外的加速。

您也可以為 C 和 Julia 執行 SIMD。

朱莉婭

function mysum(A)
   s = zero(eltype(A)) 
   @simd for a in A
       s += a
   end
   return s
end

C

const Clib_fastmath = tempname()   # make a temporary file

# The same as above but with a -ffast-math flag added
open(`gcc -fPIC -O3 -msse3 -xc -shared -ffast-math -o $(Clib_fastmath * "." * Libdl.dlext) -`, "w") do f
   print(f, C_code) 
end

# define a Julia function that calls the C function:
c_sum_fastmath(X::Array{Float64}) = ccall(("c_sum", Clib_fastmath), Float64, (Csize_t, Ptr{Float64}), length(X), X)

在此處輸入圖像描述

然而,一旦你使用 SIMD,你真的需要知道你在實現和使用它時做了什麼。@simd 宏的不正確使用可能會導致意外結果。

雖然這聽起來像是深奧的電腦科學內容,但您可以查看以下問題,了解在合理的正常情況下浮點數學結果會發生什麼。迄今為止,我所見過的最能證明浮點數學危險的最有趣的例子來自Stefan Karpinksi。您可以使用它來說明從左到右求和和從左到右求和與 @simd 註釋的區別。

本質上,如果你在 Julia 中使用這個程式碼片段,

function sumsto(x::Float64)
   0 <= x < exp2(970) || throw(ArgumentError("sum must be in [0,2^970)"))
   n, p₀ = Base.decompose(x) # integers such that `n*exp2(p₀) == x`
   [floatmax(); [exp2(p) for p in reverse(-1074:969) if iseven(n >> (p-p₀))]

   -floatmax(); [exp2(p) for p in reverse(-1074:969) if isodd(n >> (p-p₀))]]
end    

s = rand()*10^6
v = sumsto(s)

您將隨機向量 (10^6) 從最高值到最低值排序。Julia 或 Python 中的標準算法都沒有提供真正的總和,儘管有趣的是,在這個例子中 SIMD 在這種情況下最接近。原因是在求和的情況下,嚴格的從左到右求和實際上是最不准確的算法之一,因此將@simd 放在循環中可以更快、更準確地計算真實和,因為 SIMD 在很大程度上允許重新關聯在浮點數中實際上不相關的數學關聯運算。這包括+*。通常,(a + b) + c可以a + (b + c)改為評估,但這不會為浮點值計算相同的東西。

正確答案可以使用Kahan-Babuska-Neumaier (KBN)求和算法來計算總和(和累積總和)。這帶來了大約 20 倍的速度差異,這就是為什麼沒有一個傳統的實現使用它的原因。

在我看來,底線是 Julia 服務於一個利基市場。雖然論文指出 Julia 兼具速度和可用性,但主要問題是在 Julia 中達到 C(C++ 或 Fortran 速度)仍然是一項艱鉅的任務。如果您需要真正的速度,公司會更喜歡 C++(至少在我的領域,量化金融,C++ 是迄今為止最常用的語言,還有一些利基產品,如 Lexifi 或彭博社的 DLIB 使用的一些小眾產品,它們專注於復雜的衍生產品)。大多數使用者不需要這種速度,因為模型太小,或者數據不夠大。例如,每天的彭博使用者永遠無法下載那麼多的數據(BBG 對每日數據使用量有限制),它確實會產生明顯的差異,就像 ~150 秒(相比 Julia 中的 2 秒)不要'

也就是說,包確實在 Julia 中開發,圖表功能和 DataFrames 包確實做得很好。您可以在這裡查看製作一些不錯的互動式圖表(動態選項 PnL 圖表)需要多少程式碼。

此外,自 1.0 版發布以來,您通常不會再因棄用而面臨破壞程式碼的問題。如果日常數據集增長,並且計算能力對於普通程序員來說變得更加重要,那麼 Julia 很可能會成為一個大玩家(主觀意見)。

引用自:https://economics.stackexchange.com/questions/48421