読者です 読者をやめる 読者になる 読者になる

圧倒亭グランパのブログ

圧倒的おじいちゃんを目指して

【ruby】キーワード引数のメソッド呼び出しは遅い!しかし2.2.0-preview2 以降で劇的に改善されていた話

ruby

Rubyのしくみ」を読んでいたら、「キーワード引数って遅いんじゃないか?」と思ったので調べてみました。

まずは結果から。

rubyの各バージョンで、キーワード引数がどのくらい遅いのかを調べてみました。

調査コードは以下です。

Makefile
VERSIONS= \
        2.0.0-p0 \
        2.1.0 \
        2.2.0-preview1 \
        2.2.0-preview2 \
        2.2.0 \
        2.3.0

run: $(VERSIONS)

$(VERSIONS):
    @rbenv global $@
    @rbenv rehash
    @echo ""
    @echo "==============================================================="
    @ruby -v
    @echo "---------------------------------------------------------------"
    @ruby keyword.rb
    @echo ""
keyword.rb
require 'benchmark'

def no_arg
  2 + 3
end

def normal_arg(a, b)
  a + b
end

def keyword_arg(a: 1, b: 1)
  a + b
end

n = 10000000
result = Benchmark.bm do |b|
  b.report("no_arg     ") { n.times { x = no_arg                  } }
  b.report("normal_arg ") { n.times { x = normal_arg(2, 3)        } }
  b.report("keyword_arg") { n.times { x = keyword_arg(a: 2, b: 3) } }
end

結果は以下。

$ make run

===============================================================
ruby 2.0.0p0 (2013-02-24 revision 39474) [x86_64-darwin14.5.0]
---------------------------------------------------------------
             user       system     total    real
no_arg       0.640000   0.010000   0.650000 (  0.645543)
normal_arg   0.690000   0.000000   0.690000 (  0.691584)
keyword_arg  8.530000   0.080000   8.610000 (  8.618338)


===============================================================
ruby 2.1.0p0 (2013-12-25 revision 44422) [x86_64-darwin14.0]
---------------------------------------------------------------
             user       system     total    real
no_arg       0.670000   0.000000   0.670000 (  0.669037)
normal_arg   0.720000   0.000000   0.720000 (  0.725388)
keyword_arg  9.940000   0.080000  10.020000 ( 10.024457)


===============================================================
ruby 2.2.0preview1 (2014-09-17 trunk 47616) [x86_64-darwin14]
---------------------------------------------------------------
             user       system     total    real
no_arg       0.660000   0.000000   0.660000 (  0.664598)
normal_arg   0.680000   0.000000   0.680000 (  0.685753)
keyword_arg 10.270000   0.080000  10.350000 ( 10.347111)


===============================================================
ruby 2.2.0preview2 (2014-11-28 trunk 48628) [x86_64-darwin14]
---------------------------------------------------------------
             user       system     total    real
no_arg       0.690000   0.000000   0.690000 (  0.692030)
normal_arg   0.790000   0.000000   0.790000 (  0.783927)
keyword_arg  1.060000   0.000000   1.060000 (  1.068646)


===============================================================
ruby 2.2.0p0 (2014-12-25 revision 49005) [x86_64-darwin14]
---------------------------------------------------------------
             user       system     total    real
no_arg       0.670000   0.010000   0.680000 (  0.663958)
normal_arg   0.680000   0.000000   0.680000 (  0.686735)
keyword_arg  0.940000   0.000000   0.940000 (  0.937196)


===============================================================
ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin14]
---------------------------------------------------------------
             user       system     total    real
no_arg       0.570000   0.000000   0.570000 (  0.578201)
normal_arg   0.600000   0.000000   0.600000 (  0.596595)
keyword_arg  0.980000   0.000000   0.980000 (  0.982302)

相変わらず「引数なし」「通常引数」に比べて遅いものの、

2.2.0-preview1 → 2.2.0-preview2 にかけて10倍近く速度が改善されています。

2.2.0-preview1以前をお使いの方。

改善の余地あるかもしれません。

 

何が起こっているのか

なんでこんなに速度改善されているのでしょうか。

実は以下のようになっているので、速度に差があるみたいです。

  • 2.2.0-preview1 以前
    • キーワード引数の変数と値のバインドに Hash を使っている
  • 2.2.0-preview2 以降
    • rubyのコアコードの改善により、通常引数のように最適化された処理で引数を扱っている

検証してみます。

 

本当にHashを使っているか

2.2.0-preview1 で試してみましょう。

これは「Rubyのしくみ」でも行っていた検証です。

hash.rb
# Hash#key? を書き換える
class Hash
  def key?(val)
    puts "Looking for key #{val}"
    false
  end
end

def keyword(a: 1, b: 1)
  a + b
end

puts keyword(a: 2, b: 3)
$ ruby -v
ruby 2.2.0preview1 (2014-09-17 trunk 47616) [x86_64-darwin14]
$ ruby hash.rb
Looking for key a
Looking for key b
2

Hash#key? を各引数で呼び出していることがわかります。

かつ、強制的にfalseを返し「引数が見つからない」と応答しているので、デフォルト引数が使われていることも分かります。

では 2.2.0-preview2 で試してみましょう。

$ rbenv global 2.2.0-preview2
$ ruby -v
ruby 2.2.0preview2 (2014-11-28 trunk 48628) [x86_64-darwin14]
$ ruby hash.rb
5

今度は Hash#key? を使っていません。

かつ、与えた引数がちゃんと使われています。

以上の検証より、

  • 2.2.0-preview1では、実際にHashが使われている
  • 2.2.0-preview2では、Hashは使われていない

ということがわかりました。

 

通常引数と同じようにキーワード引数が扱われているか

メソッド呼び出しでどのような処理が行われているかは、rubyVMである YARV (Yet Another Ruby VM) の命令を見ると少しわかります。

以下のコードで検証します。

yarv.rb
code = <<-EOS
def keyword(a: 1, b: 1)
  a + b
end
puts keyword(a: 2, b: 3)
EOS

puts RubyVM::InstructionSequence.compile(code).disasm

2.2.0-preview1で実行してみます。

