よくある質問

目次

一般

Julia という名前には由来となった人物や物事がある?

ありません。

MATLAB/Python/R/... のコードを Julia にコンパイルしないのはなぜ?

「他の動的言語を書き慣れている人は大勢いて、そういった人々は自身が慣れ親しんだ言語で既にたくさんのコードを書いているわけで、なら MATLAB や Python をフロントエンドとして使って Julia バックエンドで実行すれば (あるいはそういった言語を Julia に "トランスパイル" すれば)、Julia の高速な実行速度を新しい言語を学ぶことなく得られるではありませんか。単純な話です。そうでしょ?」

この提案の根本的な問題は、Julia のコンパイラに特別な部分は無いという事実にあります。私たちが使ったのはありふれたコンパイラ基盤 (LLVM) であり、他の言語の開発者が知らない "秘伝のタレ" はありません。実を言えば、Julia のコンパイラは他の動的言語のコンパイラ (PyPy や LuaJIT など) よりはるかに単純です。

Julia の高い性能はほぼ全てがフロントエンドに由来します: Julia 言語の意味論のおかげで、上手く書かれた Julia プログラムはコンパイラが最適化しやすいコードとなり、効率に優れるコードとメモリレイアウトを生成しやすくなります。

MATLAB や Python のコードを Julia にコンパイルしようとすれば、Julia のコンパイラは MATLAB や Python の意味論に縛られるので、生成されるコードはそういった言語に対する既存のコンパイラが生成するコードと大して変わらない性能を持つことになります (おそらく性能は劣るはずです)。言語の意味論が重要な意味を持つという事実は、既存の Python コンパイラ (Numba や Pythran) が言語の小さなサブセット (例えば Numpy 配列とスカラーに対する演算など) だけを最適化しようとしている理由でもあります。こういった意味論に限って言えば、既存の Python コンパイラは少なくとも Julia のコンパイラ以上の性能を持ちます。こういったプロジェクトに取り組んでいる非常に有能な人々はこれまでに素晴らしい結果を残していますが、インタープリタ言語として設計された言語を後から高速になるよう修復するのは非常に難しい問題です。

Julia の強みは、高い性能が組み込み型とその演算だけからなる小さな集合に制限されておらず、任意のユーザー定義型に対して動作する高水準の型汎用なコードを書いても高い速度とメモリ効率が得られる点です。Python といった言語における型は同じ機能を達成するための十分な情報をコンパイラにそもそも提供できないので、Julia のフロントエンドとして使ってもすぐに手詰まりとなります。

同様の理由で、他の動的言語を Julia へ自動的に変換したとしても、生成されるのは読解不可能で、低速で、全く Julia らしくないコードとなるでしょう。そのようなコードを他の言語から Julia へのネイティブな移植の第一歩として使うことはできません。

一方で、言語の相互運用性 (interoperability) は非常に有用です: 他の言語で書かれた質の高いコードは Julia から活用できるべきであり、逆に Julia も他の言語で活用できなくてはなりません! これを実現する一番優れた方法はトランスパイラではなく、簡単な言語間呼び出し機能です。私たちは熱心にこの問題に取り組んできており、組み込みの ccall 命令や Julia を Python, MATLAB, C++ といった言語に接続する JuliaInterop パッケージが存在します。

セッションと REPL

メモリ上に存在するオブジェクトを削除するには?

MATLAB の clear 関数に対応する関数を Julia は持ちません。Julia セッション (正確には Main モジュール) で名前が定義されると、その名前は永久にそのままです。

メモリ使用量が心配なら、オブジェクトをメモリ使用量の少ないオブジェクトで入れ替えることができます。例えば A という数ギガバイトの配列をこれ以上使わないのであれば、A = nothing でメモリを解放できます。メモリは次にガベージコレクタが実行されたときに解放されますが、gc() でガベージコレクタを強制的に実行することもできます。さらに、ほとんどのメソッドは Nothing 型に対して定義されないので、これ以降 A を使うとエラーが発生するようになります。

セッション中に型の宣言を変更するには?

おそらく一度定義した型に対して新しいフィールドを足したくなったのでしょう。これを REPL から実行するとエラーが発生します:

ERROR: invalid redefinition of constant MyType

Main モジュール内の型は再定義できません。

