Vimのパッチ存在確認処理を速くした

昨日Apple Eventを待機しながらVimのコードを眺めていたら、なんだか香ばしい匂いのするコードを見つけてしまいました。

/*
 * Return TRUE if patch "n" has been included.
 */
    int
has_patch(int n)
{
    int        i;

    for (i = 0; included_patches[i] != 0; ++i)
        if (included_patches[i] == n)
            return TRUE;
    return FALSE;
}

ペロッ… こ、これは、線形探索!

Vimは、メインのブランチのすべてのコミットでパッチバージョンが上がっていく方式をとっています。 プラグインが新しい機能を使いたい時に、ユーザーが使っているVimに特定のパッチが入っているかをチェックする必要があります (関数やイベントなど機能が入っているかを直接チェックすることができる場合もありますが、機能が入ってもバグっていることがありますし、直接チェックできない機能もあります。その時はパッチバージョンでチェックする必要があります)。 こういうプラグイン実装者のために、 has('patch-8.2.1200') のように関数 has() でパッチバージョンが含まれているかをチェックすることができます (詳しくは :h has-patch)。

含まれているパッチバージョンは included_patches という配列にすべて羅列されており、これを線形に探索しているコードです。 しかし included_patches は必ず大きい番号順にソートされており、二分探索したほうがほとんどのケースでお得なはずです。 ということで昨日シュッとコードを書いて、出してみました。

github.com 素朴な二分探索です。

/*
 * Return TRUE if patch "n" has been included.
 */
    int
has_patch(int n)
{
    int        h, m, l;

    // Perform a binary search.
    l = 0;
    h = (int)(sizeof(included_patches) / sizeof(included_patches[0])) - 1;
    while (l < h)
    {
        m = (l + h) / 2;
        if (included_patches[m] == n)
            return TRUE;
        if (included_patches[m] < n)
            h = m;
        else
            l = m + 1;
    }
    return FALSE;
}

朝起きたら、無事 v8.2.1973 として取り込まれていました。修正なしで入っていると嬉しいですね。 github.com

ここの処理は大昔から変わっておらず (少なくとも2004年からずっと同じコードでした。もっと遡れるかもしれません) で、今まで見過ごされていたのが不思議なくらいです。 Vimがまた少し速くなってよかったですね。

自分の生産性について考えてみる

組織の生産性を上げるために何かアクションを取りたいのだけど、今は何も計測できていないし、改善案も出せていない。 組織はいろいろな人が同時に動いていて難しいので、まずは自分自身の生産性について考えてみる。

自分はWFHで生産性が下がっているような気はしていたけれど、WFHだから下がっているのか、立場が変わって下がっているのか、そもそも実は下がっていないのかもしれない。 自分の生産性をどう評価するかは置いておいて、まずは素朴にパフォーマンスがでることとでないことを考えてみた。 普段はこういうことは外には書かないんだけど、恥を忍んで出してみる。

得意なこと

  • 一ユーザーとして不満に思っていたことの改善
  • 課題感や解決方法に納得のいくタスク
    • 電気系出身なので「共振」できることってイメージだけど他の人に伝わらないから「共感」とか「納得」って言ってる
  • ユーザー目線でサービスを触ったり告知を読んだりする
  • 一つのものを集中して作り上げる
  • 技術的に正しい改善
  • 既存の実装のリファクタリングまたは再実装
    • 仕様を整理し直してきれいに作りたい
    • 長年の課題を再設計して解決したい
  • 既存の実装の別言語での実装
    • よくやる
  • 実行パスのカバレッジを脳内で上げる
    • 不具合の原因を探る時によくやる
  • シンプルに作り保守的にメンテしていく
    • シンプルな仕様を考えるのもわりと得意
  • データ構造やファイルフォーマットの設計
    • 単に好きってだけ
  • 好きなことなら休日でもコードを書いている
  • OSSを触る、OSSを直す、OSSを作る

