入門: 末尾呼び出し最適化 /tail-call-elimination-intro

入門: 末尾呼び出し最適化 /tail-call-elimination-intro

This slide is distributed under CC-BY 4.0 license.
https://creativecommons.org/licenses/by/4.0/deed.ja

5e8c7a93f4cd63b62ced5dd347f1a8e0?s=128

Miyakawa Taku

May 18, 2019
Tweet

Transcript

  1. 入門: 末尾呼び出し最適化 2019-05-18 JJUG CCC 2019 Spring ハッシュタグ: #ccc_a4b #jjug_ccc

    宮川 拓
  2. ⚫ @miyakawa_taku ⚫ JJUG幹事です ⚫ Salesforceで働いています ⚫ 奄美出身の力士が好きです☆ ⚫ オレオレJVM言語Kinkを作っています

    https://bitbucket.org/kink/kink 自己紹介 #ccc_a4b 2/48
  3. 本セッションの背景 #ccc_a4b 3/48

  4. #ccc_a4b 背景(しいて言えば) ⚫ 継続をOpenJDKに導入する取り組みである Project Loomが現在進行中です ◼ 継続は、非同期IOとグリーンスレッド の組み合わせを実現するための 基盤として構想されています

    ⚫ 末尾呼び出しは継続の理解に直結します 4/48
  5. 演目 #ccc_a4b 末尾呼び出し最適化とは? どこまでも続くループ なぜスタックはあふれる? 末尾呼び出し最適化の仕組み うれしい予感 5/48

  6. 演目 #ccc_a4b 末尾呼び出し最適化とは? どこまでも続くループ なぜスタックはあふれる? 末尾呼び出し最適化の仕組み うれしい予感 6/48

  7. #ccc_a4b 末尾呼び出しとは? 末尾呼び出し: ◼ 手続きの末尾での手続き呼び出し 7/48

  8. #ccc_a4b 末尾呼び出し最適化とは? 末尾呼び出し最適化: ◼ 末尾呼び出しがスタックを消費しない よう、処理系がとりはからうこと ◼ 末尾呼び出しがいくつ連続しても、 スタックオーバーフローしないことが 保証される

    8/48
  9. #ccc_a4b いわゆる「最適化」ではない! ⚫ 処理を速くするためのものではない ⚫ オプショナルな機能ではない ⚫ 言語仕様レベルで規定されることが重要 ◼ ある種の処理の安全性を保証するため

    9/48
  10. #ccc_a4b どの言語がサポートする? ⚫ フルスペックの末尾呼び出し最適化 ◼ Scheme ◼ OCaml ◼ Kink!

    ◼ Haskell...? ⚫ 部分的な末尾呼び出し最適化 ◼ Scala (自己末尾呼び出しのみ) ◼ Clojure (loop+recur) 10/48
  11. 演目 #ccc_a4b 末尾呼び出し最適化とは? どこまでも続くループ なぜスタックはあふれる? 末尾呼び出し最適化の仕組み うれしい予感 11/48

  12. #ccc_a4b 題材とするプログラム ⚫ 標準入力から1バイトずつ読み込む ⚫ 標準出力にそのまま書き出す 12/48

  13. まずは普通のCプログラム #ccc_a4b #include <stdio.h> void cat(FILE *in, FILE *out) {

    while (1) { int byte = getc(in); if (byte < 0) { return; } putc(bt, out); } } int main(void) { cat(stdin, stdout); return 0; } 13/48
  14. #ccc_a4b while→再帰呼び出し すべての「whileループ」は 「再帰呼び出しを行う手続き」に変換できる ループ条件が 真のとき 再帰呼び出しする ループ条件が 偽のとき 再帰呼び出ししない

    変換ルール 14/48
  15. 再帰呼び出しを行うプログラム #ccc_a4b #include <stdio.h> void cat(FILE *in, FILE *out) {

    int byte = getc(in); if (byte >= 0) { putc(byte, out); cat(in, out); } } int main(void) { cat(stdin, stdout); return 0; } 15/48
  16. 入力サイズが大きいと…… #ccc_a4b $ ./recur <『大菩薩峠』.txt 大菩薩峠 甲源一刀流の巻 中里介山 ... ...

    編笠も取らず、用事をも言わず、小手招《こ てまね》きするので、巡礼の老爺は怖る怖る、 「はい、何ぞ御用でござりまするか」 小腰《こごし》をかがめて進み寄ると、 Segmentation fault (=スタックオーバーフロー) 16/48
  17. 演目 #ccc_a4b 末尾呼び出し最適化とは? どこまでも続くループ なぜスタックはあふれる? 末尾呼び出し最適化の仕組み うれしい予感 17/48

  18. #ccc_a4b なぜスタックオーバーフロー? ⚫ 手続きの呼び出しはスタックフレームを スタック領域に積む ◼ スタックフレーム: 手続き実行ごとの作業領域 ⚫ 呼び出し階層が深くなると

    スタックフレームが増える ⚫ スレッドに割り当てられたスタック領域の 範囲を超えるとスタックオーバーフロー 18/48
  19. 普通のCプログラム #ccc_a4b 19/48

  20. 再帰呼び出しを行うプログラム #ccc_a4b ... 20/48

  21. スタック #ccc_a4b mainの スタックフレーム catの スタックフレーム putcの スタックフレーム 21/48

  22. スタックオーバーフロー #ccc_a4b mainの スタックフレーム catの スタックフレーム catの スタックフレーム catの スタックフレーム

    catの スタックフレーム catの スタックフレーム 22/48
  23. スタックフレームの中身の例 #ccc_a4b ... ... ... ローカル変数2 ... ローカル変数1 ... ...

    [fp-24] 引数2 [fp-16] 引数1 [fp-8] 戻り先アドレス fp レジスタ sp レジスタ ※実際のレイアウトは「呼び出し規約」に依存 23/48
  24. 呼び出し・戻り処理の疑似機械語 #ccc_a4b 呼び出し側 呼び出され側 ... [sp-8] ← 戻り先 [sp-16] ←

    byte [sp-24] ← out sp ← sp-24 fp ← sp jump to putc 戻り先: fp ← fp+120 ... ... sp ← fp jump to [sp-8] 24/48
  25. スタックの積まれ方: 時系列 #ccc_a4b main cat#1 cat#2 cat#3 main cat#1 cat#2

    main cat#1 main main cat#1 cat#2 main cat#1 main call call call ret ret ret スタックの消費量は呼び出し階層の深さに比例 → 深くなりすぎるとスタックオーバーフロー 25/48
  26. 演目 #ccc_a4b 末尾呼び出し最適化とは? どこまでも続くループ なぜスタックはあふれる? 末尾呼び出し最適化の仕組み うれしい予感 26/48

  27. Kinkの再帰プログラム #ccc_a4b :cat <- {(:In :Out) In.read_byte.each{(:Byte) Out.write_byte(Byte) cat(In Out)

    } } :main <- { cat(stdin stdout) } 27/48
  28. 入力サイズが大きくても…… #ccc_a4b $ kink cat.kn <『大菩薩峠』.txt ... ... 「ああ、金椎だ」 と言って、二人は遠のいて避けて通るようにし

    ましたけれども、避けなかったところで、相手 は気がつくはずもなかったのです。 「相変らず、イエス・キリストを信じている よ」 と田山白雲が言いますと、柳田平治が、 「ちぇッ、キリシタン!」 と、噛《か》んで吐き出すように言いました。 <終> 28/48
  29. なぜスタックがあふれない? #ccc_a4b 29/48

  30. #ccc_a4b なぜスタックがあふれない? ⚫ catの呼び出しは手続きの末尾での 呼び出し = 末尾呼び出し :cat <- {(:In

    :Out) In.read_byte.each{(:Byte) Out.write_byte(Byte) cat(In Out) } } ←末尾 ←非・末尾 30/48
  31. #ccc_a4b なぜスタックがあふれない? ⚫ 末尾呼び出し最適化の対象であるため、 スタック領域を消費しない → スタックオーバーフローしない! 31/48

  32. 末尾呼び出しのシーケンス #ccc_a4b ここでやっていることは、 単に元の呼び出し元に戻るだけ 32/48

  33. 戻り方の変更 #ccc_a4b それなら、深い階層から 一気に戻っても話は同じ 33/48

  34. #ccc_a4b スタックフレームの積み方 再 一個ずつ戻るやりかた main cat#1 cat#2 cat#3 main cat#1

    cat#2 main cat#1 main main cat#1 cat#2 main cat#1 main call call call ret ret ret スタックの消費量は呼び出し階層の深さに比例 34/48
  35. #ccc_a4b スタックフレームの積み方 改 一気に戻るやりかた main cat#2 main cat#1 main main

    cat#3 main call call ret ret 呼び出し階層が深くなってもスタック消費は増えない =末尾呼び出し最適化 35/48
  36. 演目 #ccc_a4b 末尾呼び出し最適化とは? どこまでも続くループ なぜスタックはあふれる? 末尾呼び出し最適化の仕組み うれしい予感 36/48

  37. #ccc_a4b うれしい予感 ⚫ 再帰呼び出しで安全にループが書ける ⚫ 状態遷移をともなう処理が素直に書ける ⚫ 継続渡しスタイルのプログラムが 制約なく直接実行できる 37/48

  38. 再帰呼び出しで 安全にループが書ける #ccc_a4b 38/48

  39. #ccc_a4b 再帰によるループ Kinkのwhileは末尾呼び出しを用いた高階関数 として定義されている :while <- {(:cond :body) :loop <-

    { :Cond = cond Cond.if_true{ body loop } } loop } 39/48
  40. 状態遷移をともなう処理が 素直に書ける #ccc_a4b 40/48

  41. #ccc_a4b 《ルビ》を本文から取り除く 状態遷移図 本文 ルビ 「《」 「》」 「《」 「》」 入力終

    入力終 41/48
  42. #ccc_a4b 対応するコード 状態遷移は手続き呼び出しで直接表現できる 「《」 「》」 「《」 「》」 入力終 入力終 https://bitbucket.org/miyakawataku/2019-05-jjug-ccc-

    tailcall/src/default/noruby.kn 42/48
  43. 継続渡しスタイルの プログラムが直接実行できる #ccc_a4b 43/48

  44. #ccc_a4b 継続渡しスタイル 継続渡しスタイル (CPS): ◼ 手続きの最後に実行する処理(継続)を、 引数として渡すプログラミングスタイル ◼ 継続の呼び出しは末尾呼び出しとなる ◼

    Kinkのストリーム処理はCPSで 書かれている 44/48
  45. #ccc_a4b CPS変換 ⚫ ふつうのプログラムをCPSに変換すると、 任意の場所の継続を陽に取り出す処理 (例: call/cc)が実現できる †1 ⚫ つまり、末尾呼び出し最適化をおこなう

    処理系では、CPS変換されたSchemeのAST インタプリタが書ける 45/48
  46. 演目 #ccc_a4b 末尾呼び出し最適化とは? どこまでも続くループ なぜスタックはあふれる? 末尾呼び出し最適化の仕組み うれしい予感 46/48

  47. 注釈 #ccc_a4b 47/48

  48. #ccc_a4b †1 CPS変換 Martin Gasbichler, Michael Sperber “Final Shift for

    Call/cc: Direct Implementation of Shift and Reset”, 2002 48/48