新しいコードを開発しているときはこの仕様が不便に思えるかもしれませんが、素晴らしいワークアラウンドが一つ存在します: モジュールは再定義できるので、新しいコードを全てモジュールの中に入れれば型や定数の再定義が可能になります。型の名前を Main にインポートしたときはその型の再定義はできませんが、モジュール名を使ってスコープを解決できます。開発中のワークフローの一例を示します:

include("mynewcode.jl")              # 新しいモジュール MyModule が定義される
obj1 = MyModule.ObjConstructor(a, b)
obj2 = MyModule.somefunction(obj1)
# エラーが発生したので、"mynewcode.jl" のコードを変更する。
include("mynewcode.jl")              # モジュールを再度読み込む。
obj1 = MyModule.ObjConstructor(a, b) # 古いオブジェクトは有効でないので、もう一度構築する。
obj2 = MyModule.somefunction(obj1)   # 今度は動いた!
obj3 = MyModule.someotherfunction(obj2, c)
...

スクリプト

メインスクリプトとして実行されているかどうかを判定するには?

「ファイルが julia file.jl としてメインスクリプトとして実行されるときはコマンドライン引数の処理のような機能を有効にしたい」という場合もあるでしょう。abspath(PROGRAM_FILE) == @__FILE__true かどうかを判定すれば場合分けが行えます。

CTRL-C を捕捉するには?

julia file.jl で起動した Julia を CTRL-C (SIGINT) で停止させても InterruptException は発生しません。Julia スクリプトが終了するときに特定のコードを実行させるには atexit を使ってください (atexit で設定されたコードは CTRL-C で終了されたかどうかに関わらず実行されます)。また julia -e 'include(popfirst!(ARGS))' file.jl としてスクリプトを実行した場合には InterruptExceptiontry ブロックで捕捉できます。

#!/usr/bin/env を使うときに Julia へオプションを渡すには?

Linux などのプラットフォームでシェバン (shebang) と呼ばれる機能を使って julia にオプションを渡すときは、#!/usr/bin/env julia --startup-file=no などとしても julia にオプションを渡せない可能性があります。これはシェバンの引数のパース方法が規定されておらずプラットフォームによって異なるためです。Unix ライクな環境で実行可能スクリプトの julia に引数を渡すには、bash スクリプトとして起動した上で exec を使ってプロセスを julia に置き換えてください:

#!/bin/bash
#=
exec julia --color=yes --startup-file=no "${BASH_SOURCE[0]}" "$@"
=#

@show ARGS  # Julia コードをここに書く。

この例で #==# の間にあるコードは bash スクリプトとして実行され、Julia からは複数行コメントとして無視されます。bash プロセスは exec によって Julia に切り替わるので、exec 以降の Julia コードは bash からは実行されません。

情報

スクリプトで CTRL-C の捕捉を有効にするには、次のシェバンを使ってください:

#!/bin/bash
#=
exec julia --color=yes --startup-file=no -e 'include(popfirst!(ARGS))' \
    "${BASH_SOURCE[0]}" "$@"
=#

@show ARGS  # put any Julia code here

この方法では PROGRAM_FILE が設定されないことに注意が必要です。

関数

関数内で引数を変更しても、呼び出し側の値が変わらないのはなぜ?

関数を次のように呼び出したとします:

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 の値が変わっていない!
10

Julia では変数 x を関数の引数として渡しても、x が束縛する値がどれかは変わりません。上の例で change_value!(x) を呼ばれると新しい変数 y が作成され、yx の値 10 が束縛されます。その後 y には定数 17 が新たに束縛されますが、外側のスコープの x が束縛する値は 10 で変わりません。

ただし xArray 型 (あるいは任意の可変型) の値を束縛しているなら、関数の中で x が束縛する値の内容を変えることはできます (ただし、この場合でも x が束縛する値がどれかを変えることはできません)。例を示します:

julia> x = [1,2,3]
3-element Array{Int64,1}:
 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 Array{Int64,1}:
 5
 2
 3

change_array! 関数は引数に渡された配列の第一要素に 5 を代入します。この例で change_array! に渡される配列は呼び出し側で x に束縛されており、関数の中では A に束縛されます。関数の呼び出しが終了した後も x が参照する配列がどれかは変わりませんが、その配列の内容は変更されます。変数 Ax は同じ Array 型の可変オブジェクトを指す異なる束縛です。