$ rbenv global 2.2.0-preview1
$ ruby -v
ruby 2.2.0preview1 (2014-09-17 trunk 47616) [x86_64-darwin14]
$ ruby yarv.rb
-- snip --
== disasm: <RubyVM::InstructionSequence:keyword@<compiled>>=============
local table (size: 4, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, keyword: 2@2] s0)
[ 4] a          [ 3] b          [ 2] ?
0000 getlocal_OP__WC__0 2                                             (   1)
0002 dup
0003 putobject        :a
0005 opt_send_simple  <callinfo!mid:key?, argc:1, ARGS_SKIP>  # (1)
0007 branchunless     18
0009 dup
0010 putobject        :a
0012 opt_send_simple  <callinfo!mid:delete, argc:1, ARGS_SKIP>  # (2)
0014 setlocal_OP__WC__0 4
0016 jump             21
0018 putobject_OP_INT2FIX_O_1_C_
0019 setlocal_OP__WC__0 4
0021 dup
0022 putobject        :b
0024 opt_send_simple  <callinfo!mid:key?, argc:1, ARGS_SKIP>  # (1)
0026 branchunless     37
0028 dup
0029 putobject        :b
0031 opt_send_simple  <callinfo!mid:delete, argc:1, ARGS_SKIP>  # (2)
0033 setlocal_OP__WC__0 3
0035 jump             40
0037 putobject_OP_INT2FIX_O_1_C_
0038 setlocal_OP__WC__0 3
0040 pop
-- snip --

(1)(2)でHashのメソッドを呼び出しています。

Hash#key? だけでなく Hash#delete も呼んでいたんですね。

次に 2.2.0-preview2 です。

$ rbenv global 2.2.0-preview2
$ ruby -v
ruby 2.2.0preview2 (2014-11-28 trunk 48628) [x86_64-darwin14]
$ ruby yarv.rb
-- snip --
== disasm: <RubyVM::InstructionSequence:keyword@<compiled>>=============
local table (size: 4, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: 2@0, kwrest: -1])
[ 4] a          [ 3] b          [ 2] ?
0000 trace            8                                               (   1)
0002 trace            1                                               (   2)
0004 getlocal_OP__WC__0 4
0006 getlocal_OP__WC__0 3
0008 opt_plus         <callinfo!mid:+, argc:1, ARGS_SIMPLE>
0010 trace            16                                              (   3)
0012 leave                                                            (   2)

こちらは Hashメソッドが見当たりません。

では、「通常引数のメソッド呼び出し」と比べてみましょう。

yarv_normal.rb
code = <<-EOS
def normal(a, b)
  a + b
end
puts normal(2, 3)
EOS

puts RubyVM::InstructionSequence.compile(code).disasm

通常引数の場合のYARV命令列を見てみます。

$ rbenv global 2.2.0-preview2
$ ruby -v
ruby 2.2.0preview2 (2014-11-28 trunk 48628) [x86_64-darwin14]
$ ruby yarv_normal.rb
-- snip --
== disasm: <RubyVM::InstructionSequence:normal@<compiled>>==============
local table (size: 3, argc: 2 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 3] a<Arg>     [ 2] b<Arg>
0000 trace            8                                               (   1)
0002 trace            1                                               (   2)
0004 getlocal_OP__WC__0 3
0006 getlocal_OP__WC__0 2
0008 opt_plus         <callinfo!mid:+, argc:1, ARGS_SIMPLE>
0010 trace            16                                              (   3)
0012 leave                                                            (   2)

ところどころ数値は違いますが、先ほどの2.2.0-preview2のキーワード引数の場合と似ていますね。

つまり、2.2.0-preview2以降では、キーワード引数の扱いは通常引数と同じように最適化されているということになります。

 

まとめ

  • キーワード引数呼び出しは、引数なしや通常引数のメソッド呼び出しよりも遅い
  • キーワード引数呼び出しは、2.2.0-preview1以前は相当遅かった
  • キーワード引数呼び出しは、2.2.0-preview2以降、2.2.0-preview1以前よりも10倍程度速い

まだまだ YARVの中身は理解できていませんが、rubyの内部処理に触れたことでこのような疑問を持つことになりましたし、検証方法も学べるので良いですね。

