常見問題

一般

Julia 是以某人或某事命名的嗎?

否。

為什麼不將 Matlab/Python/R/… 程式碼編譯成 Julia?

由於許多人熟悉其他動態語言的語法,而且已經有許多程式碼是用這些語言編寫的,因此自然會好奇為什麼我們不直接將 Matlab 或 Python 前端插入 Julia 後端(或將程式碼「轉譯」成 Julia),以便在不需要程式設計師學習新語言的情況下,獲得 Julia 的所有效能優勢。很簡單,對吧?

基本問題在於Julia 的編譯器沒有什麼特別之處:我們使用的是一個通用的編譯器(LLVM),沒有其他語言開發人員不知道的「秘密武器」。事實上,Julia 的編譯器在許多方面比其他動態語言(例如 PyPy 或 LuaJIT)的編譯器簡單得多。Julia 的效能優勢幾乎完全來自於其前端:其語言語義允許編寫良好的 Julia 程式為編譯器提供更多機會來產生有效率的程式碼和記憶體配置。如果您嘗試將 Matlab 或 Python 程式碼編譯成 Julia,我們的編譯器將受到 Matlab 或 Python 的語義限制,無法產生比這些語言現有編譯器更好的程式碼(甚至可能更差)。語義扮演關鍵角色,也是為什麼幾個現有的 Python 編譯器(例如 Numba 和 Pythran)只嘗試最佳化語言的一小部分(例如對 Numpy 陣列和純量的操作),而且對於這個子集,它們已經做得和我們在相同語義下所能做的一樣好,甚至更好。參與這些專案的人才華洋溢,而且已經取得了驚人的成就,但是將編譯器套用在設計為要解釋執行的語言上是一個非常困難的問題。

Julia 的優勢在於,良好的效能不僅限於「內建」類型和運算子的小子集,而且可以撰寫高階的類型泛用程式碼,在任意使用者定義的類型上運作,同時保持快速且記憶體使用率低。像 Python 這樣的語言中的類型,並未提供足夠的資訊給編譯器,以獲得類似的能力,因此,只要您將這些語言用作 Julia 前端,就會陷入困境。

由於類似的理由,自動翻譯成 Julia 通常也會產生難以閱讀、速度慢、非慣用的程式碼,這對於從其他語言移植到 Julia 的原生程式碼而言,並非一個好的起點。

另一方面,語言「互操作性」非常有用:我們希望從 Julia 中利用其他語言中現有的高品質程式碼(反之亦然)!啟用此功能的最佳方式並非轉譯器,而是透過簡易的語言間呼叫功能。我們在這方面付出了很多努力,從內建的 ccall 內在函式(呼叫 C 和 Fortran 函式庫)到將 Julia 連接到 Python、Matlab、C++ 等的 JuliaInterop 套件。

公開 API

Julia 如何定義其公開 API?

Julia Base 和標準函式庫功能,說明在 文件 中,未標記為不穩定(例如實驗性和內部)的,都涵蓋在 SemVer 中。函式、類型和常數,如果未包含在文件中,則不屬於公開 API,即使它們有文件字串

有一個有用的未記錄函式/類型/常數。我可以使用它嗎?

如果您使用非公開 API,更新 Julia 可能會中斷您的程式碼。如果程式碼是獨立的,將其複製到您的專案中可能是一個好主意。如果您想依賴複雜的非公開 API,特別是在從穩定套件中使用它時,最好開啟一個 議題提交請求 以開始討論將其轉換為公開 API。但是,我們並不反對嘗試建立公開穩定介面的套件,同時依賴 Julia 的非公開實作細節,並緩衝不同 Julia 版本之間的差異。

文件不夠準確。我可以依賴現有的行為嗎?

請開啟一個 議題提交請求 以開始討論將現有行為轉換為公開 API。

工作階段和 REPL

如何在記憶體中刪除一個物件?

Julia 沒有類似 MATLAB 的 clear 函數;一旦在 Julia 工作階段中定義一個名稱(技術上是在模組 Main 中),它就會一直存在。

如果記憶體使用量是您的疑慮,您隨時可以將物件替換為使用較少記憶體的物件。例如,如果 A 是您不再需要的千兆位元組陣列,您可以使用 A = nothing 釋放記憶體。下次垃圾收集執行時,記憶體將會釋放;您可以使用 GC.gc() 強制執行此動作。此外,嘗試使用 A 可能會導致錯誤,因為大多數方法未定義在 Nothing 類型上。

如何修改工作階段中類型的宣告?

也許您已定義一個類型,然後發現需要新增一個新欄位。如果您在 REPL 中嘗試此操作,您會收到錯誤

ERROR: invalid redefinition of constant MyType

無法重新定義 Main 模組中的類型。

當您開發新程式碼時,這可能會造成不便,但有一個絕佳的解決方法。模組可以透過重新定義來取代,因此如果您將所有新程式碼包裝在模組中,您可以重新定義類型和常數。您無法將類型名稱匯入 Main,然後預期可以在其中重新定義它們,但您可以使用模組名稱來解析範圍。換句話說,在開發時,您可能會使用類似下列內容的工作流程

include("mynewcode.jl")              # this defines a module MyModule
obj1 = MyModule.ObjConstructor(a, b)
obj2 = MyModule.somefunction(obj1)
# Got an error. Change something in "mynewcode.jl"
include("mynewcode.jl")              # reload the module
obj1 = MyModule.ObjConstructor(a, b) # old objects are no longer valid, must reconstruct
obj2 = MyModule.somefunction(obj1)   # this time it worked!
obj3 = MyModule.someotherfunction(obj2, c)
...

