julia1.0.x での for ループと(グローバル) 変数の使い方。

julia が 1.0 になったとたんに、for ループ内の変数の扱い様式に変更が加えられました。julia 0.6 以前のプログラムが動かない結構な理由になっていると考えます。簡単に言うと、"global" という宣言を付けるべき所に付けるということで、かなり解決しそうです。自分では、漠然と関数内部の変数の扱いに似ているとは思っていましたが、for ループが動く時と、動かない時の区別を理解できていませんでした。

もう少し詳しく知るには、グローバル変数とローカル変数について勉強するのが良いようです。

今さらながら、@yatra9 さんの Julia 1.0の注意点 を良く読んでみたら理解できました。変数のスコープ Scope of Variables という事項の勉強が必要だったようです。将来的には、REPL 内では、特赦の対象となる可能性がある項目のようです。for ループ内で動くか動かないかには、グローバル変数と、ローカル変数というものの理解が必要なようです。ちなみに、julia のコードの高速化のほぼ筆頭に挙げられるのが、グローバル変数を避けるということです。(Juliaのコードを更に高速化する方法) 変数のスコープ Scope of Variables は、それほど英語に精通していなくても、例の部分は理解できるが多いと考えますので、目を通すと理解が深まります。また、@yatra9 さんの Julia 1.0の注意点 には、なんと6項目にまとめていただいています。ここでは、もう少し、手前から説明をしてみます。さらに、for ループと関数化の速度比較も行ってみます。

さて、グローバル変数は、julia を落すか、書き替えるまで、ずっと値を保持します。ローカル変数は、for ... end の中や、function ... end の中だけで働く変数です。普通に、"a=0" と REPL に入力すると、a は 0 という値を、書き替えられない限りずっと格納しています。つまり、a はグローバル変数として扱われています。a = "hoge" としたら、a はグローバル変数で、"hoge" という値を格納しています。このグローバル変数 a をそのまま for ループ内で使用することは可能です。ただし、a を書きかえることはできません。

julia> a = "hoge"
"hoge"

julia> for i in 1:3
         println(a)
       end
hoge
hoge
hoge

書き替えることはできませんが、a を演算に使うことも可能です。また、グローバル変数 A (::Array) を "for i in A ..." のような書き方をすることも可能です。

julia> a = "hoge"
"hoge"

julia> for i in 1:3
         println(a * string(i))
       end
hoge1
hoge2
hoge3

julia> A = ["hoge"*string(i) for i in 1:3]
3-element Array{String,1}:
 "hoge1"
 "hoge2"
 "hoge3"

julia> for i in A
           println(i)
       end
hoge1
hoge2
hoge3

では、「グローバル変数を、for ループ内で書き替えることができない」とは、どういうことか? と、いう例を示します。

julia> a = 0
0

julia> for i in 1:3
           a = a + 1
       end
ERROR: UndefVarError: a not defined
Stacktrace:
 [1] top-level scope at ./REPL[123]:2 [inlined]
 [2] top-level scope at ./none:0

エラー が出ました。for ループの中で、a が global 変数であることを宣言すると、書き替えてくれます。

julia> a = 0
0

julia> for i in 1:3
           global a = a + 1
       end

julia> a
3

このように global という宣言が必要になっています。ちょっとした for ループ内でも必要なのは、ちょっと面倒です。for ループ内での global 変数の宣言を理解できれば、julia 0.6 以前のコードの移植をする元気がだいぶ出てきます。

let ... end でも通りますが、let ... end の間の変数がローカル変数であることに配慮が必要です。

julia> a=0
0

julia> let
         a = 0
         for i in 1:10^5
           a = a + 1
         end
         a
       end
100000

julia> a
0

上の例のように let で計算した a は、グローバル変数の a を書き替えていません。

さて、julia といえば、速度が問題になることも少なくありません。自分の理解では、変数を替えるものは関数化して、1回だけ計算なら for ループで通れば良いと考えていました。for ループで書いても、こうも変数がグローバルか、ローカルかを考えないといけないなら、function ... end との距離は、それ程、違わないように感じます。for ループ にするか、関数化するかは、あとはパフォーマンス次第ということかと考えます。関数化した場合は、コンパイル後の 2回目以降の計算は、julia では圧倒的に速いです。

関数化と for ループ と、どちらが速いか、1例を示します。0 に 1 を 10 万回足す計算をしてみます。以下の2つの式の速度を測定してみましょう。

# for ループ
a = 0
@time for i in 1:10^5
  global a = a+1
end

# 関数化
function add_1()
  a=0
  for i in 1:10^5
    a=a+1
  end
  return a
end

では、まず for ループから測定してみます。

julia> a=0
0

julia> @time for i in 1:10^5
         global a = a+1
       end
  0.005627 seconds (99.49 k allocations: 1.518 MiB)

続いて、関数化した場合です。1回目はコンパイルの為に、時間が掛りますので、2回、計算してみます。

julia> function add_1()
         a=0
         for i in 1:10^5
           a=a+1
         end
         return a
       end
add_1 (generic function with 1 method)

julia> @time add_1()
  0.012268 seconds (45.12 k allocations: 2.549 MiB)
100000

julia> @time add_1()
  0.000002 seconds (5 allocations: 176 bytes)
100000

いったい、2回目以降の関数は何倍速いかは、計算機が必要です。

julia> 0.005627/0.000002
2813.5

なんと2回目以降では、単純な for ループと比較して 2000倍以上、高速化するようです。

1回目は for ループも優秀なので、ちょっと悩むところですね。

おしまい。

B! LINE