TODESKING
技術ブログ

Scala雑記

|

しばらく前ですが、Scalive #1というところでLTしてきました。

Scalaのコンパイルを3倍速くした話

こういう実務寄りのScalaイベントって珍しい気がしますね。なんと発表者が誰もモナドって言わなかった!! その代わりコンパイル速度について言及されまくってましたが……。

3月末からScalaでアドテクやるという会社に転職したのでScalaでアドテクをやってますが、Rubyでソシャゲやるのとそれほど感覚は変わらない。どちらも制限時間付きの大量リクエストをさばく必要があるけど、それなりのインフラがあれば、あとはふつうに高品質なコードを書いて粛々とレスポンスを返すのみですよワハハ。なのでハイパフォーマンスまわりのおもしろい話はできません。まあ常識で設計してふつうに開発すればいいんじゃないの。とか言ってると強い人から殴られそうだな……

最近は技術的負債の返済を主なミッションとしてビルドシステムの見直しとかフラジャイルなテストを叩いて直すとかやっているのだけど、とにかく依存性管理に悩まされることが多い。これはScalaというよりJavaのエコシステムに起因してるのだけど、とにかくお前らライブラリをちゃんと管理してほしい……。

例を挙げる。

  • プロジェクトが依存している hbasejruby-complete に依存しており、jruby-completeにはなぜかjoda-timeライブラリ(古い)のクラスまで同梱されている
  • いっぽうプロジェクト自身もjoda-time(新しい)へ依存している

その結果、クラスパスの順序によってコンパイルが通ったり通らなかったりする。

この手の「複数のモジュールが同じクラスを含んでいる」問題があまりにだるいのでsbt-conflict-classesというsbtプラグインを作った。クラスパス内の衝突しているクラスを抽出して表示するという代物で、トラブルシュートにたいへん便利。このへんの依存性解決ノウハウはそのうちまとめたいです。あとジャバコミュニティは同一jarに別ライブラリのクラスを同梱したりバージョンアップ時にorganization名変えたりするのをやめろ(みんな困ってないんだろうか……)。

GDBでsegmentation faultの原因を調査する(on OSX)

|

作業ログです。

1
2
§ ./ctags -R ~/.vim
Segmentation fault: 11

グエッ。というわけで原因を調査するはめに。

コアダンプ取る

1
$ ulimit -c unlimited

/cores/core.{PID}にコアダンプが出力されるようになる。

なんかcore file size: cannot modify limit: Operation not permittedとか言われて変更できないことがある(参照)けど、どうにかする。

gdbでコアダンプ読む

1
2
3
4
5
$ gdb -c /cores/core.1234
GNU gdb (GDB) 7.7.1
....
"/cores/core.1234": no core file handler recognizes format
(gdb)

とか言われて読み込みに失敗する。いったいなんなんだ(未解決)

検索すると普通にこれで読めるという説と対応してねえよという説があって謎。

コンパイルオプションをあれしてGDBであれする

1
2
# Makefile
CFLAGS    = -g -O0 -ggdb

-ggdb でGDB用デバッグ情報を付与できる。

1
2
3
4
5
6
7
8
$ gdb ./ctags
...
Reading symbols from ./ctags...done.
(gdb) r -R ~/.vim
Starting program: ./ctags -R ~/.vim

Program received signal SIGSEGV, Segmentation fault.
0x00007fff8b85ef80 in ?? ()

フムー落ちた

スタックトレース見る

1
2
3
4
5
(gdb) bt
#0  0x00007fff8b85ef80 in ?? ()
#1  0x00007fff5fbfee10 in ?? ()
#2  0x0000000100033fa8 in findVimTags () at vim.c:720
Backtrace stopped: frame did not save the PC

フレーム #2 の中身見る

1
2
3
(gdb) frame 2
#2  0x0000000100033fa8 in findVimTags () at vim.c:720
720             if ( strncmp ((const char*) line, "UseVimball", (size_t) 10) == 0 )

lineという変数怪しいですね

1
2
(gdb) p line
$1 = (const unsigned char *) 0x0

ヌル