腳本

如何檢查目前檔案是否作為主腳本執行?

當使用 julia file.jl 將檔案作為主腳本執行時,可能會想要啟用額外的功能,例如命令列引數處理。判斷檔案是否以這種方式執行的其中一種方法是檢查 abspath(PROGRAM_FILE) == @__FILE__ 是否為 true

不過,建議不要撰寫同時作為腳本和可匯入函式庫的檔案。如果需要同時以函式庫和腳本的形式提供功能,最好將其寫成函式庫,然後將功能匯入到不同的腳本中。

如何於腳本中捕捉 CTRL-C

使用 julia file.jl 執行 Julia 腳本時,當您嘗試使用 CTRL-C (SIGINT) 終止腳本時,不會擲回 InterruptException。若要在終止 Julia 腳本(無論是否由 CTRL-C 造成)之前執行某段程式碼,請使用 atexit。或者,您可以使用 julia -e 'include(popfirst!(ARGS))' file.jl 執行腳本,同時可以在 try 區塊中捕捉 InterruptException。請注意,使用此策略時,PROGRAM_FILE 將不會設定。

如何使用 #!/usr/bin/env 將選項傳遞給 julia

在所謂的 shebang 行中傳遞選項給 julia,例如 #!/usr/bin/env julia --startup-file=no,在許多平台(BSD、macOS、Linux)上無法運作,因為在這些平台上,核心與殼層不同,不會在空白字元處拆分引數。選項 env -S 會將單一引數字串拆分成多個引數,類似於殼層,提供了一個簡單的解決方法

#!/usr/bin/env -S julia --color=yes --startup-file=no
@show ARGS  # put any Julia code here
注意

選項 env -S 出現在 FreeBSD 6.0(2005)、macOS Sierra(2016)和 GNU/Linux coreutils 8.30(2018)中。

為什麼 run 不支援 * 或管道來編寫外部程式腳本?

Julia 的 run 函式直接啟動外部程式,不會呼叫 作業系統殼層(與其他語言(例如 Python、R 或 C)中的 system("...") 函式不同)。這表示 run 不會執行 * 的萬用字元擴充("glob"),也不會詮釋 殼層管道,例如 |>

不過,你仍然可以使用 Julia 功能來執行 glob 和管道。例如,內建 pipeline 函式允許你串連外部程式和檔案,類似於殼層管道,而 Glob.jl 套件 實作了與 POSIX 相容的 glob。

當然,你可以透過明確地傳遞殼層和命令字串給 run 來透過殼層執行程式,例如 run(`sh -c "ls > files.txt"`) 以使用 Unix Bourne 殼層,但你通常應該優先使用純 Julia 編寫腳本,例如 run(pipeline(`ls`, "files.txt"))。我們預設避免使用殼層的原因是 殼層執行很糟糕:透過殼層啟動程序很慢,容易受到特殊字元引用的影響,錯誤處理不佳,而且在可攜性方面有問題。(Python 開發人員也得到 類似的結論。)

變數和指派

為什麼我會從一個簡單的迴圈中得到 UndefVarError

你可能會遇到像這樣的問題

x = 0
while x < 10
    x += 1
end

並注意到它在互動式環境(例如 Julia REPL)中運作良好,但當你嘗試在腳本或其他檔案中執行它時,它會給出 UndefVarError: `x` not defined。發生這種情況的原因是 Julia 通常要求你明確指派給局部範圍內的全域變數

在這裡,x 是全域變數,while 定義了一個 局部範圍,而 x += 1 是在該局部範圍內對全域變數的指派。

如上所述,Julia(1.5 版或更新版本)允許你省略 REPL(以及許多其他互動式環境)中程式碼的 global 關鍵字,以簡化探索(例如從函式複製貼上程式碼以互動式執行)。但是,一旦你轉移到檔案中的程式碼,Julia 就需要對全域變數採取更嚴謹的方法。你至少有三個選項

  1. 將程式碼放入函式中(這樣 x 就是函式中的局部變數)。一般來說,使用函式而不是全域腳本是一種良好的軟體工程(在網路上搜尋「為什麼全域變數不好」以查看許多解釋)。在 Julia 中,全域變數也 很慢
  2. 使用 let 區塊包裝程式碼。(這使得 x 成為 let ... end 陳述式中的局部變數,再次消除了對 global 的需求)。
  3. 在指派給 x 之前,在局部範圍內明確將 x 標記為 global,例如寫入 global x += 1

可以在手冊部分 關於軟範圍 中找到更多說明。

函數

我將一個引數 x 傳遞給函數,在函數內修改它,但函數外,變數 x 仍然不變。為什麼?

假設您這樣呼叫函數

julia> x = 10
10

julia> function change_value!(y)
           y = 17
       end
change_value! (generic function with 1 method)

julia> change_value!(x)
17

julia> x # x is unchanged!
10

在 Julia 中,變數 x 的繫結無法透過將 x 作為引數傳遞給函數來變更。在上述範例中呼叫 change_value!(x) 時,y 是新建立的變數,最初繫結到 x 的值,也就是 10;然後 y 重新繫結到常數 17,而外部範圍的變數 x 則保持不變。

不過,如果 x 繫結到 Array 類型的物件(或任何其他可變類型)。在函數內,您無法將 x 從這個陣列「解除繫結」,但您可以變更其內容。例如

