末尾再帰の最適化が使えるようになってた
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ 末尾再帰の最適化が最新 Chromium で使えます
◆ harmony オプションが必要です
◆ 末尾再帰の関数ならスタックサイズオーバーに悩まされることがなくなります
◆ 便利な反面 無限ループという別の問題も
◆ harmony オプションが必要です
◆ 末尾再帰の関数ならスタックサイズオーバーに悩まされることがなくなります
◆ 便利な反面 無限ループという別の問題も
ES6 の機能でちょっと特殊な末尾再帰の最適化 Chromium で使えるようになってました
最新の
Chrome 50.0.2649.0
V8 4.10.200
--js-flags=--harmony-tailcalls
で確認してます
ところで今の Chrome だと
Uncaught RangeError: Maximum call stack size exceeded
これまでの JavaScript だとよくこの スタックサイズオーバーなエラーになっていましたがこれからは再帰をどんどん書いていけるということですね
でもその分 再帰の条件ミスがあってもスタックサイズオーバーで止まってくれないので 無限ループになると考えるとやっかいなものともいえます
今のところは コンソールで入力したコードを開発者ツールが有効な状態で実行しても最適化されずエラーになります
ソースに書かれているコードは開発者ツールが開かれていても最適化が効いています
動作確認のためにコンソールで実行してもエラーになるのはちょっと不便です
コンソールを閉じているときに関数が実行されると大丈夫なので ボタンのイベントに設定したり setTimeout を設定して実行が始まるときは開発者ツールを閉じておくという手もあります
不便なのは変わらないですけどね
だそうです
なので全部の再帰関数が無限に再帰ができるってことではありません
末尾再帰というのは名前通り 末尾 つまり関数の最後で再帰関数を呼び出しているものです
上の OK な方は return のあとは直接文字列か再帰関数呼び出しです
下の OK じゃない方は 再帰関数の実行したあとに何か処理が入って return しています
普通の再帰関数の呼び出しはこんなのですよね
一番深い recur まで言ったら順に戻ってきて最初の呼び出し元の main まで戻ります
関数の呼び出し元に戻るための情報はこんなです
このときに再帰呼び出し後の上矢印のところで 何もすることなくそのまま上に戻るだけを繰り返すのだったら 再帰中のそれぞれの関数の内部の情報(ローカル変数とか)を保存しておく必要はなくなります
それなら 関数を呼び出すときに元々の呼び出し元の情報を渡しておけば
途中を通さず一気に戻れるというわけです
関数の戻り先の情報なんて普段 JavaScript 使ってて気にするようなところじゃないので分かりづらいですが 機械語 アセンブリ言語とかの低レイヤーな部分で関数呼び出しがどうなってるのかイメージすればわかるのかも
この図だと recur が呼び出した数だけ残っていますが いま実行中の recur 以外は消してしまっても良いので 実質無限に呼び出せるということです
もし 関数呼び出したあとで足し算したり何かの処理がある場合は全部の recur を保存しておかないとだめなので 最適化ができずに呼び出せる限界があります
作るものによっては 末尾再帰にできないときもあると思いますが できるなら末尾再帰にしてみると良いと思います
何重にも呼び出した関数を 1 つ 1 つ戻る必要がないので速度もちょっとは早くなってそうです
ただ 最初の方にも書いた条件ミスで無限ループっていうのが怖いのでそこは気をつけないとです
最新の
Chrome 50.0.2649.0
V8 4.10.200
--js-flags=--harmony-tailcalls
で確認してます
function recur(x, acc){
if(x <= 0) return acc
return recur(x - 1, acc + x)
}
recur(100, 0) // 5050
recur(1000000, 0) // 500000500000
if(x <= 0) return acc
return recur(x - 1, acc + x)
}
recur(100, 0) // 5050
recur(1000000, 0) // 500000500000
ところで今の Chrome だと
Uncaught RangeError: Maximum call stack size exceeded
これまでの JavaScript だとよくこの スタックサイズオーバーなエラーになっていましたがこれからは再帰をどんどん書いていけるということですね
でもその分 再帰の条件ミスがあってもスタックサイズオーバーで止まってくれないので 無限ループになると考えるとやっかいなものともいえます
今のところは コンソールで入力したコードを開発者ツールが有効な状態で実行しても最適化されずエラーになります
ソースに書かれているコードは開発者ツールが開かれていても最適化が効いています
動作確認のためにコンソールで実行してもエラーになるのはちょっと不便です
コンソールを閉じているときに関数が実行されると大丈夫なので ボタンのイベントに設定したり setTimeout を設定して実行が始まるときは開発者ツールを閉じておくという手もあります
不便なのは変わらないですけどね
そもそも末尾再帰って
今回の機能はあくまで 末尾再帰 の最適化だそうです
なので全部の再帰関数が無限に再帰ができるってことではありません
末尾再帰というのは名前通り 末尾 つまり関数の最後で再帰関数を呼び出しているものです
// OK
function recur(r){
if(r < 0) return "complete"
return recur(r - 1)
}
function recur(r){
if(r < 0) return "complete"
return recur(r - 1)
}
// NOT OK
function recur(r){
if(r < 0) return "complete"
var x = recur(r - 1)
return x + "!"
}
function recur(r){
if(r < 0) return "complete"
var x = recur(r - 1)
return x + "!"
}
上の OK な方は return のあとは直接文字列か再帰関数呼び出しです
下の OK じゃない方は 再帰関数の実行したあとに何か処理が入って return しています
普通の再帰関数の呼び出しはこんなのですよね
一番深い recur まで言ったら順に戻ってきて最初の呼び出し元の main まで戻ります
main
↓ ↑
recur
↓ ↑
recur
↓ ↑
recur
↓ ↑
recur
↓ ↑
recur
↓ ↑
recur
関数の呼び出し元に戻るための情報はこんなです
main
↓ ↑
recur (main の 10 行目に戻る)
↓ ↑
recur (recur の 2 行目に戻る)
↓ ↑
recur (recur の 2 行目に戻る)
↓ ↑
recur (main の 10 行目に戻る)
↓ ↑
recur (recur の 2 行目に戻る)
↓ ↑
recur (recur の 2 行目に戻る)
このときに再帰呼び出し後の上矢印のところで 何もすることなくそのまま上に戻るだけを繰り返すのだったら 再帰中のそれぞれの関数の内部の情報(ローカル変数とか)を保存しておく必要はなくなります
それなら 関数を呼び出すときに元々の呼び出し元の情報を渡しておけば
main
↓
recur (main の 10 行目に戻る)
↓
recur (main の 10 行目に戻る)
↓
recur (main の 10 行目に戻る)
↓
recur (main の 10 行目に戻る)
↓
recur (main の 10 行目に戻る)
↓
recur (main の 10 行目に戻る)
main
↓ ↑
recur |
↓ |
recur |
↓ |
recur (main の 10 行目に戻る)
↓ ↑
recur |
↓ |
recur |
↓ |
recur (main の 10 行目に戻る)
途中を通さず一気に戻れるというわけです
関数の戻り先の情報なんて普段 JavaScript 使ってて気にするようなところじゃないので分かりづらいですが 機械語 アセンブリ言語とかの低レイヤーな部分で関数呼び出しがどうなってるのかイメージすればわかるのかも
この図だと recur が呼び出した数だけ残っていますが いま実行中の recur 以外は消してしまっても良いので 実質無限に呼び出せるということです
もし 関数呼び出したあとで足し算したり何かの処理がある場合は全部の recur を保存しておかないとだめなので 最適化ができずに呼び出せる限界があります
作るものによっては 末尾再帰にできないときもあると思いますが できるなら末尾再帰にしてみると良いと思います
何重にも呼び出した関数を 1 つ 1 つ戻る必要がないので速度もちょっとは早くなってそうです
ただ 最初の方にも書いた条件ミスで無限ループっていうのが怖いのでそこは気をつけないとです