苦手なこと

  • 課題感や解決方法に共感できないタスク
    • 仕事はやらねばならぬことは理解している
    • やれといわれたらやるがスピードは出ない
  • 複数のタスクを同時に進める
    • とくに面倒なタスクが同時に目の前にあると急激に集中力が落ちる
  • 自分から様子を見に行く
    • 組織の生産性が上がることはわかるがポーリングは大変
    • 呼びつけてくれ…
  • 隙間時間に仕様を把握する
    • 一日集中したい
  • 旗振り役
    • アサイン決めたり期限を決めたり…
  • 組織の問題点を指摘する
    • 現状維持になってしまう
    • 変えていくのが正義なのは知っている
    • 組織がどうあるべきかの知識がない
  • 評価のためのアクションプランを考える
    • したことを評価基準にあわせてそれっぽく書いている
  • 本を読む
    • 一年に一冊くらいしか読まない

苦手なことを減らすには

  • 意識して苦手なことを克服する時間を取る
    • 意識して本を読む
      • 自分はもっと本を読むべきって数年思ってるけど、目の前に面白い問題があったらコード書いちゃう
  • それっぽい本を読んで方法論を学ぶ
  • タスクを直列にしてそれぞれに集中して片付ける
    • 意識してこれもやる
      • 昔ためしたポモドーロは合わなかった
  • 苦手なことを補ってくれる人を捕まえる
    • 自分が苦手なことが得意な人はいるので相談する
    • 苦手なことは移譲する
      • やりすぎると自分の成果は?となる
  • 得意なことを活かせるロールにつく

この休日でもう少し深堀りして考えてみる。

Regexp::AssembleのGo実装 rassemble-go を作りました

PerlにはRegexp::Assembleという便利なライブラリがあります。 複数の正規表現を受け取り、それらのいずれかにマッチする正規表現を構築するためのライブラリです。

my $ra = Regexp::Assemble->new;
$ra->add( 'ab+c' );
$ra->add( 'ab+\\d*\\s+c' );
$ra->add( 'a\\w+\\d+' );
$ra->add( 'a\\d+' );
print $ra->re; # prints (?:a(?:b+(?:\d*\s+)?c|(?:\w+)?\d+))

このライブラリのGo実装を金曜日の夜から書き始めて、ようやく形になってきたので公開しました。

package main

import (
    "fmt"
    "log"

    "github.com/itchyny/rassemble-go"
)