julia> x = [1,2,3]
3-element Vector{Int64}:
 1
 2
 3

julia> function change_array!(A)
           A[1] = 5
       end
change_array! (generic function with 1 method)

julia> change_array!(x)
5

julia> x
3-element Vector{Int64}:
 5
 2
 3

這裡我們建立一個函數 change_array!,它將 5 指定給傳遞陣列的第一個元素(在呼叫位置繫結到 x,在函數內繫結到 A)。請注意,在函數呼叫後,x 仍然繫結到同一個陣列,但該陣列的內容已變更:變數 Ax 是指稱同一個可變 Array 物件的不同繫結。

我可以在函數內使用 usingimport 嗎?

不,您不被允許在函式內有 usingimport 陳述式。如果您想匯入模組,但只在特定函式或函式組中使用其符號,您有兩個選項

  1. 使用 import

    import Foo
    function bar(...)
        # ... refer to Foo symbols via Foo.baz ...
    end

    這會載入模組 Foo,並定義一個變數 Foo,該變數參考模組,但不會將模組中的任何其他符號匯入目前的名稱空間。您透過其限定名稱 Foo.bar 等來參考 Foo 符號。

  2. 將您的函式包在模組中

    module Bar
    export bar
    using Foo
    function bar(...)
        # ... refer to Foo.baz as simply baz ....
    end
    end
    using Bar

    這會匯入 Foo 中的所有符號,但僅在模組 Bar 內匯入。

... 算子做了什麼?

... 算子的兩個用途:吸入和噴灑

許多 Julia 新手會對 ... 算子的用途感到困惑。讓 ... 算子令人困惑的部分原因是,它會根據上下文而有兩種不同的意義。

... 在函式定義中將許多引數組合成一個引數

在函式定義的上下文中,... 算子用於將許多不同的引數組合成一個單一引數。這種將許多不同的引數組合成一個單一引數的 ... 用法稱為吸入

julia> function printargs(args...)
           println(typeof(args))
           for (i, arg) in enumerate(args)
               println("Arg #$i = $arg")
           end
       end
printargs (generic function with 1 method)

julia> printargs(1, 2, 3)
Tuple{Int64, Int64, Int64}
Arg #1 = 1
Arg #2 = 2
Arg #3 = 3

如果 Julia 是一種更自由使用 ASCII 字元的語言,那麼吸入算子可能會寫成 <-... 而不是 ...

... 將一個引數拆分為許多不同的引數,用於函式呼叫

與在定義函式時使用 ... 運算子將許多不同的引數合併為一個引數相反,... 運算子也用於在函式呼叫的語境中將單一函式引數拆分為許多不同的引數。這種 ... 的用法稱為 splatting

julia> function threeargs(a, b, c)
           println("a = $a::$(typeof(a))")
           println("b = $b::$(typeof(b))")
           println("c = $c::$(typeof(c))")
       end
threeargs (generic function with 1 method)

julia> x = [1, 2, 3]
3-element Vector{Int64}:
 1
 2
 3

julia> threeargs(x...)
a = 1::Int64
b = 2::Int64
c = 3::Int64

如果 Julia 是一種更自由使用 ASCII 字元的語言,splatting 運算子可能會寫成 ...->,而不是 ...

指派運算的回傳值是什麼?

運算子 = 永遠回傳右手邊,因此

julia> function threeint()
           x::Int = 3.0
           x # returns variable x
       end
threeint (generic function with 1 method)

julia> function threefloat()
           x::Int = 3.0 # returns 3.0
       end
threefloat (generic function with 1 method)

julia> threeint()
3

julia> threefloat()
3.0

類似地

julia> function twothreetup()
           x, y = [2, 3] # assigns 2 to x and 3 to y
           x, y # returns a tuple
       end
twothreetup (generic function with 1 method)

julia> function twothreearr()
           x, y = [2, 3] # returns an array
       end
twothreearr (generic function with 1 method)

julia> twothreetup()
(2, 3)

julia> twothreearr()
2-element Vector{Int64}:
 2
 3

類型、類型宣告和建構函式

「類型穩定」是什麼意思?

這表示輸出的類型可以從輸入的類型預測。特別是,這表示輸出的類型不會根據輸入的而有所不同。下列程式碼不是類型穩定的

julia> function unstable(flag::Bool)
           if flag
               return 1
           else
               return 1.0
           end
       end
unstable (generic function with 1 method)

它會根據其引數的值回傳 IntFloat64。由於 Julia 無法在編譯時預測這個函式的回傳類型,因此任何使用它的運算都必須能夠處理這兩種類型的值,這使得產生快速的機器碼變得困難。

為什麼 Julia 會對某些看似合理的運算給出 DomainError

某些運算在數學上是有意義的,但會導致錯誤

julia> sqrt(-2.0)
ERROR: DomainError with -2.0:
sqrt was called with a negative real argument but will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
[...]

此行為是類型穩定性需求的不便後果。在 sqrt 的情況下,大多數使用者希望 sqrt(2.0) 給出實數,如果它產生複數 1.4142135623730951 + 0.0im,他們會不滿意。可以撰寫 sqrt 函數,僅在傳遞負數時切換到複數值輸出(這是 sqrt 在某些其他語言中所執行的動作),但結果將不會是 類型穩定,而且 sqrt 函數的效能會很差。

在這些和其他情況下,您可以透過選擇傳達您願意接受結果可以表示的輸出類型輸入類型來取得您要的結果