usingimport は関数内で使える?

使えません。usingimport は関数の中で許されていません。特定の関数でのみ使うモジュールをインポートしたい場合には、二つの選択肢があります:

  1. import を使う:

    import Foo
    function bar(...)
        # ... Foo のシンボルを Foo.baz と参照する ...
    end
    

    こうするとモジュール Foo が読み込まれ、そのモジュールを参照する変数 Foo が定義されます。しかし Foo モジュールに含まれるシンボルは現在の名前空間にインポートされないので、Foo のシンボルは Foo.bar と修飾した名前で参照することになります。

  2. 関数をモジュールの中に書く:

    module Bar
    export bar
    using Foo
    function bar(...)
        # ... Foo.baz を単に baz と参照できる ....
    end
    end
    using Bar
    

    こうすると Foo のシンボルが全てインポートされますが、そのシンボルを参照できるのは Bar の中でだけです。

... 演算子は何をしている?

多くの Julia 初心者は ... 演算子の使い方が分かりにくいと感じるようです。... 演算子が分かりにくいのは、文脈によって二つの異なる意味を持つためです:

関数定義に含まれる ... は複数の引数を一つにまとめる

関数定義の文脈で使われる ... 演算子は複数の異なる引数を一つの引数にまとめます。この ... の使い方は slurp と呼ばれます1:

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 文字を使う言語だったなら、slurp 演算子は ... ではなく <-... と書かれていたかもしれません。

関数呼び出しに含まれる ... は一つの引数を複数の引数に分ける

関数呼び出しの文脈で使われる ... 演算子は単一の引数を複数の引数に分けます。これは関数定義で複数の引数を一つの引数にまとめる ... 演算子 (slurp) と対称的です。この ... の使い方は splat と呼ばれます2:

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 Array{Int64,1}:
 1
 2
 3

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

Julia がもっとクリエイティブに ASCII 文字を使う言語だったなら、splat 演算子は ... ではなく ...-> と書かれていたかもしれません。

代入文の値は?

代入演算子 = は必ず右辺の値を返します。例を示します:

julia> function threeint()
           x::Int = 3.0
           x # 変数 x の値 3 が返る。
       end
threeint (generic function with 1 method)

julia> function threefloat()
           x::Int = 3.0 # 3.0 が返る。
       end
threefloat (generic function with 1 method)

julia> threeint()
3

julia> threefloat()
3.0

次の例でも同様です:

julia> function threetup()
           x, y = [3, 3]
           x, y # タプルが返る。
       end
threetup (generic function with 1 method)

julia> function threearr()
           x, y = [3, 3] # 配列が返る。
       end
threearr (generic function with 1 method)

julia> threetup()
(3, 3)

julia> threearr()
2-element Array{Int64,1}:
 3
 3

型・型の宣言・コンストラクタについて

「型安定」とは?

関数が「型安定 (type-stable)」とは、出力の型が入力の型から分かることを言います。例えば型安定な関数では、入力のが変わっても出力の型が変わりません。明らかに型安定でない関数の例を示します:

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

この関数は引数の値に応じて Int または Float64 を返すので、Julia はコンパイル時に返り値の型を決定できません。この関数を使う任意の計算は二つの型を扱う必要があり、高速な機械語の生成が困難になります。

DomainError が不必要に思える箇所で発生するのはなぜ?

数学的に意味のある操作であってもエラーが発生する場合があります:

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

この不便な振る舞いは型安定性を優先させた結果として生じます。通常 sqrt(2.0) を使うユーザーが計算したいと思っているのは実数であり、複素数 1.4142135623730951 + 0.0im ではありません。sqrt を負の実数に対して複素数を返すようにもできますが、そうすると sqrt 関数が型安定でなくなり、性能が落ちてしまいます。

こういった場合には、所望の出力の型に合わせて入力の型を調整してください:

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

型パラメータを宣言・計算するには?

パラメトリック型のパラメータは型またはビット値 (isbitstypetrue を返す型の値) であり、型自身はパラメータを使って表現されます。例えば Array{Float64, 2} は要素型を表す Float64 と次元数を表す整数値 2 をパラメータに持ちます。

