Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

⚫ @miyakawa_taku ⚫ JJUG幹事です ⚫ Salesforceで働いています ⚫ 奄美出身の力士が好きです☆ ⚫ オレオレJVM言語Kinkを作っています https://bitbucket.org/kink/kink 自己紹介 #ccc_a4b 2/48

Slide 3

Slide 3 text

本セッションの背景 #ccc_a4b 3/48

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

#ccc_a4b どの言語がサポートする? ⚫ フルスペックの末尾呼び出し最適化 ◼ Scheme ◼ OCaml ◼ Kink! ◼ Haskell...? ⚫ 部分的な末尾呼び出し最適化 ◼ Scala (自己末尾呼び出しのみ) ◼ Clojure (loop+recur) 10/48

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

まずは普通のCプログラム #ccc_a4b #include 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

Slide 14

Slide 14 text

#ccc_a4b while→再帰呼び出し すべての「whileループ」は 「再帰呼び出しを行う手続き」に変換できる ループ条件が 真のとき 再帰呼び出しする ループ条件が 偽のとき 再帰呼び出ししない 変換ルール 14/48

Slide 15

Slide 15 text

再帰呼び出しを行うプログラム #ccc_a4b #include 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

Slide 16

Slide 16 text

入力サイズが大きいと…… #ccc_a4b $ ./recur <『大菩薩峠』.txt 大菩薩峠 甲源一刀流の巻 中里介山 ... ... 編笠も取らず、用事をも言わず、小手招《こ てまね》きするので、巡礼の老爺は怖る怖る、 「はい、何ぞ御用でござりまするか」 小腰《こごし》をかがめて進み寄ると、 Segmentation fault (=スタックオーバーフロー) 16/48

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

#ccc_a4b なぜスタックオーバーフロー? ⚫ 手続きの呼び出しはスタックフレームを スタック領域に積む ◼ スタックフレーム: 手続き実行ごとの作業領域 ⚫ 呼び出し階層が深くなると スタックフレームが増える ⚫ スレッドに割り当てられたスタック領域の 範囲を超えるとスタックオーバーフロー 18/48

Slide 19

Slide 19 text

普通のCプログラム #ccc_a4b 19/48

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

スタックフレームの中身の例 #ccc_a4b ... ... ... ローカル変数2 ... ローカル変数1 ... ... [fp-24] 引数2 [fp-16] 引数1 [fp-8] 戻り先アドレス fp レジスタ sp レジスタ ※実際のレイアウトは「呼び出し規約」に依存 23/48

Slide 24

Slide 24 text

呼び出し・戻り処理の疑似機械語 #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

Slide 25

Slide 25 text

スタックの積まれ方: 時系列 #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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

入力サイズが大きくても…… #ccc_a4b $ kink cat.kn <『大菩薩峠』.txt ... ... 「ああ、金椎だ」 と言って、二人は遠のいて避けて通るようにし ましたけれども、避けなかったところで、相手 は気がつくはずもなかったのです。 「相変らず、イエス・キリストを信じている よ」 と田山白雲が言いますと、柳田平治が、 「ちぇッ、キリシタン!」 と、噛《か》んで吐き出すように言いました。 <終> 28/48

Slide 29

Slide 29 text

なぜスタックがあふれない? #ccc_a4b 29/48

Slide 30

Slide 30 text

#ccc_a4b なぜスタックがあふれない? ⚫ catの呼び出しは手続きの末尾での 呼び出し = 末尾呼び出し :cat <- {(:In :Out) In.read_byte.each{(:Byte) Out.write_byte(Byte) cat(In Out) } } ←末尾 ←非・末尾 30/48

Slide 31

Slide 31 text

#ccc_a4b なぜスタックがあふれない? ⚫ 末尾呼び出し最適化の対象であるため、 スタック領域を消費しない → スタックオーバーフローしない! 31/48

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

#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

Slide 35

Slide 35 text

#ccc_a4b スタックフレームの積み方 改 一気に戻るやりかた main cat#2 main cat#1 main main cat#3 main call call ret ret 呼び出し階層が深くなってもスタック消費は増えない =末尾呼び出し最適化 35/48

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

状態遷移をともなう処理が 素直に書ける #ccc_a4b 40/48

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

継続渡しスタイルの プログラムが直接実行できる #ccc_a4b 43/48

Slide 44

Slide 44 text

#ccc_a4b 継続渡しスタイル 継続渡しスタイル (CPS): ◼ 手続きの最後に実行する処理(継続)を、 引数として渡すプログラミングスタイル ◼ 継続の呼び出しは末尾呼び出しとなる ◼ Kinkのストリーム処理はCPSで 書かれている 44/48

Slide 45

Slide 45 text

#ccc_a4b CPS変換 ⚫ ふつうのプログラムをCPSに変換すると、 任意の場所の継続を陽に取り出す処理 (例: call/cc)が実現できる †1 ⚫ つまり、末尾呼び出し最適化をおこなう 処理系では、CPS変換されたSchemeのAST インタプリタが書ける 45/48

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

注釈 #ccc_a4b 47/48

Slide 48

Slide 48 text

#ccc_a4b †1 CPS変換 Martin Gasbichler, Michael Sperber “Final Shift for Call/cc: Direct Implementation of Shift and Reset”, 2002 48/48