julia> sqrt(-2.0+0im)
0.0 + 1.4142135623730951im

如何限制或計算類型參數?

參數化類型的參數可以容納類型或位元值,而類型本身會選擇如何使用這些參數。例如,Array{Float64, 2} 以類型 Float64 為參數來表達其元素類型,並以整數值 2 來表達其維度數。在定義您自己的參數化類型時,您可以使用子類型約束來宣告某個參數必須是某個抽象類型或先前類型參數的子類型 (<:)。然而,並沒有專門的語法來宣告參數必須是給定類型的 — 也就是說,您無法直接宣告維度類型的參數 isa Intstruct 定義中,例如。同樣地,您無法對類型參數進行運算(包括加法或減法等簡單運算)。相反地,這些類型的約束和關係可能會透過額外的類型參數來表達,這些參數會在類型的 建構函式 中進行運算和強制執行。

舉例來說,考慮

struct ConstrainedType{T,N,N+1} # NOTE: INVALID SYNTAX
    A::Array{T,N}
    B::Array{T,N+1}
end

使用者希望強制執行第三個類型參數始終是第二個參數加上一。這可以用一個明確的類型參數來實作,並由 內部建構函式方法 進行檢查(它可以與其他檢查結合)

struct ConstrainedType{T,N,M}
    A::Array{T,N}
    B::Array{T,M}
    function ConstrainedType(A::Array{T,N}, B::Array{T,M}) where {T,N,M}
        N + 1 == M || throw(ArgumentError("second argument should have one more axis" ))
        new{T,N,M}(A, B)
    end
end

這個檢查通常是無成本的,因為編譯器可以對有效的具體類型省略檢查。如果第二個引數也經過運算,提供一個執行此計算的 外部建構函式方法 可能是有利的

ConstrainedType(A) = ConstrainedType(A, compute_B(A))

為什麼 Julia 使用原生機器整數運算?

Julia 對整數運算使用機器運算。這表示 Int 值的範圍是有界的,並且在任一端都會換行,因此加、減和乘以整數可能會溢位或下溢,導致一些結果一開始可能會令人不安

julia> x = typemax(Int)
9223372036854775807

julia> y = x+1
-9223372036854775808

julia> z = -y
-9223372036854775808

julia> 2*z
0

顯然地,這與數學整數的行為相去甚遠,你可能會認為這對於高級程式語言來說,不理想,因為它會將此暴露給使用者。然而,對於效率和透明度至關重要的數值工作而言,其他替代方案更糟。

一個可以考慮的替代方案是檢查每個整數運算的溢位,並在溢位的狀況下,將結果提升為更大的整數類型,例如 Int128BigInt。不幸的是,這會在每個整數運算上引入大量的開銷(想想遞增迴圈計數器)——它需要發出程式碼,以便在算術指令和處理潛在溢位的分支後,執行執行時期的溢位檢查。更糟糕的是,這將導致每個涉及整數的運算都具有不穩定的類型。正如我們上面提到的,類型穩定性對於有效產生高效的程式碼至關重要。如果你無法依賴整數運算的結果是整數,就無法像 C 和 Fortran 編譯器那樣產生快速、簡單的程式碼。

避免型態不穩定的另一種方法,是將 IntBigInt 型態合併成單一的混合整數型態,當結果不再符合機器整數大小時,會在內部變更表示方式。雖然這在 Julia 程式碼層級上避免了型態不穩定,但只是將問題掃到地毯下,將所有相同的困難轉嫁到實作此混合整數型態的 C 程式碼上。這種方法可以運作,在許多情況下甚至可以變得非常快,但有幾個缺點。一個問題是整數和整數陣列的記憶體中表示方式不再符合 C、Fortran 和其他具有原生機器整數的語言所使用的自然表示方式。因此,要與這些語言互通,我們最終還是需要引入原生整數型態。任何無界整數表示方式都不可能有固定的位元數,因此無法儲存在具有固定大小槽位的陣列中,大型整數值將永遠需要獨立的堆疊分配儲存空間。當然,無論使用多麼巧妙的混合整數實作,總是會有效能陷阱,效能會在預料之外下降。複雜的表示方式、缺乏與 C 和 Fortran 的互通性、無法在沒有額外堆疊儲存空間的情況下表示整數陣列,以及不可預測的效能特性,讓即使是最巧妙的混合整數實作也成為高效能數值運算的糟糕選擇。

使用混合整數或提升為 BigInt 的替代方案,是使用飽和整數運算,其中加到最大的整數值會讓它保持不變,減去最小的整數值也一樣。這正是 Matlab™ 所做的

>> int64(9223372036854775807)

ans =

  9223372036854775807

>> int64(9223372036854775807) + 1

ans =

  9223372036854775807

>> int64(-9223372036854775808)

ans =

 -9223372036854775808

>> int64(-9223372036854775808) - 1

ans =

 -9223372036854775808

乍看之下,這似乎相當合理,因為 9223372036854775807 遠比 -9223372036854775808 接近 9223372036854775808,而且整數仍以固定的方式表示,與 C 和 Fortran 相容。然而,飽和整數運算卻有很大的問題。第一個也是最明顯的問題是,這並非機器整數運算運作的方式,因此實作飽和運算需要在每個機器整數運算後發出指令,以檢查是否發生下溢或溢位,並適當地將結果替換為 typemin(Int)typemax(Int)。這會讓每個整數運算從單一、快速的指令擴充為六個指令,可能還包括分支。好痛。但情況會更糟,飽和整數運算並不具備結合律。請考慮以下 Matlab 計算