で、ソースの該当箇所を見るとNULLチェック忘れてるということがわかる。

1
2
3
4
5
6
line = readVimLine(); // May returns NULL

if ( strncmp ((const char*) line, "UseVimball", (size_t) 10) == 0 )
{
  parseVimBallFile (line);
}

おわり

H2 Database、マルチスレッドでアクセスするとLock timeoutが頻発する件の解決法について

|

h2 1.3.176で確認。

jdbc:h2:file:....でローカルのDBを開いて、マルチスレッドでクエリ発行してるとテーブルのlock timeoutでエラーになる。

1
Caused by: org.h2.jdbc.JdbcSQLException: Timeout trying to lock table "USER";

みたいなやつ。

同時アクセスを相当制限しても低確率で発生するし、タイムアウト長くしても発生するし、何らかのバグがある気がする……。

接続時のjdbc文字列に、MVCC=TRUEオプションを指定したら解決しました。

http://stackoverflow.com/questions/4162557/timeout-error-trying-to-lock-table-in-h2

Scalaプロジェクトをmvnリポジトリに公開する

|

github.ioにプライベートなmvnリポジトリ作ってそこにアップロードするというのをやってみた。

1
2
3
4
5
6
7
8
9
10
11
// build.sbt

organization := "com.todesking"

name := "library_name"

version := "1.2.3"

scalaVersion := "2.10.4"

publishTo := Some(Resolver.file("com.todesking", file("./repo/"))(Patterns(true, Resolver.mavenStyleBasePattern)))

くらいを指定しておく。

最後のpublishToがライブラリ出力先の指定。

この状態で sbt publish することで./repo/に成果物ができるので、その内容をアップロードすればよし。

使用時には、

1
2
3
resolvers += "resolver name" at "http://成果物ルートディレクトリの場所"

libraryDependencies += "(organization)" %% "(name)" % "(version)"

とすればよし。

java.net.URLをHashMapに突っ込むと大変なことになるのでやめろ、それどころかequalsを呼ぶだけでも大変なことに

|

参照: http://stackoverflow.com/questions/2348399/why-does-java-net-urls-hashcode-resolve-the-host-to-an-ip

公式ドキュメントによると:

public boolean equals(Object obj)

2 つの URL オブジェクトが等しいのは、同じプロトコルを持ち、同じホストを参照し、ホスト上のポート番号が同じで、ファイルとファイルのフラグメントが同じ場合です。

2 つのホストが等価と見なされるのは、両方のホスト名が同じ IP アドレスに解決されるか、どちらかのホスト名を解決できない場合は、大文字小文字に関係なくホスト名が等しいか、両方のホスト名が null に等しい場合です。

ホスト比較には名前解決が必要なので、この操作はブロック操作です。

java.net.URL#equals()

もちろん等価性に依存するhashCode()などもこの影響を受けるので、うっかりコレクションにURLを格納すると大量の名前解決が発生して死ぬほど遅くなる。

代替案:java.net.URIを使う

ではどうするのがいいかというと、java.net.URIのほうを使うと名前解決しないので良いです。

基本的には同じような操作が可能ですが、java.net.URLより書式に厳密なので注意。不正な文字列を与えるとjava.net.URISyntaxExceptionになります。

よくある日本語文字列がそのまま入ったURLなどもアウトなので、事前にjava.net.URLEncoderなどを使ってダメな文字をエスケープする必要があります。

別の代替案:URLStreamHandlerを明示的に指定してURLを構築する

ホスト比較時の動作はURLStreamHandlerを使ってるようなので、名前解決しない実装を作成してURL(String protocol, String host, int port, String file, URLStreamHandler handler)等のコンストラクタに明示的に渡すようにすれば良いと思われます(未確認)。こっちのほうが影響範囲は少なそう。

Macで日本語入力が有効になってるかどうか返すコマンド作った

|

https://github.com/todesking/is_ascii_capable.mac

is_ascii_capableコマンドが0を返したらIME無効、1だったら有効というやつです(IMEまわりのアーキテクチャに明るくないので用語はいいかげん)。ビルドは各自でやってください。