独自のパラメトリック型を定義するときは、<: を使って値のパラメータを何らかの抽象型または他の型パラメータの部分型に制限できます。しかし、あるパラメータが特定の型の値でなければならないことを宣言する構文は用意されていません ──例えば struct の定義で、次元を表す特定の型パラメータの値は Int 型の値でなければならないと指定することはできません。同様に、型パラメータに対して任意の計算 (加算や減算のような単純なものを含む) を行えません。その代わり、複数の型パラメータの間に制約や関係があるときは、ひとまず異なる型パラメータを使って型を宣言した上で、その型のコンストラクタで制約や関係を計算・確認するようにしてください。

例えば、次の型を書きたいとします:

struct ConstrainedType{T,N,N+1} # 注意: 正しくない
    A::Array{T,N}
    B::Array{T,N+1}
end

つまり、三つ目の型パラメータが二つ目の型パラメータに 1 を足した値であることを強制したいということです。二つの型パラメータを異なるものとして宣言した上で内部コンストラクタメソッドを使うとこれを実装できます (このメソッドでは他の確認も行えます):

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

この確認には通常コストがかかりません。正当な具象型に対しては確認のための条件文が定数式になるので、コンパイラが確認を省略できるからです。また二番目の引数 (B) が一番目の引数から (A) から計算できるなら、外部コンストラクタメソッドを使って計算を行うとよいでしょう:

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

Julia がマシンネイティブな整数算術を使うのはなぜ?

Julia は整数の計算でマシンネイティブな算術を使います。これは Int 型の値の範囲が有界であり、上限と下限を超す計算がラップアラウンドすることを意味します。つまり整数の加算・減算・乗算では正および負のオーバーフロが起こり、一見すると奇妙な結果が得られます:

julia> typemax(Int)
9223372036854775807

julia> ans+1
-9223372036854775808

julia> -ans
-9223372036854775808

julia> 2*ans
0

これが数学的な整数の振る舞いと異なることは明らかです。もしかすると、Julia のような高水準プログラミング言語がユーザーに対してこの性質を露出させるのは理想的ではないと思うかもしれません。しかし効率と透明性が求められる計算処理では、これが別の選択肢より優れています。

別の選択肢の一つとして、整数演算でオーバフローを毎回確認し、もしオーバフローが起これば Int128BigInt といった大きな整数型へと結果を昇格するというものがあります。しかし残念ながら、こうすると全て整数演算で大きなオーバーヘッドが生まれます (ループカウンターを進める処理を考えてみてください) ──生成されるコードに含まれる全て算術命令に実行時のオーバーフローチェックが挿入され、さらにオーバーフローが起きたときのための分岐も必要になります。

さらに悪いことに、こうすると整数が絡む全ての処理が型不安定となります。上述の通り型の安定性は効率的なコードを効率的に生成するために不可欠であり、整数演算の結果が整数になるという事実を利用できなければ、C や Fortran のコンパイラが生成するような高速で単純なコードは生成できません。

このアプローチを少し変えれば、見かけ上は型の不安定性を取り除けます: IntBigInt を単一のハイブリッドな整数型に合体させ、マシンネイティブな整数に収まらなくなったら内部で表現を切り替えるという方法です。たしかにこうすれば表面の Julia コードからは型の不安定性が無くなりますが、このハイブリッドな整数型を実装する C コードに元々の問題が押し付けられただけで、実際には何も解決していません。

このアプローチの実装は可能であり、もしかしたら多くの場合で非常に高速に動作させられるかもしれません。しかしそうだとしても、いくつか欠点があります。一つ目の問題は、整数と整数の配列のメモリ上の表現が C や Fortran といったマシンネイティブな整数を使う言語における自然な表現と合わないことです。つまりそういった言語との相互運用では、どのみちネイティブな整数をどこかで使う必要があります。また非有界な整数の表現は固定ビット幅を持てないので、固定サイズのスロットを持った配列に格納できません ──大きな整数値には必ずヒープにアロケートされたストレージが個別に必要となります。そしてもちろん、ハイブリッドな整数型の実装がどれだけ賢くても、性能に関する落とし穴 ──性能が予期しない形で落ちる状況── は必ず存在します。

C や Fortran と相互運用できない複雑な表現を持つこと、追加のヒープストレージ無しに整数配列を表現できないこと、そしてパフォーマンスの特性を予測できないこと、これらの問題により、ハイブリッドな整数型は実装をどれだけ賢く行ったとしても高性能な計算処理における良い選択肢にはなりません。

