L2 Lisp 7.3 と Ruby 1.9.0 の再評価

2008.7.4 (鈴)

1. はじめに

L2 Lisp 7.3 は, Ruby 用 L2 Lisp 7.0 を Python 用 L2 Lisp 7.2 に準じて以下の点で改訂したものである。

また,独自の改良として,Ruby 言語との相互作用のために用意した ruby-send, ruby-send-apply 両関数で send メソッドにかえて __send__ メソッドを使うように改めた。 ruby 1.8.2, 1.8.5, 1.8.6, 1.8.7, 1.9.0jruby 1.1.2 で動作を確認した。

本稿は,まず,L2 Lisp 7.3 の主要な改訂点である対話セッションでの行編集機能を Ruby でどのように実現したか記述する。

「より実用的な L2Lisp.rb」§5「L2 Lisp の Python 2.5 & 3.0 への移植」§4 に見るように ruby 1.9.0-1 は従来の ruby に比べ L2 Lisp の実行速度が劣っていた。 しかし,ruby 1.9.0-2 が発表されたのを機に再評価したところ,意外な事実が判明した。 本稿は,次に,この評価とその結果について述べる。

2. 行編集機能の実現

irb などに使われている Ruby の履歴付き行編集機能は,readline で提供されている。 require 'readline' をした後,STDIN.gets の代わりに (戻り値の文字列から改行文字が省かれることに注意して) Readline.readline(prompt, true) を呼び出せばよい。 L2 Lisp 7.3 では,gets メソッドが呼び出されると内部で Readline.readline を実行するような疑似ファイル・クラス InteractiveInput を定義し, そのインスタンスで STDIN を代替することにより,この機能を利用する。

ただし,Mac OS X 10.4 標準添付の ruby 1.8.2 のように,readline がない場合もある。 その場合は,内部で STDIN.gets を実行するように疑似ファイル・クラスを定義する。 下記に実際のコードを示す。

  begin
    require 'readline'
  rescue LoadError
    # ロード失敗 → 行編集機能なしのクラス定義
    class InteractiveInput < InteractiveInputBase
      def gets        # 初回は1次,以降2次プロンプトで1行を入力する
        if @primary
          STDOUT.print @ps1
          @primary = false
        else
          STDOUT.print @ps2
        end
        STDOUT.flush
        @lineno += 1
        return STDIN.gets
      end
    end # InteractiveInput
  else
    # ロード成功 → 行編集機能ありのクラス定義
    class InteractiveInput < InteractiveInputBase
      def gets
        if @primary
          prompt = @ps1
          @primary = false
        else
          prompt = @ps2
        end
        @lineno += 1
        s = Readline.readline(prompt, true)
        return (s.nil?) ? nil : s + "\n"
      end
    end # InteractiveInput
  end

ここで InteractiveInputBase の定義は次のとおりである。

  # 場合分けの共通部分をになう基底クラス
  class InteractiveInputBase
    attr :lineno                # 現在の行番号

    def initialize(ps1, ps2)    # 引数は1次プロンプトと2次プロンプト
      @ps1 = ps1
      @ps2 = ps2
      @primary = true
      @lineno = 0
    end

    def reset                   # プロンプトを1次に戻す
      @primary = true
    end

    def close                   # 何もしない
    end
  end # InteractiveInputBase
上記の三つの class 文は一つにまとめることもできます。class 文も,その中の def 文も実行文です。

Interp#run メソッドは,InteractiveInput クラスを使って次のように定義される。 ここでは,今回の改訂点の一つである rescue Interrupt 節による Control-C 打鍵の捕捉にも注目されたい。

    # IO または String から式の並びを読んで評価する。
    # 無引数ならば対話的に入力/評価/出力を繰り返す。
    def run(rf=nil)
      if interactive = rf.nil?
        rf = InteractiveInput.new('> ', '  ')
      elsif String === rf
        rf = StringIO.new(rf)
      end
      rr = Reader.new(rf)
      result = nil
      loop {
        begin
          x = rr.read
          if x == S_EOF
            puts 'Goodbye' if interactive
            return result
          end
          result = eval(x)
          puts LL.str(result) if interactive
        rescue Interrupt => ex # (典型的には) Control-C が打鍵されたとき
          raise unless interactive
          puts "\n" + ex.inspect
        rescue EvalError => ex
          raise unless interactive
          print ex
        end
      }
    end