Rubyのしくみ」オススメです。(結局最後はこれ

【ruby】 メソッド探索から見る、モジュール・特異メソッド・特異クラス

ruby

rubyを書き始めて間もない頃、

 「なんで NoMethodError なんだ...。あ、メソッド定義にself 付けたら通った。」

みたいなことがありました。

rubyの本を読んでいると、そのあたりがハッキリとイメージできるようになったのでまとめておきます。

参考にした本

年明けからひたすらRuby本を読んでいます。読了したのは以下。

現在は Effective Ruby を読んでいます。

これらを読んでいくと、中途半端に理解していた部分がカチッとハマるのでオススメです。

※ 今回のコードは ruby 2.2.0 で試したものです。

 

オブジェクトとクラスの関係

サンプルコードを見てみましょう。

class C
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new

当初、自分はオブジェクトとクラスの関係を以下のように考えていました。
(図は本を参考にした独自のものです。クラス図とは関係ありません。)

f:id:at_grandpa:20160201020553p:plain:w300

  • クラスCは雛形
  • 雛形に合わせてobjオブジェクトが生成される

しかし、ruby本を読んでいると、実際はこうでした。

f:id:at_grandpa:20160201020600p:plain:w300

  • オブジェクトは インスタンス変数クラスへの参照 を持つ
  • クラスは インスタンスメソッド を持つ

一番驚いたのは、「オブジェクトの中にはメソッドが無い」ということ。

これはどういうカラクリかというと、objのメソッド呼び出しの時にわかります。

 

メソッド探索

サンプルコードでメソッドを呼び出してみましょう。

class C
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new
obj.c_instance_method # ここでインスタンスメソッドを呼び出した

この時、何が起きるかというと、rubyは以下のようにメソッドを探索します。

f:id:at_grandpa:20160201022623p:plain:w700

  • 参照しているクラスにメソッドを探索しにいく
  • 見つかったら実行

objを2つ生成した場合はどうでしょうか。

class C
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new
obj.c_instance_method

obj2 = C.new           # 別のオブジェクトを生成
obj2.c_instance_method # インスタンスメソッドを呼び出す

以下のようになります。

f:id:at_grandpa:20160201023219p:plain:w700

では、存在しないメソッドを呼び出した場合はどうなるのでしょうか。

class C
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new
obj.missing_method # 存在しないメソッドを呼び出した

rubyは、メソッドが見つからなければsuperclassをどんどん遡って探索します。

f:id:at_grandpa:20160201030738p:plain:w700

  • メソッドが見つからなければ、superclassをどんどん遡って探索
  • メソッドが見つかれば、そのメソッドを実行する
  • BasicObjectまで探しても見つからなければ NoMethodError

では、今度はクラスを継承してみましょうか。

class C
  def c_instance_method
    @my_var = 1
  end
end

class D < C; end # Cを継承したDクラスを定義

obj = D.new           # Dクラスのオブジェクトを生成
obj.c_instance_method # インスタンスメソッドを呼び出す

探索は以下のようになります。

f:id:at_grandpa:20160201030756p:plain:w600

  • はじめに探索するのは、オブジェクトの参照しているクラス(今回はDクラス)
  • そこで見つからなければsuperclassを探索する
  • 探索でメソッドが見つかれば、そこで実行する

例えばこの時、DクラスにCクラスのメソッドと同じ名前の「c_instance_method」が存在すると、探索の結果、Dクラスのメソッドが呼び出されます。これはお馴染みの「オーバーライド」です。

 

これで、メソッド探索の基本はOKですね。

次はもう一歩踏み込みます。

 

モジュール

rubyにはクラスの他にモジュールがあります。

モジュールもメソッド探索に影響を与えます。

include

モジュールをincludeした場合を見ていきます。

module M
  def m_method
    "m_method"
  end
end

class C
  include M  # モジュールをinclude
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new
obj.m_method # モジュールのメソッドを呼び出す

探索は以下のようになります。

f:id:at_grandpa:20160202145825p:plain:w600

  • includeしたモジュールは、includeしたクラスのすぐ一つ上に挿入される
  • superclassの探索の前にモジュールが探索される

もし、モジュールにも無いメソッドが呼び出された場合は、モジュールの次にsuperclass(上の図では Objectクラス)を探索します。

では2つのモジュールをincludeしたらどうなるでしょうか。

module M1
  def m1_method
    "m1_method"
  end
end

module M2
  def m2_method
    "m2_method"
  end
end

class C
  include M1
  include M2
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new
obj.m1_method # M1のメソッドを呼ぶ

探索は以下のようになります。

f:id:at_grandpa:20160203002548p:plain:w600

  • 後にincludeしたモジュールが先に探索される
  • つまり、後にincludeしたモジュールでオーバーライドできる

イメージが湧きましたでしょうか。

次は、もう一つモジュールを取り組む方法である prepend です。

prepend

モジュールの取り込み方法には prepend もあります。

module M
  def m_method
    "m_method"
  end
end

class C
  prepend M  # モジュールをprepend
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new
obj.m_method # モジュールのメソッドを呼び出す

探索は以下のようになります。

f:id:at_grandpa:20160203092729p:plain:w600

  • prependしたモジュールは、prependしたクラスのすぐ一つ下に挿入される

図より、prependでメソッドのオーバーライドが可能です。

では、2つのモジュールをprependしたらどうなるでしょうか。

module M1
  def m1_method
    "m1_method"
  end
end

module M2
  def m2_method
    "m2_method"
  end
end

class C
  prepend M1
  prepend M2
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new
obj.m1_method # M1のメソッドを呼ぶ

探索は以下のようになります。

f:id:at_grandpa:20160203093017p:plain:w600

  • 後にprependしたモジュールが先に探索される

includeと同じで、あとからprependしたモジュールが優先され、それでオーバーライドができます。

では、さらに一歩踏み込んで、特異メソッド・特異クラスにいきましょう。

 

特異メソッドと特異クラス

当初の自分の理解では、「特異メソッドとは、特定のオブジェクトにのみ追加したメソッドである」というものだけでした。

メソッドはクラスに属している」のならば、特異メソッドはどのクラスに属しているのか、疑問ですよね。

「オブジェクトの参照しているクラスに追加される」のであれば、他のオブジェクトからも呼び出せてしまうのでこれは違います。

いったいどこに特異メソッドがあるのか...。

このからくりを解き明かしていきます。

class C
  def c_instance_method
    @my_var = 1
  end
end

obj = C.new

def obj.obj_singleton_method  # 特異メソッドを定義
  "obj_singleton_method"
end

obj.obj_singleton_method # 特異メソッドを呼ぶ

探索は以下のようになります。

f:id:at_grandpa:20160204095424p:plain:w600

  • 実は、各オブジェクトは必ず一つの特異クラスを持っている
  • 実は、メソッド探索では、特異クラスを一番最初に探索していた
    • 今までは特異クラスにメソッドが見つからなかったので素通りしていた
  • 特異メソッドは特異クラスに属する
    • obj.singleton_class.instance_methods(false) #=> [:obj_singleton_method]
  • objの特異クラスのsuperclassは、オブジェクトの参照しているクラスである
    • obj.singleton_class.superclass #=> C
    • つまり、特異クラスにメソッドが見つからなければ、次の探索対象はオブジェクトの参照クラスである
  • しかし、objの参照クラスはCクラスであることに注意
    • obj.class #=> C
    • これはclassメソッドの仕様。特異クラスは無視する。

ちょっと複雑かもしれませんが、

  • オブジェクトは必ず一つの特異クラスを持っている
  • メソッド探索の第一歩目は特異クラスである
  • 特異メソッドは特異クラスに属する

の3点を抑えれば良いかなと思います。

 

さて、これを踏まえて、最終章「クラスメソッド」に行きます。

 

クラスメソッド

クラスメソッドは、クラスから参照できるメソッドであり、クラスからnewしたオブジェクトからは参照できません。

class C
  def c_instance_method; end    # インスタンスメソッド
  def self.c_class_method; end  # クラスメソッド
end

obj = C.new

obj.c_instance_method  # => ① nil (メソッドが存在)
obj.c_class_method     # => ② NoMethodError
C.c_instance_method    # => ③ NoMethodError
C.c_class_method       # => ④ nil (メソッドが存在)

ここで、「rubyではクラスもオブジェクトである」ということに着目すると、

 クラスメソッドは、そのクラスの特異メソッドである

ということが言えます。

つまり、クラスメソッドはどこにいるかというと、下図のように「クラスの特異クラス」にいるのです。

f:id:at_grandpa:20160214082739p:plain:w760

これを頭に入れた上で、上記の4パターンの探索を見てみましょう。

 

① obj.c_instance_method # => nilメソッドが存在)

f:id:at_grandpa:20160214082908p:plain:w760

  • レシーバは obj
  • objの特異クラスである #obj を探索の第一歩とする
  • 見つからないので superclass を探索
  • 見つかったのでメソッドを実行

 

② obj.c_class_method # => NoMethodError

f:id:at_grandpa:20160214083011p:plain:w760

  • レシーバは obj
  • objの特異クラスである #obj を探索の第一歩とする
  • 見つからないので superclass を探索
  • BasicObject まで探しても見つからないので NoMethodError

 

③ C.c_instance_method # => NoMethodError

この場合、一気に複雑になります。図と箇条書きの説明をひとつずつ照らしあわせてください。

f:id:at_grandpa:20160214080300p:plain:w780

  • レシーバは C
  • Cの特異メソッドである #C を探索の第一歩とする
  • 見つからないので superclass を探索
  • #C の superclass は、「C の superclass である Object」の特異クラス #Object である
  • その要領で #BasicObject まで探索する
  • 特異クラス #BasicObject の superclass は Classクラス
  • 引き続き Class → Module → Object → BasicObject と探索する
  • BasicObject まで探しても見つからないので NoMethodError

ここで、気をつける点があります。図を見ながら追ってみてください。

  • 一般的なクラス(Classクラス のオブジェクト)の「特異クラスのsuperclass」は そのクラスのsuperclassの特異クラス である
    • C.singleton_class.superclass # => #<Class:Object>
    • C.superclass.singleton_class # => #<Class:Object>
    • 同じ!
  • 一般的なオブジェクト(任意のクラスからnewされたもの)の「特異クラスのsuperclass」は そのオブジェクトの参照するクラス である
    • obj.singleton_class.superclass # => C
    • obj.class # => C
    • 同じ!

 

④ C.c_class_method # => nilメソッドが存在)

これはもう簡単ですね。

f:id:at_grandpa:20160214083548p:plain:w790

  • レシーバは C
  • Cの特異メソッドである #C を探索の第一歩とする
  • 見つかったのでメソッドを実行

 

いかがでしたでしょうか。

③ が特に複雑です。本当にそうなっているのか?と思ったので、pry で確かめてみました。

Cから特異クラスに渡り、そこからひたすらsuperclassを追っていきます。

[1] pry(main)> class C;end
=> nil
[2] pry(main)> C.singleton_class
=> #<Class:C>
[3] pry(main)> C.singleton_class.superclass
=> #<Class:Object>
[4] pry(main)> C.singleton_class.superclass.superclass
=> #<Class:BasicObject>
[5] pry(main)> C.singleton_class.superclass.superclass.superclass
=> Class
[6] pry(main)> C.singleton_class.superclass.superclass.superclass.superclass
=> Module
[7] pry(main)> C.singleton_class.superclass.superclass.superclass.superclass.superclass
=> Object
[8] pry(main)> C.singleton_class.superclass.superclass.superclass.superclass.superclass.superclass
=> BasicObject
[9] pry(main)> C.singleton_class.superclass.superclass.superclass.superclass.superclass.superclass.superclass
=> nil

ちゃんと、メソッド探索の順になっていますね。

 

まとめ

いかがでしたでしょうか。メソッド探索のイメージが湧きましたでしょうか。

今回のおさらいは以下の項目です。ここまで読んでくださった方なら、すんなりイメージできると思います。

  • オブジェクトは「インスタンス変数」と「クラスへの参照」を持つ
  • クラスは「インスタンスメソッド」を持つ
  • 全てのオブジェクトは必ず特異クラスを持つ
  • メソッド探索の第一歩目は特異クラスである
  • メソッドが見つからなければsuperclassを探索する
  • 一般的なクラス(Classクラス のオブジェクト)の「特異クラスのsuperclass」は そのクラスのsuperclassの特異クラス である
  • 一般的なオブジェクト(任意のクラスからnewされたもの)の「特異クラスのsuperclass」は そのオブジェクトの参照するクラス である
  • BasicObject まで探索してもメソッドが見つからなければ NoMethodError

メタプログラミング Ruby」では、このようなメソッド探索を、

 「レシーバから右へ一歩、そこからは上へ」

と表現しています。

なるほど、確かにそのような動きをしますね。

  • オブジェクトがレシーバでも「レシーバから右へ一歩、そこからは上へ」
  • クラスがレシーバでも「レシーバから右へ一歩、そこからは上へ」

となっていることに気づくと思います。

 

これで、「あれ?メソッド書いたのになんでNoMethodErrorなんだろう」ということはなくなりそうです。

ぜひ、メソッド迷子の手助けになればmm

Kindleで本を読む量が増えた自分なりの理由

今朝、この記事を読んだ。

honeshabri.hatenablog.com

ここに書いてあることは全て自分に当てはまり、自分の読書量が増えた理由をハッキリと述べてくれている。

だけど、Kindleで読書量が増えた「自分なりのもう一つの理由」がある。

それは、

  残りのページ数がわかりづらい

ということだ。

 

なかなか読書量を増やせなかった理由の一つは、

 「あぁ、あとこれだけ読まないといけないのか。」

と思う瞬間があるからだった。

「一冊を読み切った時の達成感」が大きく、それを期待しているからだろう。

 

まぁ、そう思うのは自分の性格なので簡単に変えられないと割り切ろう。

必要なのは「その性格とどう付き合っていくか」だし。

自分の性格を呪っても、ストレスしか生まないし。

 

で、この負の感情をうまく隠してくれるのがKindleだと思う。

スマホKindleだと、読んでる時には画面上に残りのページ数が表示されない。

ちょうど、ネットサーフィンしているときの感覚に似ていると思う。

残りのページ数など気にせずどんどん先に進める。

これは良い。

 

とはいえ、本当に好きな本に出会えれば、そのような負の感情には悩まなくて済むんだろうけども。

 

つまり、言いたいことは以下の2つ。

  • 現状の自分の読書不足をKindleは少し解消してくれた
  • もっとたくさんの本に触れ合って、好きな本に出会おう

ということで、Kindleセール見てきます。

人に優しい定期バッチ処理を書く

この記事は VOYAGE GROUP Advent Canlendar 2015 の17日目の記事です。

こんにちは。 @at_grandpa です。

Slackで将棋を指したあの日 から、もう1年が経つのですか。

早いですねぇ。

ちなみに at_grandma は、 Heroku様の神の裁き によって無期限活動停止中です。

 

さて、最近の自分はというと、定期バッチ処理を書くことが多いです。

定期バッチ処理

それは、サービスを運用する上で、少なからずお付き合いすることになるもの。

今回は、「人に優しい定期バッチ処理を書く」ために自分が普段心掛けていることを書こうと思います。

 

定期バッチ処理とは

言葉の通り、定期的にバッチ処理を行うことです。

メジャーなものとしては、crontab を用いての定期処理などが挙げられます。

用途としては、例えば、

  • 日次集計バッチ
  • 月次集計バッチ
  • 定期お掃除バッチ
  • 1時間毎の最適化バッチ
  • 監視バッチ

などなど、挙げ始めたらキリがありません。用途は多岐に渡ります。

定期バッチ処理を書いているといろいろと考える点があります。今回の内容は以下の6つ。

  1. コケた箇所を把握できるようにログを吐く
  2. 途中から再開できるようにする
  3. 冪等性(べきとうせい)を担保する
  4. リリース後の挙動を多方面からチェックする
  5. DRY_RUN mode
  6. README.mdを更新する

 

1.コケた箇所を把握できるようにログを吐く

バッチ処理は、どんな理由であれ必ずコケます。そういうものなのです。潔く認めましょう。

そして、そのコケた箇所を把握できるようにしましょう。でないと、リカバリできません。

理経過をログに吐き出すと、経過を後から追うことができます。

I, [2015-12-17T01:42:07.806195 #18175]  INFO -- : 最適化処理開始
W, [2015-12-17T01:42:15.152063 #18481]  WARN -- : user_nameが空です [USER_ID:34525]
W, [2015-12-17T01:42:17.001054 #18481]  WARN -- : user_nameが空です [USER_ID:66452]
I, [2015-12-17T01:42:18.951053 #18481]  INFO -- : 更新件数 1034 件
I, [2015-12-17T01:42:23.398336 #18856]  INFO -- : 削除件数 556 件
I, [2015-12-17T01:42:43.968574 #18956]  INFO -- : ファイル [/var/tmp/result_20151217.tsv] への書き込み完了

件数や書き込み先などを出力しておくと、件数が異常に変わったのはいつからか把握できたり、自分以外の人がログを見た時に有力な情報を与えることができます。

ですが、有力なログ吐き出しはなかなか難しいものです。運用過程が変わってくると欲しい情報も変わってきますし。

コツは以下のようなものかなと思います。

  • 各処理のポイントでログを出力する
  • コケた時のリカバリを想像して、必要な情報をログに出力する
  • 新たな情報が必要になったら追加するが、ログの出力量を確認する(情報量が多すぎないか、ディスクは大丈夫か)

昔、私は「ありったけのログを吐いておけば、欠損情報の原因がわかる!」と思い、細かにログを吐き出すプログラムを書いていました。

無論、そのログは、情報量が多すぎて誰にも見てもらえず、さらにはディスクを圧迫し、OPSチームに迷惑をかける事態となってしまいました。

適切なログ吐き出しで、自分にも優しく、周りにも優しいバッチを書きましょう。

 

2.途中から再開できるようにする

コケたバッチはコケた地点から再実行できるようにしましょう。

方法としては、「各処理単位で結果をファイルに吐き出す」のがオススメです。

例としてはこんな感じです。(Makefile)

run: target calc update

target:
    php target.php --date=$(DATE) > $(TARGET_FILE)

calc:
    php calc.php --date=$(DATE) --target=$(TARGET_FILE) > $(CALC_RESULT)

update:
    php update.php --date=$(DATE) --result=$(CALC_RESULT)

メリットとしては以下の点があります。

  • コケる手前の処理までは成功しているので、その結果ファイルさえあれば途中から再実行できる
  • テスト用ファイルを用意すれば、全体の処理を簡単にテストできる
  • 出力ファイルのフォーマットさえ決めてしまえば、処理は何の言語で書いても良い

しかし、これも銀の弾丸ではなくて、

  • ディスクを圧迫してしまう可能性がある
  • 速度が求められる場合、ファイルIOはかなりのロスとなる

などのケースがあります。この辺りは都度考える必要があるでしょう。

再実行するのは自分だけではないのです。周りにも優しい再実行機構を。

 

3.冪等性(べきとうせい)を担保する

これはバッチ処理では重要な要素です。

冪等性 = 「引数が同じであれば、バッチを何度叩いても結果が同じになる」 です。

以下のようなものはNGです。

  • バッチを何度も叩くと、DBのレコード件数が増えていく(または減っていく)
  • バッチを何度も叩くと、パラメータの値が徐々に変わっていく

冪等性のメリットは、

  • 自動実行でコケても、手動実行でリカバリできる
  • 何回叩いてもOKという安心感
  • 他のバッチへの依存性が少なくなる

です。これで安心してリトライできますね。

例えば、「1日分の集計データ1000件をDBに保存する」というバッチの場合、その日付のレコードを一旦 delete してから insert します。

こうすることで、そのバッチを何回叩いても、日付さえ同じであれば結果は変わりません。

冪等性が維持されていなければ、叩くのすら怖いバッチになってしまうでしょう。

 

4.リリース後の挙動を多方面からチェックする

リリース後の挙動をチェックするのは当たり前ですが、無事バッチが終わったからといって安心してはいけません。

下記項目を、定期間隔の1周期分は必ず見ましょう。

バッチの実行結果が妥当か

これはバッチの直接的な挙動確認です。

妥当な件数が更新されているか、パラメータは不可解な値になっていないか。

実行後にすぐにチェックしましょう。

サーバ監視項目

CPU, メモリ, トラフィック, ディスク容量, etc...

バッチ処理は大抵重い処理になりがちです。

その分、サーバーにかかる負荷も無視できません。

また、定期バッチが並行で複数走っている場合などは、他のバッチに影響を与えてしまうかもしれません。

できれば負荷チェックは開発環境で事前に行うべきですが、基本、本番環境のサーバーと全く同じパフォーマンスではないので、本番での様子見も必ず行いましょう。

サービスのKPIの挙動

これは忘れがちですが、非常に重要です

バッチが動いたということは、何らかの状態が変わったわけで、その影響はサービスに直接関わってきます。

ユーザーのアクセス数は変わっていないか、配信量に変化はないか、売上が極端に変わっていないか、etc...

これらの項目は大丈夫ですか?

大事なことは、バッチが正常に終わったからといって サービスが正常である保証はない ということです。

「バッチは何のエラーも吐かずに終わったが、パラメータの値が全て0になっていた...」

ということも十分起こり得ます。

バッチを書いて動作確認して終わり、、、ではないのです。サービスのためにバッチを書いているわけで、サービスに悪影響がないことを必ず確認しましょう。

ここまでの動作チェックを行えば、ディレクターさんにも優しいバッチ処理になります。

 

5.DRY_RUN mode

間違ったバッチを実行してしまった!

ああああ!DBにupdateが走ってしまっている!!!!

Ctrl+CCtrl+CCtrl+C

...

安心してください。それ、DRY_RUN mode ですよ。

 

何も考えずに引数を指定してバッチを手動実行。

$ php my_batch.php --date=2015/12/17

しかしこれは DRY_RUN mode であり、計算処理は行いますが、DBへの更新は一切行わないようにするのです。

実際にDBに書き込みを行うときは、

$ php my_batch.php --date=2015/12/17 --run

と、明示的に --run を引数に指定しなければならないようにします。

DBにアクセスする前に、必ず一度立ち止まることができます。

人に優しいバッチですね。

 

6.README.mdを更新する

修正したら README.md を更新しましょう。

そこのあなた、忘れていませんか?

あなたの書いたコードとREADME.mdの内容が違っていたら、「あなたの担当した箇所のREADME.mdは信用できない」となってしまいます。

そうすると、README.md は存在しないも同然になってしまいます。

あなたのコードはみんなが触るのです。

 

まとめ

いかがでしたでしょうか。人によっては当たり前かもしれませんが、自分にとっては色々と気付く点があったのでまとめました。

まぁ、ケースバイケースなことも多いですが、一般的に考えるべきことなのかなと思います。

これらの他にも、

  • リカバリコマンドを用意するのは正しいのか?
  • オレオレバッチではなく、チームの慣例に従う
  • 開発環境で、出来るだけ本番に近い状態でチェックする
  • 一夜漬けdeploy

などがありますが、今回はここまで。

明日は @_yukinoi さんです。

なんの話なんだろーなー。

AJITOグッズのご紹介

この記事は #ajiting Advent Calendar 2015 の11日目の記事です。

「#ajiting とは何だ?」という方は、上のリンク先のアドベントカレンダーで他の方が楽しく紹介してくださっているので是非ご覧ください。

AJITO。

実はAJITOにはグッズもあるのです。その制作に深く関わったので、今回はAJITOグッズをご紹介します。

#ajitingパーカ

ちょうど1年前くらいに出来上がりました。かっこいいパーカ。

f:id:at_grandpa:20151209205713j:plain:w600

去年の VOYAGE GROUP エンジニアブログ:Advent Calendar 2014 にて、@_nishigori さんも記事に書かれています。

atsumori.de

パーカのデザインをメインメンバーで話し合い、それをIllustratorでイメージにする部分を担当しました。

githubを利用してレビューしていただいたり、デザインを紙に印刷して黒パーカにあててみたり。

データはIllustratorで作成しました。一度入稿してしまうと届くまで現物を確認できないので多少恐怖があります。

revertできないリリースって怖いですよね。。。

開封の際は、「みんなワクワク」「自分はドキドキ」。そんな感じだったと思います。

 

f:id:at_grandpa:20151209213340p:plain:w400

 

寒くなってきた今日このごろ。チラホラと社内でも #ajitingパーカを見かけます。

背中の文字が気になる方は、ぜひAJITOに遊びに来てご確認ください。

 

#ajiting Tシャツ

さて、冬はパーカを作りましたが、夏になるとTシャツが欲しくなりますよね。

ということで、2015年春の終わりごろに #ajiting Tシャツ プロジェクトが始まりました。

シンプルなTシャツが良さそうという意見が多く、AJITOロゴを単色で全面に押し出すデザインに決定。

たたき台として、様々なカラーバリエーションをイメージ化して、みなさんの意見を伺いました。

こんな感じ。

f:id:at_grandpa:20151209215411p:plain

裏はパーカと同じ文字列が描かれています。

当初はこの中から2色を選ぶ予定でしたが、この時点で結構好評の声が多く、最終的に全色の中から購入者が選ぶ形になりました。

そしてさらに、「毎日が金曜日Tシャツ」も作成!

f:id:at_grandpa:20151209220649p:plain:w700

毎日が金曜日のごとく、AJITOはいつも賑やか。そんな思いを込めた1枚が出来上がりました。

 

そしていざ社内で注文を開始すると、なんと 注文総数100枚超え!!

AJITOロゴver、毎日が金曜日ver、合わせて総数127枚の注文!

みんなAJITO好きすぎるw

 

こちらが配布の様子。

f:id:at_grandpa:20151209221155j:plain:w500

早速試着した様子。

f:id:at_grandpa:20150513125930j:plain:w500

f:id:at_grandpa:20150513125754j:plain:w500

金曜日感出てるw

歴代の会社Tシャツ達の仲間入りもしました!

f:id:at_grandpa:20151209221243j:plain:w500

 

このように、Tシャツが多くの人の手に渡り、社内がカラフルになったのはいい話だなぁと思います。

一連の制作に携われて本当に良かったです。

 

#ajiting ステッカー

Tシャツ制作の合間に、YAPC::Asia Tokyo 2015 のノベルティ制作の話もありました。

「AJITOのステッカーを作ろう」ということで話が転がり始めます。

「やはりPCに貼りたいよねー」との意見が多かったので、背景が透明なシールに決定。

いろいろ試行錯誤して出来上がったデザインがこちらです。

f:id:at_grandpa:20151209222712p:plain

丸のAJITOロゴはTシャツ時代からのお決まりですが、右側ステッカーのロゴはちょっと雰囲気を変えてみました。

こちらがPCに貼った様子。

f:id:at_grandpa:20151209223133j:plain:w500

良さ気な感じがします。

依頼者も満足そう。

 

f:id:at_grandpa:20151209223403p:plain:w400

 

こうしてイベントで配られたステッカーは、数百枚に上ります。

今日もどこかで、このステッカーを貼ったPCが Pull Request を送っていることでしょう。

 

#ajiting コースター

コースターの話は以前からあったのですが、なかなか制作に至らず。

そんな時、弊社の美術サークルの方が、会社近くの FabCafe にてレーザー加工を行うと耳にしました。

それに便乗してコースター制作もお願いすることに。

出来上がったものがこちら。

f:id:at_grandpa:20151209224311j:plain:w400

思ったよりもかなり良い出来。

AJITOの雰囲気にもよく合う!

f:id:at_grandpa:20151209224448j:plain:w500

このコースターでお酒を飲むのも、#ajitingの楽しみ方の一つです。

 

思ったこと

これらの経験を踏まえて思ったことを。

AJITOはみんなで創るもの

AJITOの文化は、誰かが決めたものではありません。

みんなが自然と動き、「みんなのAJITO」として育っていきます。

一度訪れれば、あなたもAJITOの創造主なのです。

ノリと真面目さが絶妙なバランス

これは、僕の「AJITOが好きな理由」のひとつです。

ただノリだけでなんでもやっちゃうのではなく、かといって、真面目すぎるわけでもなく。

うまい具合に力を抜きつつ、やる時は本気で。

この絶妙なバランスが、ひとつの「心地よさ」を生んでいるのだと思います。

しかも、(上でも書きましたが)誰かがそうしようと決めたわけではないんですよね。

これはすごいことなんじゃないかなと個人的には思っています。

みんなAJITO好きなんだなぁ

うんうん。わかる。

 

最後に

いろんなグッズを制作するにあたって、メインで進めてくれた方、意見を言って頂けた方、グッズを手に取ってくれた方、その他携わって頂けた皆さん、本当にありがとうございました。

また何か制作するときには参加したいなーと思います。

 

全ての道はAJITOに通ず。

ぜひ一度お越しくださいませ。

 

明日は @ara_ta3 さんです。

基礎からのベイズ統計学入門 輪読会 #1 で発表しました

好きな定理はストークスの定理です(統計の定理ではない)。

こんにちは @at_grandpa です。

昨日、下記勉強会で発表してきました。

stats-study.connpass.com

資料は以下です。

www.slideshare.net

基礎からやりましょうということで参加。

2章を担当しました。

「その言葉、当たり前のように使っているけど、本当の意味わかってる?」と自問しながら勉強しました。

確率変数の解釈は、個人的には上の説明で納得しているのですが、ご意見いただければと思います。 Twitter@at_grandpa

 

勉強会は「一人が講師、他のみんなは生徒」ではなくて、「みんなで議論して前に進もう!」というスタンスが好きです。

今回の会もそのような雰囲気になって良かった。

また次回も参加して、3章以降の知識を身に付けたいです。

 

ですが、やっぱり自分の知識になるのは、担当した部分が大半なんですよねー。

勉強会Driven、おすすめです。

vimの関数ジャンプのかゆいところをMakefileと.vimrcで解決する

vim ctags

最近寒いので毛布を出した @at_grandpa です。

みなさん、関数ジャンプしてますか?

してますよね!

今までエラーで挫けていて導入していなかったのですが、最近本腰いれて解決に臨み、結果、素晴らしいライフチェンジングになりました。

偉大な先人の方々のツールは素晴らしい!巨人の肩の上に乗りまくりましょう。

ctags + vim

ググればたくさん出てきますが、簡単に導入方法と紹介を。

インストール

[debian系]
sudo apt-get install exuberant-ctags

[CentOS/RedHat系]
yum install ctags

[mac]
brew install ctags

tagsの生成

cd /path/to/target_dir
ctags -R

これで、tags というファイルが生成されます。

vimの設定

vimに読み込ませるtagsファイルを指定します。

:set tags=./tags;

;は、「親ディレクトリを探していく」というもので、tagsをプロジェクトのルートディレクトリに置いておけば参照できます。

ジャンプ!

/path/to/target_dir 以下で関数ジャンプが可能になります。

ジャンプしたいキーワードにカーソルを合わせ、<C-]> でジャンプします。

戻るのは<C-t>

これであなたもライフチェンジング。

 

かゆいところ

ですが、使っていて微妙にかゆいところがあるんですね。

蚊に刺されるほどかゆくはない。

少し布に触れてかゆくなる、そんな程度。

 

自分の環境は、結構大きなプロジェクトのリポジトリでして、複数言語が混じって構築されているんですね。

ここで ctags -R をすると、ctags君は一途にデータを集めて、対応言語全てで「たったひとつのtagsファイル」を作ってくれます。

こうなると、全言語が入り混じっているtagsファイルなので、ジャンプ候補に他言語が出てきてしまうのです。

例えばこんな感じです。(この候補から一番左の番号を選んでジャンプします)

  # pri kind tag               file
  1 F   f    FunctionName     /path/to/file.php
               public static function FunctionName() {
  2 F   m    FunctionName     /path/to/file.rb
               class:ClassName
               module FunctionName
  3 F   s    FunctionName     /path/to/file.pl
               sub FunctionName {
...

ちょっとかゆいですよね。

(「機能別に言語が分かれているだろうし、同じ名前を付けるのはナンセンス」というご意見もあるでしょうが、今は触れないこととします。)

 

Makefileと.vimrcで解決

やりたいことは「ジャンプ候補リストに他言語が混じらないようにする」です。

今回は、Makefile : 40行 + .vimrc : 15行 で解決しました。

 

Makefile

Makefileの役割は「言語別にtagsファイルを生成する」です。

言語別にtagsを生成すれば、ジャンプ候補リストに他言語が混じることはありません。

以下にその40行を示します。

# ----------------------------------------------------
#  tagを生成する
# ----------------------------------------------------

# 言語
# see also `ctags --list-languages`
lang := PHP    \
        Ruby   \
        Python \
        Perl

lower_lang := $(shell echo $(lang) | tr A-Z a-z)

# 各言語のtag対象ファイルの拡張子
# see also `ctags --list-maps`
ext := default       \
       .rb.ruby.spec \
       default       \
       default

TARGET_PATH  = $(PWD)  # ここは基本的に書き換える
git_toplevel = $(shell cd $(TARGET_PATH);git rev-parse --show-toplevel)
seq          = $(shell seq 1 $(words $(lang)))

ifeq ($(git_toplevel),)
    # gitリポジトリ管理ではない場合
    tags_save_dir = $(realpath $(TARGET_PATH))/tags
    tags_target_dir = $(realpath $(TARGET_PATH))
else
    # gitリポジトリ管理である場合
    tags_save_dir = $(HOME)/dotfiles/tags_files/$(shell basename $(git_toplevel))
    tags_target_dir = $(git_toplevel)
endif

.PHONY: create_tags $(seq)

create_tags: $(seq)

$(seq):
  mkdir -p $(tags_save_dir)
  ctags -R \
      --languages=$(word $@,$(lang)) \
      --langmap=$(word $@,$(lang)):$(word $@,$(ext)) \
      -f $(tags_save_dir)/$(word $@,$(lower_lang))_tags $(tags_target_dir)

これは、

make -f /path/to/Makefile create_tags TARGET_PATH=./

とすることで、カレントディレクトリ以下について、各言語($(lang)で指定してある言語)のtagsファイルを生成します。

こんな感じ。

ls ~/dotfiles/tags_files/{リポジトリ名}/
perl_tags
php_tags
python_tags
ruby_tags

簡単に言うと、以下のことをやっています。

  • $(lang)に設定された言語についてtagsファイルを生成
  • tagsファイルの名前は {言語名}_tags
  • git管理のリポジトリでない場合、カレントディレクトリに各tagsを配置
  • git管理のリポジトリである場合、~/dotfiles/tags_files/{リポジトリ名}/*に各tagsを集約

これだけです。

これで言語別にtagsファイルを生成できました。

.vimrc

.vimrcの役割は「ファイル毎に適切なtagsファイルを設定する」です。

以下にその15行を示します。

" ファイルタイプ毎 & gitリポジトリ毎にtagsの読み込みpathを変える
function! ReadTags(type)
    try
        execute "set tags=".$HOME."/dotfiles/tags_files/".
              \ system("cd " . expand('%:p:h') . "; basename `git rev-parse --show-toplevel` | tr -d '\n'").
              \ "/" . a:type . "_tags"
    catch
        execute "set tags=./tags/" . a:type . "_tags;"
    endtry
endfunction

augroup TagsAutoCmd
    autocmd!
    autocmd BufEnter * :call ReadTags(&filetype)
augroup END

以下のことをやっています。

  • BufEnterReadTags(&filetype)が発動
    • バッファに入ったときに発動。ウィンドウ移動やタブ移動などで発生。
  • execute "set tags=..."で読み込むtagsファイルを切り替え
    • git管理されているなら ~/dotfiles/tags_files/{リポジトリ名}の言語別のtagsを指定
      • &filetypeを使用することで、ファイルの言語別に読み込みが可能
    • git管理されていないなら、ルートディレクトリにある各言語のtagsを指定する

これで、ファイルの種類の応じて適切なtagsファイルを設定できます。

 

その他の設定

かゆいところを無くすために、キーマッピングも設定しました。

set notagbsearch

" [tag jump] カーソルの単語の定義先にジャンプ(複数候補はリスト表示)
nnoremap tj :exe("tjump ".expand('<cword>'))<CR>

" [tag back] tag stack を戻る -> tp(tag pop)よりもtbの方がしっくりきた
nnoremap tb :pop<CR>

" [tag next] tag stack を進む
nnoremap tn :tag<CR>

" [tag vertical] 縦にウィンドウを分割してジャンプ
nnoremap tv :vsp<CR> :exe("tjump ".expand('<cword>'))<CR>

" [tag horizon] 横にウィンドウを分割してジャンプ
nnoremap th :split<CR> :exe("tjump ".expand('<cword>'))<CR>

" [tag tab] 新しいタブでジャンプ
nnoremap tt :tab sp<CR> :exe("tjump ".expand('<cword>'))<CR>

" [tags list] tag list を表示
nnoremap tl :ts<CR>

自分の直感に近い感じで設定できたので、始めからストレス無く活用できました。

 

良かった点

tags読み込みの自動切り替えやキーマッピングを設定して、最終的に良かった点は以下です。

  • ジャンプ候補リストに他言語の候補が出なくなった
  • 既存リポジトリ内にtagsファイルを置くのではなく、dotfiles/tags_files/{リポジトリ名}に集約することで管理しやすい
  • .gitignoretagsと書かなくて良い
  • 直感的操作でタグジャンプできた
  • コマンド一発で設定できる

これでコードを読むストレスが格段に減り、いろんなコードを読みたくなりました。

これは良い傾向。

これらのコードはgithubに上げてあります。

github.com

 

もしかしたら車輪の再発明

もしかしたら、もっともっと簡単に解決できる方法があるかもしれません。

実は車輪の再発明だったりして...

良い方法、ご存知の方いらっしゃいましたら、コメントやTwitterで是非つぶやきましょう。
(確かneobundleのプラグインでtags系のものがあったと思いますが、vimが重くなったのでやめた記憶)

個人的にはvimscriptと戯れることができたので面白かったです。

時には車輪の再発明をしてみるのも良い経験になりそうですね。

 

周りのエンジニアに関数ジャンプについて聞いてみると、

  「IntelliJ IDEA 使ってるよー

IDE...

なにそれ美味しいの?( ^ω^)おっおっ

 

美味しいらしい...

味見してこよう...λ............トボトボ