>> n = int64(2)^62
4611686018427387904

>> n + (n - 1)
9223372036854775807

>> (n + n) - 1
9223372036854775806

這使得撰寫許多基本的整數演算法變得困難,因為許多常見的技術仰賴機器加法會發生溢位的結合律。請考慮使用 Julia 中的運算式 (lo + hi) >>> 1 來尋找整數值 lohi 之間的中間點

julia> n = 2^62
4611686018427387904

julia> (n + 2n) >>> 1
6917529027641081856

看到了嗎?沒問題。這是 2^62 和 2^63 之間正確的中間點,儘管 n + 2n 為 -4611686018427387904。現在在 Matlab 中試試看

>> (n + 2*n)/2

ans =

  4611686018427387904

糟了。將 >>> 算子加入 Matlab 並沒有幫助,因為在加法 n2n 時發生的飽和已經毀損了計算正確中間點所需的資訊。

結合律的缺乏不僅對無法依賴此律的程式設計師來說很不幸,也破壞了編譯器可能想用來最佳化整數運算的任何方法。例如,由於 Julia 整數使用正常的機器整數運算,LLVM 可以積極最佳化像 f(k) = 5k-1 這樣的簡單小函式。此函式的機器碼如下

julia> code_native(f, Tuple{Int})
  .text
Filename: none
  pushq %rbp
  movq  %rsp, %rbp
Source line: 1
  leaq  -1(%rdi,%rdi,4), %rax
  popq  %rbp
  retq
  nopl  (%rax,%rax)

函式的實際主體只有一個 leaq 指令,它同時計算整數乘法和加法。當 f 內嵌到另一個函式中時,這甚至更有利

julia> function g(k, n)
           for i = 1:n
               k = f(k)
           end
           return k
       end
g (generic function with 1 methods)

julia> code_native(g, Tuple{Int,Int})
  .text
Filename: none
  pushq %rbp
  movq  %rsp, %rbp
Source line: 2
  testq %rsi, %rsi
  jle L26
  nopl  (%rax)
Source line: 3
L16:
  leaq  -1(%rdi,%rdi,4), %rdi
Source line: 2
  decq  %rsi
  jne L16
Source line: 5
L26:
  movq  %rdi, %rax
  popq  %rbp
  retq
  nop

由於對 f 的呼叫被內嵌,迴圈主體最後只是一個 leaq 指令。接下來,考慮如果我們讓迴圈迭代次數固定會發生什麼事

julia> function g(k)
           for i = 1:10
               k = f(k)
           end
           return k
       end
g (generic function with 2 methods)

julia> code_native(g,(Int,))
  .text
Filename: none
  pushq %rbp
  movq  %rsp, %rbp
Source line: 3
  imulq $9765625, %rdi, %rax    # imm = 0x9502F9
  addq  $-2441406, %rax         # imm = 0xFFDABF42
Source line: 5
  popq  %rbp
  retq
  nopw  %cs:(%rax,%rax)

因為編譯器知道整數加法和乘法是結合的,而且乘法會分配在加法上——這兩個都不適用於飽和運算——它可以將整個迴圈最佳化為僅乘法和加法。飽和運算完全破壞了這種最佳化,因為結合性和分配性可能會在每個迴圈反覆運算中失敗,導致不同的結果,具體取決於失敗發生在反覆運算中的哪個部分。編譯器可以展開迴圈,但它無法將多個運算代數化簡為更少的等效運算。

對整數運算無聲溢位的最合理的替代方案是在所有地方執行檢查運算,在加法、減法和乘法溢位時引發錯誤,產生不正確的值。在這個部落格文章中,Dan Luu 分析了這一點,發現這種方法的成本並非理論上應有的微不足道,而是由於編譯器(LLVM 和 GCC)無法優雅地針對新增的溢位檢查進行最佳化,因此最終會產生大量的成本。如果這在未來有所改善,我們可以考慮在 Julia 中預設為檢查整數運算,但就目前而言,我們必須接受溢位的可能性。

在此期間,可以使用外部函式庫(例如 SaferIntegers.jl)來達成防溢整數運算。請注意,如前所述,使用這些函式庫會大幅增加使用檢查整數類型的程式碼執行時間。然而,對於有限的使用,這遠遠小於用於所有整數運算的情況。您可以在此處追蹤討論狀態。

遠端執行期間 UndefVarError 的可能原因是什麼?

正如錯誤所述,遠端節點上 UndefVarError 的直接原因是沒有該名稱的繫結。讓我們探討一些可能的原因。

julia> module Foo
           foo() = remotecall_fetch(x->x, 2, "Hello")
       end

julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: `Foo` not defined
Stacktrace:
[...]

閉包 x->x 帶有對 Foo 的參照,並且由於 Foo 在節點 2 上不可用,因此會擲出 UndefVarError

Main 以外的模組下的全域變數不會以值的形式序列化到遠端節點。只會傳送參照。建立全域變數繫結的函式(Main 除外)可能會導致稍後擲出 UndefVarError

julia> @everywhere module Foo
           function foo()
               global gvar = "Hello"
               remotecall_fetch(()->gvar, 2)
           end
       end

julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: `gvar` not defined
Stacktrace:
[...]

在上述範例中,@everywhere module Foo 在所有節點上定義了 Foo。但是呼叫 Foo.foo() 在本機節點上建立了新的全域變數繫結 gvar,但在節點 2 上找不到它,導致 UndefVarError 錯誤。