日本語入力と相性の悪いエディタをどうにかするために作ったけど2014年にもなってなんでこのような苦労を……(社会はきびしい)。

GCCでCarbonを使用したObjective-Cのコードをコンパイルする

|

自由を大切にしている老人なのでエックスコードとかいう難しいやつ使いたくないんですよ……。

結論としては以下のコマンドでいけました。

1
2
3
4
5
6
7
gcc \
  objc_code.m \
  -o output_file_name \
  -lobjc \
  -mmacosx-version-min=10.9 \
  --sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/ \
  -Wl,-framework,Carbon
  • -lobjcで言語を指定する
  • -mmacosx-version-minでOSXのバージョン指定する(どういう意味があるのか分かってません)
  • -sysrootでOSX SDKの場所を指定する
  • -Wlでリンカオプションを渡し、使用するフレームワーク名を指定する

良かったですね。

Rubyで高速にパターンマッチするgemを作った

|

Ripperの出力とかParseletの解析結果などを扱うのに、ArrayやHashでパターンマッチして中身を取り出す処理を多用する必要があったのでパターンマッチライブラリを作りました。

GitHub: todesking/patm

同様のライブラリとしてはpattern-matchがあります。 機能面ではpattern-matchのほうが豊富ですが、PATMは高速なのが売りです(DSLによるメソッド定義を使用した場合、ネイティブRubyコードにコンパイルされるため50倍くらい速い。case式内で使用した場合でも7倍程度)。ベンチマークについてはこの記事の下のほう参照。

主な機能

DSLによるメソッド定義

extend Patm::DSL することで define_matcherを使ったメソッド定義が可能です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
require 'patm'

class Matcher
  extend Patm::DSL

  P = Patm
  _ = P._any
  _1, _2, _3 = P._1, P._2, P._3
  _xs = P._xs

  define_matcher :match do|r|
    # ルールオブジェクトrが引数に渡ってくるので、 on および else を使ってパターンを定義する。
    # - on(pattern) {|match, _self| }
    #    patternにマッチした場合ブロックが呼ばれる。
    #    match: マッチオブジェクト。キャプチャした値にアクセスできる。
    #    _self: 他のメソッドを呼ぶ場合、この引数を経由して呼ぶ
    # - else {|value, _self| }
    #    onで指定されたどのパターンにもマッチしなかった場合にブロックが呼ばれる。
    #    (elseを指定しない場合はMatchError例外となる)
    #    value: マッチしなかった値
    #    _self: 上述

    r.on(value: {key: _1, value: _2}) {|m| "KV: #{m._1}, #{m._2}" }
    r.on([:assign, [:v, _1, [_2, _3]]]) {|m| "AS: #{m._1}, #{m._2}, #{m._3}" }
    r.on([:container, _1]) {|m, _self| _self.match(m._1) }
    r.else {|obj| "Unknown: #{obj.inspect}" }
  end
end

m = Matcher.new

m.match(1)
#=> Unknown: 1

m.match({value: {key: 10, value: 999}})
#=> "KV: 10, 999"

m.match([:assign, [:v, 10, [20, 30]]])
#=> "AS: 10, 20, 30"

m.match([:container, [:assign, [:v, 10, [20, 30]]]])
#=> "AS: 10, 20, 30"

case式内での使用

Patm.match(pattern)を使用することで、case式内でパターンマッチできます。

手軽だけど、毎回パターンオブジェクトを構築する必要があるのでDSLを使用するよりは重い。

1
2
3
4
5
case value
when m = Patm.match([1, 2, Patm._1])
  m._1
# ...
end

パターン: 値によるマッチ

1, "foo", :symbol, /regex.*/, 等。

case式と同様、===による比較を行います。

パターン: 任意の値

Patm._anyで任意の値にマッチします。

パターン: キャプチャ

Patm._1, Patm._2, ... は、任意の値にマッチし、その結果を対応する数字でキャプチャします。 キャプチャした結果は、マッチオブジェクトからm._1, m._2, ...を使用してアクセス可能です。