ただし,疑似ファイル・クラス InteractiveInput による抽象化は完全ではない。 2次プロンプトから1次プロンプトに戻すため,Lisp 式を読み取る Reader#read メソッドで陽に InteractiveInput#reset を呼び出す必要がある。

    def read
      begin
        _read_token
        return _parse_expression
      rescue SyntaxError => ex
        @buf.clear    # その行の残りのトークンを捨てて次回の回復を図る
        raise EvalError, 'SyntaxError: %s -- %d: %p' % [ex, @rf.lineno, @line]
      ensure
        @rf.reset if InteractiveInput === @rf
      end
    end

3. Ruby 1.9.0 の再評価: ruby 1.9.0-2 の高速性

今回の改訂に先立ち ruby 1.9.0-2 が発表された。そこで Mac OS X 10.4.11 (MacBook Pro, Core Duo 2GHz, 1GB メモリ) 上で各バージョンの ruby を改めて構築し,その L2 Lisp 7.3 での速度を比較した。 インストール先を切り分けるために ./configure--prefix= を指定した以外,構築パラメタはすべてデフォルトのままである。 起動時間を含めた実行時間をそれぞれ 3 度 time で計り,最短値を採った。
Ruby farmer-and-….l tak.l 8queens.l
ruby 1.8.5-p231 0.272 [s] 8.172 [s] 21.681 [s]
ruby 1.8.6-p230 0.270 [s] 8.306 [s] 21.944 [s]
ruby 1.8.7-p22 0.278 [s] 8.413 [s] 22.199 [s]
ruby 1.9.0-0 0.237 [s] 4.110 [s] 10.782 [s]
ruby 1.9.0-1 0.358 [s] 13.842 [s] 36.801 [s]
ruby 1.9.0-2 0.229 [s] 4.040 [s] 10.458 [s]

これから分かるように ruby 1.9.0-2 が最も高速であり,ruby 1.9.0-0 がこれに次ぐ。 実行時間が極度に短い (したがって起動時間を無視できない) farmer-and-wolf-etc1.l のテストを除けば,両者は ruby 1.8 シリーズに比べ約2倍の速度である。

一方,ruby 1.9.0-1 は,今回評価した中では,あらゆる点で最低速である。

ちなみに,psyco 1.6 をもった python 2.5.2 で Python 用 L2 Lisp 7.2 の実行時間を同様に計ったところ,その結果は順に 0.191秒, 2.688秒, 7.505秒 でした。 ruby 1.9.0-2 を超える高速度です。しかし,その比は 1.5 以下にすぎません。 PyPy の完成が待ち望まれます。

4. おわりに

Ruby 用 L2 Lisp の対話入力に行編集機能を与えるとともに,Control-C の打鍵で対話セッションが中断しないようにした。 これは Lisp 言語そのものの強化ではないが,処理系の日常的な使いやすさを改善する。

また,この L2 Lisp の実行時間で計った,つまり,小さなベンチマーク・テストではなく,より現実的な規模のプログラムで見た ruby 1.9.0-01.9.0-2高速性を確認した。 「より実用的な L2Lisp.rb」§5「L2 Lisp の Python 2.5 & 3.0 への移植」§4 に見られた ruby 1.9.0-1 の悲惨な低速さは,ruby 1.9 シリーズ全般ではなく,今のところ,その版だけの現象である。


次回へつづく


Copyright (c) 2008 OKI Software Co., Ltd.