請注意,這不適用於在模組 Main 下建立的全域變數。模組 Main 下的全域變數會序列化,並在遠端節點上建立新的繫結。

julia> gvar_self = "Node1"
"Node1"

julia> remotecall_fetch(()->gvar_self, 2)
"Node1"

julia> remotecall_fetch(varinfo, 2)
name          size summary
––––––––– –––––––– –––––––
Base               Module
Core               Module
Main               Module
gvar_self 13 bytes String

這不適用於 functionstruct 宣告。但是,繫結到全域變數的匿名函式會序列化,如下所示。

julia> bar() = 1
bar (generic function with 1 method)

julia> remotecall_fetch(bar, 2)
ERROR: On worker 2:
UndefVarError: `#bar` not defined
[...]

julia> anon_bar  = ()->1
(::#21) (generic function with 1 method)

julia> remotecall_fetch(anon_bar, 2)
1

疑難排解 "method not matched":參數類型不變性和 MethodError

為什麼宣告 foo(bar::Vector{Real}) = 42 然後呼叫 foo([1]) 無法運作?

如果您嘗試這麼做,您會看到結果是 MethodError

julia> foo(x::Vector{Real}) = 42
foo (generic function with 1 method)

julia> foo([1])
ERROR: MethodError: no method matching foo(::Vector{Int64})

Closest candidates are:
  foo(!Matched::Vector{Real})
   @ Main none:1

Stacktrace:
[...]

這是因為 Vector{Real} 不是 Vector{Int} 的超類型!您可以使用類似 foo(bar::Vector{T}) where {T<:Real}(或如果函式主體中不需要靜態參數 T,則使用簡短形式 foo(bar::Vector{<:Real}))來解決此問題。T 是萬用字元:您首先指定它必須是 Real 的子類型,然後指定函式採用元素為該類型的 Vector。

這個問題同樣適用於任何複合類型 Comp,而不僅僅是 Vector。如果 Comp 有宣告為類型 Y 的參數,則另一個類型 Comp2 有類型 X<:Y 的參數,則它不是 Comp 的子類型。這是類型不變性(相反地,Tuple 在其參數中是類型協變的)。請參閱 參數化複合類型 以獲得更多說明。

Julia 為什麼使用 * 進行字串串接?為什麼不是 + 或其他?

反對 +主要論點 是字串串接不是可交換的,而 + 通常用作可交換運算子。雖然 Julia 社群承認其他語言使用不同的運算子,而 * 對某些使用者來說可能不熟悉,但它傳達了某些代數性質。

請注意,您也可以使用 string(...) 來串接字串(以及轉換為字串的其他值);類似地,可以使用 repeat 代替 ^ 來重複字串。 插值語法 也可用於建構字串。

套件和模組

「using」和「import」有什麼不同?

usingimport 有幾個不同之處(請參閱 模組區段),但有一個重要的不同之處乍看之下可能不直觀,而且在表面上(即語法上)看起來可能非常次要。使用 using 載入模組時,您需要說 function Foo.bar(... 以使用新方法擴充模組 Foo 的函式 bar,但使用 import Foo.bar 時,您只需要說 function bar(... 即可自動擴充模組 Foo 的函式 bar

之所以有這個重要的區別而給予不同的語法,是因為您不想意外擴充您不知道存在的函式,因為這很容易造成錯誤。這最有可能發生在使用一般型別(例如字串或整數)的方法上,因為您和另一個模組都可以定義一個方法來處理這種一般型別。如果您使用 import,則會用您的新實作取代另一個模組的 bar(s::AbstractString) 實作,這很容易執行完全不同的操作(並中斷依賴呼叫 bar 的模組 Foo 中所有/許多未來的函式使用)。

虛無和遺失值

Julia 中的「null」、「虛無」或「遺失」如何運作?

與許多語言(例如 C 和 Java)不同,Julia 物件在預設情況下無法為「null」。當一個參照(變數、物件欄位或陣列元素)未初始化時,存取它會立即擲回一個錯誤。可以使用 isdefinedisassigned 函式來偵測這種情況。

有些函式僅用於其副作用,不需要傳回值。在這些情況下,慣例是傳回值 nothing,它只是一個 Nothing 類型的單例物件。這是一個沒有欄位的普通類型;除了這個慣例和 REPL 沒有為它列印任何內容之外,它沒有什麼特別之處。有些語言結構原本沒有值也會產生 nothing,例如 if false; end

對於 T 類型的值 x 有時只存在的情況,Union{T, Nothing} 類型可用於函式引數、物件欄位和陣列元素類型,等同於其他語言中的 NullableOptionMaybe。如果值本身可以為 nothing(特別是當 TAny 時),Union{Some{T}, Nothing} 類型更為合適,因為 x == nothing 表示沒有值,而 x == Some(nothing) 表示存在等於 nothing 的值。something 函式允許解開 Some 物件並使用預設值代替 nothing 引數。請注意,編譯器在處理 Union{T, Nothing} 引數或欄位時能夠產生有效的程式碼。

要表示統計意義上的遺失資料(R 中的 NA 或 SQL 中的 NULL),請使用 missing 物件。有關更多詳細資訊,請參閱 遺失值 部分。

在某些語言中,空元組 (()) 被視為虛無的標準形式。然而,在 Julia 中,最好將它視為只包含零個值的常規元組。

寫作 Union{}(一個空的聯合類型)的空(或「底部」)類型,是一個沒有值和子類型(除了它自己)的類型。您通常不需要使用此類型。

記憶體

xy 是陣列時,為什麼 x += y 會配置記憶體?

在 Julia 中,x += y 會在降低期間被 x = x + y 取代。對於陣列,這會造成一個後果,它會配置一個新的陣列來儲存結果,而不是將結果儲存在與 x 相同的記憶體位置。如果您偏好變異 x,請使用 x .+= y 個別更新每個元素。

儘管這種行為可能會讓一些人感到驚訝,但這個選擇是經過深思熟慮的。主要原因是 Julia 中存在不可變的物件,它們在建立後無法變更其值。事實上,數字是一個不可變的物件;陳述式 x = 5; x += 1 沒有修改 5 的意義,它們修改與 x 連結的值。對於不可變的物件,變更值的唯一方法是重新指派它。

為了進一步說明,請考慮下列函數

function power_by_squaring(x, n::Int)
    ispow2(n) || error("This implementation only works for powers of 2")
    while n >= 2
        x *= x
        n >>= 1
    end
    x
end

在像 x = 5; y = power_by_squaring(x, 4) 這樣的呼叫之後,您會得到預期的結果:x == 5 && y == 625。然而,現在假設 *= 在用於矩陣時,會變異左手邊。會有兩個問題

  • 對於一般的方陣,A = A*B 無法在沒有暫存儲存的情況下實作:A[1,1] 會在您在右手邊使用它完成之前被計算並儲存在左手邊。
  • 假設您願意為計算分配一個暫存區 (這將消除大部分使 *= 就地運作的重點);如果您利用 x 的可變性,則此函數對可變輸入與不可變輸入的行為將有所不同。特別是,對於不可變的 x,在呼叫後您將 (通常) 擁有 y != x,但對於可變的 x,您將擁有 y == x

因為支援泛型程式設計被視為比透過其他方式 (例如,使用廣播或明確迴圈) 可達成的潛在效能最佳化更重要,所以像 +=*= 等運算子透過重新繫結新值來運作。

非同步 I/O 和並發同步寫入

為什麼並發寫入到同一個串流會導致交錯輸出?

雖然串流 I/O API 是同步的,但基礎實作是完全非同步的。

考量以下列印的輸出

julia> @sync for i in 1:3
           @async write(stdout, string(i), " Foo ", " Bar ")
       end
123 Foo  Foo  Foo  Bar  Bar  Bar

這會發生是因為,雖然 write 呼叫是同步的,但每個引數的寫入會在等待 I/O 的那部分完成時讓出給其他工作。

printprintln 在呼叫期間會「鎖定」串流。因此,在上述範例中將 write 變更為 println 會導致

julia> @sync for i in 1:3
           @async println(stdout, string(i), " Foo ", " Bar ")
       end
1 Foo  Bar
2 Foo  Bar
3 Foo  Bar

您可以使用 ReentrantLock 鎖定寫入,如下所示

julia> l = ReentrantLock();

julia> @sync for i in 1:3
           @async begin
               lock(l)
               try
                   write(stdout, string(i), " Foo ", " Bar ")
               finally
                   unlock(l)
               end
           end
       end
1 Foo  Bar 2 Foo  Bar 3 Foo  Bar

陣列

零維陣列與純量之間的差異為何?

零維陣列是形式為 Array{T,0} 的陣列。它們的行為類似於純量,但有重要的差異。它們值得特別說明,因為它們是一個特例,根據陣列的一般定義具有邏輯意義,但一開始可能有點不直觀。以下程式碼定義了一個零維陣列

julia> A = zeros()
0-dimensional Array{Float64,0}:
0.0

在此範例中,A 是包含一個元素的可變容器,可透過 A[] = 1.0 設定,並透過 A[] 擷取。所有零維陣列的大小相同 (size(A) == ()),且長度相同 (length(A) == 1)。特別是,零維陣列並非為空。如果您覺得這不直觀,以下有一些想法可能有助於理解 Julia 的定義。

  • 零維陣列是向量「線」和矩陣「平面」的「點」。就像一條線沒有面積(但仍表示一組事物),一個點沒有長度或任何維度(但仍表示一個事物)。
  • 我們定義 prod(()) 為 1,而陣列中的元素總數為大小的乘積。零維陣列的大小為 (),因此其長度為 1
  • 零維陣列本身沒有任何維度可供您索引 – 它們只是 A[]。我們可以對它們套用與所有其他陣列維度相同的「尾隨一」規則,因此您確實可以將它們索引為 A[1]A[1,1] 等;請參閱 省略和額外索引

了解與一般純量之間的差異也很重要。純量不是可變容器(即使它們可迭代並定義諸如 lengthgetindex 之類的東西,例如 1[] == 1)。特別是,如果 x = 0.0 被定義為純量,則嘗試透過 x[] = 1.0 來變更其值會產生錯誤。純量 x 可透過 fill(x) 轉換為包含其值的零維度陣列,反之亦然,零維度陣列 a 可透過 a[] 轉換為包含的純量。另一個差異是純量可以參與線性代數運算,例如 2 * rand(2,2),但使用零維度陣列 fill(2) * rand(2,2) 的類似運算會產生錯誤。

為什麼我的 Julia 線性代數運算基準與其他語言不同?

您可能會發現線性代數建構單元的簡單基準,例如

using BenchmarkTools
A = randn(1000, 1000)
B = randn(1000, 1000)
@btime $A \ $B
@btime $A * $B

與其他語言(例如 Matlab 或 R)相比可能不同。

由於這類運算只是相關 BLAS 函式的極薄封裝,因此造成差異的原因很可能是

  1. 各語言使用的 BLAS 函式庫,

  2. 並行執行緒的數量。

Julia 編譯並使用其 OpenBLAS 副本,目前將執行緒上限設為 8(或您的核心數量)。

修改 OpenBLAS 設定或使用不同的 BLAS 函式庫(例如 Intel MKL)編譯 Julia,可能會提升效能。您可以使用 MKL.jl,這是一個套件,可讓 Julia 的線性代數使用 Intel MKL BLAS 和 LAPACK,而非 OpenBLAS,或在討論論壇中搜尋有關如何手動設定的建議。請注意,Intel MKL 無法與 Julia 綑綁在一起,因為它不是開放原始碼。

計算叢集

如何在分散式檔案系統中管理預編譯快取?

在具有共享檔案系統的高效能運算 (HPC) 設施中使用 Julia 時,建議使用共享儲存區(透過 JULIA_DEPOT_PATH 環境變數)。自 Julia v1.10 以來,在功能相似的工作站上執行多個 Julia 程序,並使用相同的儲存區,將透過 pidfile 鎖定進行協調,僅在一個程序上花費精力進行預編譯,而其他程序則等待。預編譯程序將指示程序何時正在預編譯或等待正在預編譯的另一個程序。如果是非互動式的,訊息將透過 @debug 傳送。

然而,由於二進制碼快取,自 v1.9 以來的快取拒絕更為嚴格,使用者可能需要適當地設定 JULIA_CPU_TARGET 環境變數,以取得可在整個 HPC 環境中使用的單一快取。

Julia 發行版本

我想要使用 Julia 的穩定版、LTS 版,還是夜間版?

Julia 的穩定版本是 Julia 的最新發布版本,這是大多數人會想要執行的版本。它具有最新功能,包括效能提升。Julia 的穩定版本根據 SemVer 編號為 v1.x.y。對應於新穩定版本的 Julia 新次要版本會在經過幾週的候選版本測試後,大約每 4-5 個月進行一次。與 LTS 版本不同,穩定版本通常不會在發布另一個 Julia 穩定版本後收到錯誤修正。但是,升級到下一個穩定版本總是可行的,因為每個 Julia v1.x 版本都將繼續執行為早期版本編寫的程式碼。

如果您正在尋找非常穩定的程式碼庫,您可能會偏好 Julia 的 LTS(長期支援)版本。Julia 的目前 LTS 版本根據 SemVer 編號為 v1.6.x;這個分支會持續收到錯誤修正,直到選擇新的 LTS 分支為止,屆時 v1.6.x 系列將不再收到常規錯誤修正,而除了最保守的使用者之外,所有使用者都建議升級到新的 LTS 版本系列。作為套件開發人員,您可能會偏好為 LTS 版本開發,以最大化可以使用您的套件的使用者數量。根據 SemVer,為 v1.0 編寫的程式碼將持續適用於所有未來的 LTS 和穩定版本。一般來說,即使目標是 LTS,也可以在最新的穩定版本中開發和執行程式碼,以利用效能提升;只要避免使用新功能(例如新增的函式庫函數或新方法)即可。

如果您想利用語言的最新更新,並且不介意今日提供的版本偶爾無法正常運作,那麼您可能會偏好 Julia 的夜間版本。正如其名稱所暗示的,夜間版本的版本大約每晚都會更新(視建置基礎架構的穩定性而定)。一般來說,夜間版本相當安全,您的程式碼不會燒起來。然而,偶爾會出現回歸或問題,直到進行更徹底的預先發布測試才會發現。您可能希望針對夜間版本進行測試,以確保在發布之前,會發現影響您使用案例的回歸。

最後,您也可以考慮自己從原始碼建置 Julia。這個選項主要是針對那些在命令列中感到自在,或有興趣學習的人。如果您符合這個描述,您可能也有興趣閱讀我們的貢獻指南

可以在https://julialang.org/downloads/的下載頁面找到這些下載類型的連結。請注意,並非所有版本的 Julia 都適用於所有平台。

如何在我更新 Julia 版本後轉移已安裝套件清單?

每個 Julia 次要版本都有其自己的預設環境。因此,在安裝新的 Julia 次要版本後,您使用前一版本新增的套件將不會預設提供。給定 Julia 版本的環境是由位於與.julia/environments/中的版本號碼相符資料夾中的檔案Project.tomlManifest.toml定義的,例如.julia/environments/v1.3

如果你安裝了 Julia 的新次要版本,例如 1.4,並想要在其預設環境中使用與先前版本(例如 1.3)相同的套件,你可以將檔案 Project.toml 的內容從 1.3 資料夾複製到 1.4。然後,在新的 Julia 版本的執行階段中,輸入按鍵 ] 進入「套件管理模式」,並執行指令 instantiate

此操作會從複製的檔案中解析出一組可行的套件,這些套件與目標 Julia 版本相容,並會在適當的情況下安裝或更新它們。如果你想要複製的不僅是套件組,還包括你在先前 Julia 版本中使用的版本,你應該在執行 Pkg 指令 instantiate 之前複製 Manifest.toml 檔案。不過,請注意,套件可能會定義相容性約束,而這些約束可能會受到 Julia 版本變更的影響,因此你在 1.3 中擁有的確切版本組可能無法在 1.4 中使用。