また、パターンの後ろに[]をつけることにより、任意の名前でキャプチャ可能です。Patm._any[:x]は、マッチオブジェクトからm[:x]として参照可能です。

パターン: 配列

配列内の要素を元にマッチします。

配列には特殊なパターン Patm._xs を一個だけ含めることができます。 パターン[1, 2, Patm._xs[:xs], 3, 4]は、[1, 2]で始まり[3, 4]で終わる任意の配列にマッチし、中間の配列が:xsという名前でキャプチャされます。

パターン: ハッシュ

ハッシュ内の要素を元にマッチします。今のところキーは定数のみで、パターンは使えません(需要が思いつかなかったので)。

特殊なオブジェクト Patm.exactをキーに含めることで、パターンに含まれないキーを許容するかどうか指定できます。初期設定では、パターンに含まれないキーも許容します。

また、Patm.opt(...)やパターンの.optメソッドを使用することで、キーが必須かどうかを指定できます。

1
2
3
4
5
# 1
{a: Patm._1, b: Patm._2.opt, Patm.exact => true}

# 2
{a: Patm._1, b: Patm._2.opt}

1のパターンは、{a: 1, b: 2}, {a: 1} にはマッチしますが {a: 1, c: 3}にはマッチしません。

2のパターンは {a: 1, b: 2}, {a: 1}, {a: 1, c: 3} すべてにマッチします。

パターン: Struct

これは需要は特になかったけど、勢い余って作った。 構造体をScalaのcase classみたいに使える。

Patm[struct_class].(...pattern...) で、struct_class用のパターンが作成できます。

1
2
3
4
5
6
7
8
Name = Struct.new(:first, :last)

case Name.new('todes', 'king')
when m = Patm.match(Patm[Name].('todes', Patm._1))
  # ...
when m = Patm.match(Patm[Name].(last: 'king')) # ハッシュで個別の属性のみ指定できる
  # ...
end

パターン: 合成

&を使用して複数のパターンのANDを指定できます。_1&String で、任意の文字列を_1にキャプチャできます。

Patm.or(...)を使用してOR条件を指定できます。

コンパイル処理について

Patm::DSLのメソッド定義の実体はPatm::Ruleで、下記のようにすればコンパイル後のコードが確認できます。

1
2
3
4
5
6
7
rule = Patm::Rule.new(false) {|r|
  r.on([Patm._any, 1]) { 1 }
  r.on(a: [String, Patm._xs], b: Patm._1.opt) {|m| m._1}
  r.else {|value| value }
}.compile

puts rule.src
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    def apply(_obj, _self = nil)
      _ctx = @context
      _match = ::Patm::Match.new
if ((_obj.is_a?(::Array)) &&
(_obj.size == 2) &&
((_obj_elm = _obj[1]; 1 === _obj_elm)))
_ctx[0].call()
elsif (_obj.is_a?(::Hash) &&
_obj.size >= 1 &&
(_obj.has_key?(:a) && (_obj_elm = _obj[:a]; (_obj_elm.is_a?(::Array)) &&
(_obj_elm.size >= 1) &&
((_obj_elm_elm = _obj_elm[0]; _ctx[2] === _obj_elm_elm)))) &&
(!_obj.has_key?(:b) || (_obj_elm = _obj[:b]; _match[1] = _obj_elm; true)))
_ctx[3].call(_match)
else
_ctx[4].call(_obj)
end
    end

同様の処理を手書きするのに比べると4〜5倍程度遅いようです。マッチオブジェクトの生成と、マッチ時のproc呼び出しが原因だと思われます。まあ実用上は全く問題ない速度が出てる。

ベンチマーク結果

詳細はコード参照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
RUBY_VERSION: 2.0.0 p247

Benchmark: Empty(x10000)
                    user     system      total        real
manual          0.010000   0.000000   0.010000 (  0.012840)
patm            0.040000   0.000000   0.040000 (  0.044294)
pattern_match   2.230000   0.040000   2.270000 (  2.304750)

Benchmark: SimpleConst(x10000)
                    user     system      total        real