BigInt への昇格やハイブリッドな整数とは異なる選択肢として、「飽和整数算術 (saturating integer arithmetic)」を使う方法があります。これは整数の最大値への加算や最小値への減算で値を変化させない方法で、MATLAB が採用しています:

>> int64(9223372036854775807)
9223372036854775807

>> int64(9223372036854775807) + 1
9223372036854775807

>> int64(-9223372036854775808)
-9223372036854775808

>> int64(-9223372036854775808) - 1
-9223372036854775808

9223372036854775807 は -9223372036854775808 より正確な値 9223372036854775808 にずっと近く、さらに C や Fortran と互換性を持つ固定サイズの自然な方式で整数を表現できるので、一見この方法は悪くないように思えます。しかし飽和整数算術は根本的な問題を持ちます。一つ目の最も明らかな問題は、マシンの整数算術がこのように動作しないことです。生成されるコードに含まれる全ての算術命令にオーバーフローの確認を追加し、オーバーフローしていたら typemin(Int)typemax(Int) で適切に置き換えなければなりません。これだけでも整数演算を行うための単一の高速な命令が半ダースの命令に膨れ上がりますが、欠点はこれで終わりではありません ──飽和算術演算は結合性を持ちません。MATLAB における次の計算を考えてみてください:

>> n = int64(2)^62
4611686018427387904

>> n + (n - 1)
9223372036854775807

>> (n + n) - 1
9223372036854775806

このため基本的な整数アルゴリズムを書くのが難しくなります。整数アルゴリズムで使われるテクニックの多くはマシンの加算が結合性を持つことを仮定しているからです。例えば整数値 lohi の中点を見つける処理は、Julia なら (lo + hi) >>> 1 という式を使って書けます:

julia> n = 2^62
4611686018427387904

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

何の問題もありません。n + 2n-4611686018427387904 となりますが、それでも 2^622^63 の中点が正しく計算されています。MATLAB でこれを試してみましょう:

>> (n + 2*n)/2
  4611686018427387904

これは間違いです。n2n を足して飽和が起こった時点で中点を計算するのに必要な情報が失われるので、MATLAB に >>> 演算子を追加しても状況は改善されません。

結合性を持たないとこういったテクニックを使えない点で損をするだけではなく、コンパイラが整数算術に対して行う最適化のほぼ全てが不可能になる点でも損をします。例えば Julia の整数はマシンが持つ通常の整数算術なので、LLVM は f(k) = 5k-1 のような関数を積極的に最適化できます。この f に対する機械語は次のようになります:

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 は Integer overflow checking cost というブログ記事でオーバーフローの確認処理が持つコストを解析し、理論上は小さいこのコストがコンパイラによる最適化を妨げることで非常に大きくなることを発見しました。この状況が将来改善すれば、もしかしたら Julia は確認付きの整数算術をデフォルトにするかもしれません。しかし今は、オーバーフローの可能性を受け入れるしかありません。

オーバーフローが起きない整数演算は SaferIntegers.jl といった外部ライブラリで実現できます。ただし上述したように、こういったライブラリを使うと実行時間が大幅に伸びます。全ての整数演算ではなく一部でだけ使うのなら問題にはならないでしょう。確認付き整数算術に関する議論は #855 で確認できます。

リモートコールで起きた UndefVarError の原因は?

名前が示すように、UndefVarError が起きた直接の原因はリモートノードに指定された名前の束縛が存在しないことです。考えられるエラーの原因をいくつか説明します。

次の例ではクロージャ x->xFoo への参照を持つので、Foo が見えていないノード 2 でこのクロージャを使おうとした結果 UndefVarError が発生しています:

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

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

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 には gvar が存在しないので UndefVarError が発生します。

Main モジュールのグローバル変数ではこの問題は起こりません。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

Julia が文字列の連結に * を使うのはなぜ?

文字列の連結に + を使いたくない一番の理由は、通常 + が連結性を持つ演算子として使われるためです。文字列の連結は連結性を持ちません。他の言語では異なる演算子が使われており、一部のユーザーは * を使った文字列の連結に慣れていないことを Julia コミュニティは認識していますが、* は非連結性という代数的な性質を意味しています。

なお string(...) を使っても文字列の連結は可能です (文字列以外の値を文字列に変換することもできます)。同様に文字列の反復は ^ の他に repeat でも行えます。文字列の作成には補間も便利です。