func main() {
    xs, err := rassemble.Join([]string{"abc", "ab", "acbd", "abe"})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(xs) // a(?:b[ce]?|cbd)

    xs, err = rassemble.Join([]string{
        "北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県", "茨城県",
        "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県", "新潟県", "富山県",
        "石川県", "福井県", "山梨県", "長野県", "岐阜県", "静岡県", "愛知県", "三重県",
        "滋賀県", "京都府", "大阪府", "兵庫県", "奈良県", "和歌山県", "鳥取県", "島根県",
        "岡山県", "広島県", "山口県", "徳島県", "香川県", "愛媛県", "高知県", "福岡県",
        "佐賀県", "長崎県", "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県",
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(xs) // 北海道|(?:青森|岩手|宮[城崎]|秋田|山[口形梨]|福[井岡島]|茨城|栃木|
                    // 群馬|埼玉|千葉|(?:神奈|石|香)川|新潟|(?:富|和歌|岡)山|長[崎野]|岐阜|
                    // 静岡|愛[媛知]|三重|[佐滋]賀|兵庫|奈良|鳥取|島根|(?:[広徳]|鹿児)島|
                    // 高知|熊本|沖縄)県|東京都|京都府|大(?:阪府|分県)
}

ライブラリ用途を意図していますが、動作確認 (とこのブログでの説明) のためにcliツールとしても使えるようにしています。

 % go get github.com/itchyny/rassemble-go/cmd/rassemble
 % rassemble abcd abd acd ad
ab?c?d
 % rassemble $(head -n30 /usr/share/dict/words)
a(?:a(?:l(?:ii)?|m|rd(?:vark|wolf))?|ba(?:c(?:a(?:te|y)?|i(?:nat(?:e|ion)|s(?:cus|t))|k|tinal(?:ly)?)?)?)?|A(?:a(?:ni|r(?:on(?:i(?:c(?:al)?|t(?:e|ic)))?|u))|b(?:ab(?:deh|ua))?)?

作ったきっかけは、以下のブログを見たことでした。

本当はこのツールのアイデアが思いついたときには、GoのRegexp::Assemble実装も作ってやろうと思っていたのですが、難しくて断念しました…。このツールのアイデア自体は数年前にあったのですが、Regexp::Assembleがネックになって作れておらず、結局その実装を諦めた結果このツールを世に送り出すことができました。 Regexp::AssembleのGo移植は誰かやってほしい…!

GoのテストをCIで簡単に並列実行する | おそらくはそれさえも平凡な日々

確かに本家の実装を覗いてみるとそこそこ行数があり、複雑なコードで頭がクラクラします。 しかし、Regexp::Assembleは正規表現のパースのコードも含んでおり、そこがかなりの行数を占めています。 Goで実装する場合は、パースは標準ライブラリにまかせてしまえば良いので、正規表現のマージ処理に専念することができます。 また、本家を一行ずつ変換する必要はなく、テストケースを眺めながら処理を想像しつつ書いていけば、移植はそんなに難しくありません (実は私も本家のソースコードはほとんど読んでいません)。


基本的には前方がまとまっている方が正規表現は速いはずですから、トライ木の正規表現版を作れば良いわけです。 例えば

abcde
abcfg
afcfg

この3つの単語からトライ木を作ると

a -> b -> c -> d -> e
 \         \-> f -> g
  \-> f -> c -> f -> g

のようになりますから

a(?bc(?:de|fg)|fcfg)

となりますし、これが正規表現だとしても同じ話です。 a\w\d*(?:abc)?a, \w, \d* (?:abc)? がそれぞれ一つの塊と考えて、これが一致するだけ貪欲に前方をまとめるだけです。

面白いのは後方をまとめる処理です。 Regexp::Assembleの例には次のような例が載っています。

my $ra = Regexp::Assemble->new;
$ra->add( 'ab+c' );
$ra->add( 'ab+\\d*\\s+c' );
$ra->add( 'a\\w+\\d+' );
$ra->add( 'a\\d+' );
print $ra->re; # prints (?:a(?:b+(?:\d*\s+)?c|(?:\w+)?\d+))

後方をまとめないと (?:a(?:b+c|b\d*\s+?c|\w+\d+|\d+)) のようになってしまいます。 実はこれも難しい処理ではありません。 | で結合された、前方がばらばらの複数の正規表現を、二重ループで後ろが一致するものを束ねればよいのです。 もしかしたら、うまく実装したらオーダーが下がるかもしれませんが、最初は素朴に実装するのがよいでしょう。


最初に前方をまとめてしまい、その後に後方をまとめる処理を行えばよいはずです。 文字列として最短の正規表現を生成することはゴールではありません。 前方一致を優先させた正規表現を作ればよいはずです。

例えば、最初に例としてあげた a(?bc(?:de|fg)|fcfg) は、 a(?:bcde|[bf]cfg) と書くほうが文字数は少なくなります。 これは後方一致を優先させた結果ですが、 a を見た後に複数の分岐があり、それらがいずれも b を確認する必要があります。 もう少しわかりやすい例をあげると、

a00
a01
a10
a11
a22
b00
b01
b10
b11

これを前方一致でまとめると (rassemble-goの出力結果は)

a(?:[01][01]|22)|b[01][01]

となりますが、

[ab][01][01]|a22

のほうが短いですよね。 しかし、こちらの「短い」正規表現は、先頭の文字が a かどうかを複数の分岐で行っています。

解きたいのは、短い正規表現を吐くことではなく、前方一致を優先させてまとめ、程々に後方もまとめた正規表現を生成することなのです。 ですから、前方一致を貪欲にまとめていけば良いのです。 本家のソースコードをしっかりと確認したわけではありませんが、この実装で概ね本家と同じ正規表現を生成できています。

 % rassemble 'ab*c*d?' 'abcc*d?' 'ad?'
a(?:(?:b*|bc)c*)?d?   # c*, d? がまとまっている
 % rassemble abacination abaction abalienation abarticulation abbreviation abdication abduction aberration
ab(?:a(?:c(?:ina)?|(?:lien|rticul)a)|(?:brevi|err)a|d(?:ica|uc))tion # tion がまとまっている

もしかしたら後方はまとめなくてもいいんじゃないかと思われるかもしれません。 しかし、実際に後方をうまくまとめないと、冗長な正規表現になってしまうパターンは存在します。

abcde
acde
abde
abce
abe
ace
ade
ae

これを後方をまとめる処理を行わないと、

a(?:b(?:c(?:de|e)|de|e)|c(?:de|e)|de|e)

となりますが、後方をまとめることで

 % rassemble abcde acde abde abce abe ace ade ae
ab?c?d?e

とすることができます。 かなり簡潔になりました。 これがうまく動いたときは感動しました。 かなり恣意的な例ですが、後方をまとめないと指数関数的に長くなってしまう (圧倒的にまとめられる) 正規表現はあるのです。

実際にはここまで極端な例はあまりないと思いますが、都道府県を結合するときもなんとなく「県」はまとまってほしいわけです。 上記にrassemble-goを使ったときの出力を書いておきましたが、実際に多くの県がまとまっていますね。 前方一致を優先させることから、「大分県」が他の県と結合せずに「大阪府」とまとまっています。 前方一致としては 山[口形梨]|福[井岡島]、後方一致としては (?:富|和歌|岡)山(?:神奈|石|香)川(?:[広徳]|鹿児)島 のようにまとまっています。 地名が山や川や島でまとまるのは納得感があります。 福島が島じゃなく福でまとまっているのは前方一致優先ですね。

ところで、上記の例 (a(?:[01][01]|22)|b[01][01]) をRegexp::Assembleにわたすと次のような形になります。

a(?:0[01]|1[01]|22)|b(?:0[01]|1[01])

のようになり、 [01] の部分が冗長になっています。 このように、rassemble-goはいくつかのケースでRegexp::Assembleと動作が異なることが分かっています。

 % rassemble ab?c ac
ab?c # Regexp::Assembleではa(?:b?)?c
 % rassemble ab+c ac
ab*c # Regexp::Assembleではa(?:b+)?c
 % rassemble abcde abc bbcde bbc cbcde cbc
[a-c]bc(?:de)? # Regexp::Assembleでは abc(?:de)?|bbc(?:de)?|cbc(?:de)?

冗長な ? の除去や、後方一致の深い判定はやってないようですね。 でも最後の例なんかは、rassemble-goのようにまとめたほうがよい気がします。


Go言語で実装した時に一番悩んだのは、実は文字列の扱いです。 まず、正規表現を自前でパースするのは骨の折れる事ですから、regexp/syntaxパッケージを使うことは最初から決めていました。 このパッケージでパースすると以下のような構造体が返ってきます。

type Regexp struct {
    Op       Op // operator
    Flags    Flags
    Sub      []*Regexp  // subexpressions, if any
    Sub0     [1]*Regexp // storage for short Sub
    Rune     []rune     // matched runes, for OpLiteral, OpCharClass
    Rune0    [2]rune    // storage for short Rune
    Min, Max int        // min, max for OpRepeat
    Cap      int        // capturing index, for OpCapture
    Name     string     // capturing name, for OpCapture
}

例えば abcde という連続する文字列は、

&syntax.Regexp{ Op: syntax.OpLiteral, Rune: []rune("abcde") }

のような形になっています。 これは一文字ずつの木を作りたい時には若干不便です。

例えば、abcd*e?という正規表現abc, d*, e? という3つの *syntax.Regexp になります。 これと abef*g* を結合すると、 ab(?:cd*e?|ef*g*) のようになります。 これは、abcabe を比較して、2文字目まで一致するのでそこまでを切り出して、残りの文字列からまた新しい *syntax.Regexp を作る必要があります。 一文字ずつの構造体になっていれば、このような複雑さはないはずです。

この構造体から自前の構造体に変換しようか結構悩みましたが、今はまだ *syntax.Regexp のまま計算しています。 そして、上でトライ木と述べましたが、実は *syntax.Regexp のまま構文木を操作しています。 一文字ずつに分割したほうが正規表現を表す構造としてきれいですし、一致判定のコードは統一的に書けるでしょう。 しかし *syntax.Regexp 相当の構造体を自前で実装するのは大変です。 構造体に埋め込んで一文字だけを特別扱いするとか、一文字ずつの *syntax.Regexp に分割するとか色々考えましたが、結局それを分割するのにもオーバーヘッドがありますし、コードのきれいさがそこまで見合わないだろうと判断しています。 また、多くの (少なくとも入力の文字列長以上の) 構造体を確保してしまうという、パフォーマンスの懸念もあります。 なので、今の実装の文字列の一致判定は []rune 同士をたどっています。


正規表現をまとめるからには、まとめた正規表現のパフォーマンスが気になるところです。 実際に /usr/share/dict/words の先頭100件、500件、そして1000件をrassemble-goで結合した正規表現と、 | で結合した正規表現を使って、/.../words の先頭2000件の単語をマッチ (MatchString) させたときのパフォーマンスを比較してみました。

BenchmarkRassembleJoin100-16          7065            164284 ns/op
BenchmarkSimpleJoin100-16             1237           1051659 ns/op

BenchmarkRassembleJoin500-16          1634            651378 ns/op
BenchmarkSimpleJoin500-16              224           5405078 ns/op

BenchmarkRassembleJoin1000-16         1112           1038448 ns/op
BenchmarkSimpleJoin1000-16              98          10608690 ns/op

やはりGoの正規表現でも、きちんと先頭をまとめた正規表現を使ったほうが速いことが分かりますね。 苦労して実装したものが実は全く意味なかったとしたらがっくりだったのですが、意味がありそうでよかったですね。

ただし、上記のベンチマークではrassemble-goで正規表現を結合するコストを無視しています。 特に後方一致の計算には時間がかかります (時間がかかると言っても /usr/share/dict/words の全件 (235886単語、2493109バイト) を結合するのに0.6秒くらいです、10000単語なら0.05秒以下で終わります)。 前処理とかプログラムで一回だけ走る箇所に使うのは大丈夫ですが、高いパフォーマンスが求められる箇所でのご利用はお控えください。 そもそも、そういう場面でGoの正規表現を使うのは間違っているのですが…


Regexp::AssembleのGo実装 rassemble-go を作りました。 この休日にがっつり集中して書いたのですが、実装していてめちゃくちゃ楽しかったです。 休日は溶けました。 今はかなり愚直な実装になっているので、もう少しパフォーマンス改善できないか考えているところです。 バグがあったら報告してください。

lightline.vimがGitHub 5000スター達成

先日、Vimプラグインlightline.vimGitHubで5000スターを達成しました。

Stargazers over time

このプラグインを世に送り出してから丸七年になりました。本当に多くの方に使っていただいており、とてもとてもありがたいことです。

設定はユーザーに書いてもらうという形のプラグインなので、issueには設定方法に関する問い合わせがたくさん来ます。機能要望が来ることもたまにありますが、そのほとんどは多くの人に利益となることがないのでrejectしています。バグ報告はかなり少なく (7年で26件)、バグが見つかると逆にレアアイテムを見つけたかのように嬉しくなります。時にはVimに新しい機能を追加してもらってプラグインの「バグ」を直すこともあります。

また、Vimがバグってその影響を受けることもありました。range() 関数がバグったときは驚きましたね。

当初の設計は今も変わっておらず、lightline.vimのコードは小さく保っています。コーディング中、ステータスラインは常に視界に入る重要な場所です。今もなお私にとって重要なツールであり、それはこれからもずっと変わらないと思います。

これからもよろしくお願いします。

Go言語で日時と文字列を相互変換するライブラリtimefmtを作りました

Go言語でstrftimestrptime相当の関数を提供するライブラリを実装しました。

t, _ := timefmt.Parse("2020/07/24 09:07:29", "%Y/%m/%d %H:%M:%S")
fmt.Println(t) // 2020-07-24 09:07:29 +0000 UTC

str := timefmt.Format(t, "%Y/%m/%d %H:%M:%S")
fmt.Println(str) // 2020/07/24 09:07:29

str = timefmt.Format(t, "%a, %d %b %Y %T %z")
fmt.Println(str) // Fri, 24 Jul 2020 09:07:29 +0000

なぜ作ったか

Go言語の標準ライブラリには日時と文字列を変換する関数がありますが、2006年1月2日の15:04:05でフォーマットを指定するという独自の仕様をとっています。 しかしPythonRubyのようにstrftime(3)strptime(3)のフォーマットをGo言語でも使いたいという人は多く、様々なライブラリが作られてきました。

これらの先人には敬意を表しますが、どのライブラリにも満足できないところがありました。

  • %F %T%r といった複数の情報を含むものが実装されていない
  • %-y-%-m-%-d%_y-%_m-%_dのようにpaddingを消したりスペースにしたりすることができない
  • %10A, %10B %2k:%Mのような幅の指定、%^a %^bのような大文字への変換ができない
  • cgoを使っており、クロスビルドできない
  • 文字列への変換と文字列からの変換は、同じライブラリーで提供したい
    • strconv.Atoistrconv.Itoaがあるように

どのライブラリを改善するにも微妙なところがあり、自分で作ろうと思い至ったわけです。

以上が表向きの理由ですが、本当の理由はgojqに必要だったからです。 jqにはstrftimeとstrptimeがありますので、これをgojqで実現するためにはGo言語でも同じ関数が必須です。 つまり単に日時と文字列を変換したいわけではなくて (それなら標準ライブラリを使えばよい)、strftimestrptimeそのものが必要だったのです。 これまでlestrrat-go/strftimepbnjay/strptimeを使っていました。 lestrrat-go/strftimeにはほとんど不満はありませんでしたが、pbnjay/strptimeは機能的に不十分なこと (%cでパースできないなど) や、古いライブラリでライセンスファイルが置かれていないことなど不安要素が多く、新しいライブラリを作るのには十分な理由となりました。

timefmtは、Parse(source, format string)Format(t time.Time, format string)を提供しています。 strftime(3)strptime(3)のほぼ全てのフォーマット指定子、paddingの調整や幅の指定に対応しています。

パフォーマンス

timefmtライブラリを作り始めたときは、パフォーマンスはそこまで重視していませんでした。 strftimeのライブラリのベンチマーク結果は以下のようになっています。

多くのフォーマットで現状最速のライブラリだと思います。 以下の方針で実装していくと、それなりの速度がでました。

  • フォーマット指定文字列をone-passで辿る
  • 月日や時刻など二桁以下の数字の文字列化を高速化する
  • bytes.Bufferではなく[]byteを使う
  • 文字列の結合を避け、strconv.AppendIntを使う
  • モリーの確保を極力減らす

他のライブラリでは標準ライブラリのフォーマットに変換していたり、メモリー確保に気を使っていなかったりしていました。 標準ライブラリは二文字か三文字読まないとどの指定子か分からないのが遅くなる原因だと思います。 この点はstrftimeのフォーマットは優れていますね。

timefmtはこれ以上チューニングする予定はありません。 今のtimefmt.Formatが遅いという場面では、fmt.Sprintfのような関数も使ってはいけないほどパフォーマンスに厳しい場面でしょう。 loggerのように時刻を固定フォーマットで高速に出力する場面では、フォーマット指定文字列を使わずに直接手で書いてしまえば良いでしょう。 このように気楽に構えておくことによってパフォーマンスチューニングの沼に踏み込みすぎないというのは、ライブラリをシンプルに保つ一つのコツだと思います。

まとめ

Go言語で時刻と文字列を相互に変換するライブラリtimefmtを作りました。 作っておいてなんですが、ほとんどの場面では標準ライブラリを使っておくと良いでしょう。 strftime自体が欲しい場面や、標準ライブラリより少し速いライブラリが欲しいときに役に立つと思います。 gojqへの組み込みは成功し、jqとの互換性は上がったので満足しています。

strftimeには多くの指定子があり、なかなか覚えられない方もいらっしゃるかと思います。 最低限 %Y-%m-%d %H:%M:%S をそらで書けるようになりましょう。 追加で %a %b %e %I %p %Z あたりを覚えておくと便利です。 今回timefmtを作ったことで全て覚えてしまいました。 やはり再実装は深い理解への近道ですね。