manual          0.010000   0.000000   0.010000 (  0.014267)
patm            0.040000   0.000000   0.040000 (  0.040269)
patm_case       0.190000   0.000000   0.190000 (  0.193041)
pattern_match   2.260000   0.020000   2.280000 (  2.321225)

Benchmark: ArrayDecomposition(x10000)
                    user     system      total        real
manual          0.050000   0.000000   0.050000 (  0.056363)
patm            0.240000   0.000000   0.240000 (  0.269492)
patm_case       2.050000   0.010000   2.060000 (  2.105357)
pattern_match  16.520000   0.100000  16.620000 ( 17.116351)

Benchmark: VarArray(x10000)
                    user     system      total        real
manual          0.050000   0.000000   0.050000 (  0.059690)
patm            0.220000   0.000000   0.220000 (  0.219058)
patm_case       1.710000   0.010000   1.720000 (  1.727676)
pattern_match  13.280000   0.090000  13.370000 ( 14.916347)

実世界における使用例

近況

|

退職した

Rubyのローカル変数をシンタクスハイライトするVimプラグインを書いた

|

Rubyはローカル変数への参照と無引数のメソッド呼び出しを同じ記法で書けるので、コードを読むときに混乱したりtypoでNoMethodErrorを出してがっかりすることが多々あります。 幸いなことにこれらは静的に解析することができるので、ローカル変数への参照を色付けするVimプラグインを書いた。

Github/ruby_hl_lvar.vim

すごく便利な気がする!!!!!!!

Rubyインタフェース(>=1.9)が有効になったVimが必要なのでご注意ください。MacVim 7.4 KaoriYa 20140107で動作確認しました。

しくみ

Ruby1.9以降に標準添付されているripperというライブラリで、Rubyの構文解析をしてローカル変数への参照を取り出しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require 'ripper'

Ripper.sexp(<<EOS)
a = 10
b = 20
c = a + b + foo
EOS

# =>
[:program,
 [[:assign, [:var_field, [:@ident, "a", [1, 0]]], [:@int, "10", [1, 4]]],
  [:assign, [:var_field, [:@ident, "b", [2, 0]]], [:@int, "20", [2, 4]]],
  [:assign,
   [:var_field, [:@ident, "c", [3, 0]]],
   [:binary,
    [:binary,
     [:var_ref, [:@ident, "a", [3, 4]]],
     :+,
     [:var_ref, [:@ident, "b", [3, 8]]]],
    :+,
    [:vcall, [:@ident, "foo", [3, 12]]]]]]]

ripperを使うとこのように位置情報付きの構文木が取れるので、ここからローカル変数への参照(:var_ref:assign内の:var_firld等)を抽出しています。 構文木をパターンマッチして処理するのにPATMというパターンマッチgemを作ったんですが詳細はいずれ。

Vimでmatchadd()を使ってキーワードをハイライトさせる方法

抽出したローカル変数への参照情報は、[識別子, 行, 列]という形式になっています。\%(行)l\%(列)c....(識別子の文字数分)という正規表現を使うことで指定した位置の識別子を指定可能。

できた正規表現をmatchadd()で指定することでハイライトを登録しています。

注意点として、matchadd()はwindow単位で有効となるため(なぜだ)、バッファ切替時にはそれに合わせて適切にmatchdelete()してやる必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
autocmd BufWinEnter * call ruby_hl_lvar#redraw()
autocmd BufWinLeave * call ruby_hl_lvar#redraw()
autocmd WinEnter    * call ruby_hl_lvar#redraw()
autocmd WinLeave    * call ruby_hl_lvar#redraw()
autocmd TabEnter    * call ruby_hl_lvar#redraw()
autocmd TabLeave    * call ruby_hl_lvar#redraw()

function! ruby_hl_lvar#redraw()
  if w:現在のマッチ == b:現在のマッチ
    return
  endif
  call matchdelete(w:現在のマッチID)
  if b:現在のマッチ
    let w:現在のマッチID = matchadd(b:現在のマッチ)
    let w:現在のマッチ = b:現在のマッチ
  endif
endfunction

みたいな処理になってる。これでたぶん大丈夫なはず(自信なし)。