[訳注: ここにパッケージとモジュールに関する FAQ が一つありましたが、古いバージョンに存在した仕様 (importall) を説明していたので翻訳では省略しました。]

無値と欠損値

「ヌル」「無値」「欠損値」は Julia でどうなっている?

C や Java を含む多くの言語と異なり、Julia のオブジェクトはデフォルト値として "ヌル" を持つことができません。初期化されていない参照 (変数・配列要素・オブジェクトのフィールド) にアクセスすると、すぐにエラーが発生します。この状況は isdefinedisassigned といった関数を使えば検出できます。

一部の関数は副作用のためだけに利用され、値を返しません。そのような関数からは nothing を返すのが慣習となっています。nothingNothing 型のシングルトンオブジェクトであり、Nothing 型はフィールドを持たない通常の型です。この慣習で使われることと REPL で出力されないこと以外に Nothing 型に関して特別な点はありません。また if false; end のような値を持たない構文も nothing を返します。

関数の引数・オブジェクトのフィールド・配列の要素が場合によっては存在しないことのある T 型の値なら、Union{T, Nothing} 型でそれを表現できます。これは他の言語で Nullable, Option, Maybe などと呼ばれる概念です。もし値自身が 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 へ置き換えられます。このため xy が配列のときは計算結果が x と同じメモリに書き込まれるのではなく、計算結果を格納する新しい配列のアロケートが起こります。

この振る舞いを見て驚く人もいるでしょうが、こうなっているのには理由があります。一番の理由は 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 という予想通りの結果が得られます。では行列に対してこの関数を適用すると何が起こるでしょうか? もし *= が左辺が指す配列の内容をインプレースに変更するようになっていると、二つの問題が発生します:

他の方法 (例えばループの明示的な利用) で達成できる種類の性能の最適化よりも汎用的なプログラミングをサポートする方が重要なので、+=*= といった演算子は新しい値を再束縛するようになっています。

非同期 IO と並行して行われる同期的な書き込み

同じストリームに対して並行に書き込むと出力が混ざるのはなぜ?

ストリーミング IO の API は同期的であっても、内部の実装は完全に非同期的なためです。

例として次のコードからの出力を考えます:

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

出力がこのようになるのは、write の呼び出しは同期的であるものの、一つの引数を書き込んで IO の完了を待っている間に他のタスクへ処理が移る (yield する) ためです。

printprintln は呼び出しの間ストリームをロックします。このため上のコードで writeprintln に変えると、出力の様子が変わります:

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.0A[] という構文を使います。全てのゼロ次元配列は同じ次元と長さを持ち、size(A) == ()length(A) == 1 が成り立ちます。特に、ゼロ次元配列は空になりません。この事実を納得できない人のために、Julia が採用した定義を理解するのに役立つ考え方を示します:

ゼロ次元配列とスカラーの違いを理解しておくことも重要です。スカラーはゼロ次元配列と同様に反復可能であり、length, getindex が定義され、1[] == 1 が成り立ちます。しかしスカラーは可変なコンテナではありません。例えば x = 0.0 と定義したスカラーに対して x[] = 1.0 で値を変更しようとするとエラーが発生します。スカラー x をゼロ次元配列に変換するには fill(x) を使い、逆にゼロ次元配列 a をスカラーに変換するには a[] を使います。またスカラーは 2 * rand(2,2) のように線形代数演算に利用できるのに対して、ゼロ次元配列を使った fill(2) * rand(2,2) はエラーとなる点も異なります。

Julia に対する線形代数演算のベンチマーク結果が他の言語と違うのはなぜ?

線形代数の基本的な演算に対する次のような簡単なベンチマークが、MATLAB や R などの言語と異なる結果となるかもしれません:

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

こういった演算は対応する BLAS 関数に対する非常に薄いラッパーなので、性能が変わる理由は高い確率で次のいずれかです:

Julia は OpenBLAS のコピーを独自にコンパイル・実行し、そのときスレッド数は 8 (あるいは CPU コア数) に制限されます。

OpenBLAS の設定を変更したり、異なる BLAS ライブラリ (例えば Intel MKL) を使って Julia をコンパイルすることで、性能を向上させられる可能性があります。OpenBLAS の代わりに Intel MKL BLAS と LAPACK を使うには、MKL.jl を利用するか、フォーラムを検索して出てくる方法を使って手動で設定してください。Intel MKL はオープンソースでないので、Julia に付属させることができません。

計算クラスター

分散ファイルシステムで事前コンパイルキャッシュを管理するには?

高性能計算施設で julia を使うときは、n 個の julia プロセスを起動すると事前コンパイルの結果であるキャッシュファイルの最大 n 個のコピーが一時的に作成されます。この振る舞いが (低速/小規模な分散ファイルシステムが原因で) 問題となるのであれば、次の回避策を試してください:

  1. コマンドライン引数 --compiled-modules=no を付けて julia を起動し、事前コンパイルを無効にする。
  2. julia プロセスごとにキャッシュファイルのプライベートな格納場所を設定する。格納場所を private_path にするには pushfirst!(DEPOT_PATH, private_path) を実行するか、環境変数 JULIA_DEPOT_PATH$private_path:$HOME/.julia に設定する。
  3. ~/.julia/compiled をスクラッチ領域を指すシンボリックリンクにする。

Julia リリース

Julia のバージョンは Stable, LTS, nightly のどれを使うべき?

大部分の人が使うべき Julia のバージョンは Stable です。これは Julia の最新リリースであり、改善された性能を含む最新の機能が付いています。Julia の Stable にはセマンティックバージョン番号が付いており、v1.x.y といった形で表されます。Julia の新しいマイナーリリースはだいたい 4-5 か月ごとにリリース候補バージョンに対する数週間のテスト期間を経て公開されます。LTS バージョンとは異なり Stable バージョンは次の Stable バージョンがリリースされるとバグ修正が行われなくなりますが、Julia v1.x のリリースは以前のバージョンに対して書かれたコードも実行できるので、Julia バージョンのアップグレードはいつでも可能です。

非常に安定したコードベースが必要なら、LTS (Long Term Support, 長期サポート) バージョンの Julia を使うべきかもしれません。現在の Julia LTS は v1.0.x というセマンティックバージョン番号が付いており、このブランチは次の LTS ブランチが選ばれるまでバグ修正を受け続けます。次の LTS ブランチが選ばれると v1.0.x 系列は定期的なバグ修正を受けなくなり、最も保守的なユーザーを除いた全てのユーザーに対して次の LTS バージョン系列への移行が推奨されます。あなたがパッケージ開発者なら、パッケージのユーザー数を最大化するために LTS バージョンを使って開発するという選択肢もあります。なおセマンティックバージョン番号の定義に従い、v1.0 に対して書かれた全てのコードは将来の LTS および Stable の v1.x.y バージョンで動作することが保証されています。そのため一般に、LTS をターゲットにしたコードは最新の Stable で実行でき、そうすれば性能が向上します。ただしそのとき新しい機能 (ライブラリ関数や新しいメソッド) は使えません。

「Julia 言語の最新の更新を利用したい。ちょうど今入手できるバージョンが壊れていることがたまにあっても構わない」と思うなら、nightly バージョンを使うべきです。名前の示す通り、nightly バージョンはほぼ毎夜作成されます (ビルドインフラの安定性に左右されます)。基本的にリリースされた nightly バージョンは使っても問題ありません ──コードを実行してもマシンが爆発することはないでしょう。ただし、リリース前に行われる徹底したテストで初めて発見されるリグレッションや問題が残っている可能性はあります。nightly バージョンに対してテストを実行すれば、あなたのコードに影響するリグレッションが無いかどうかをリリースの前に検出できます。

最後に、Julia をソースコードからビルドすることもできます。これはコマンドラインに慣れていて、Julia の内部構造を学びたい人のための選択肢です。あなたがこのような人なら、guidelines for contributing を読むとよいでしょう。

各バージョンの Julia のダウンロードは https://julialang.org/downloads/ から行えます。全てのバージョンが全てのプラットフォームで利用できるわけではないことに注意してください。


  1. 訳注: slurp は「(麺やスープを/ずるずると音を立てて/一気に)すする/かきこむ」という意味。[return]

  2. 訳注: splat は「濡れたもの (タオルや水風船) が床や壁にぶつかって周りに水分が飛び散る (ときのビシャっという音)」という意味。slurp/splat で「かきこむ/ぶちまける」という対になっているのだと思われる。[return]

広告