Blog::kobaken

prove t/foo/bar/baz.t

ISUCON13で、参考実装のPerl移植をしました

こんにちは。kobakenです。

ISUCON13で、Goの参考実装からPerlへの移植をしました。ISUCON10から数えて、4年目の移植でした。貴重な機会をありがとうございます!

ここでは、今回の移植で工夫したことを書きます。

競技の公平性のためGoのオリジナル実装に忠実に移植しつつ、Perlらしさもあり、わかりやすく、モダンで、けど、モダンになりすぎないコードにしたいと思って移植をしました。欲張りですね。

※941さんのリプライは全く別文脈です。

class構文をいれてしまった

Perlにはv5.38からbuiltinのclass構文が入りました。まだ実験的でISUCONに使うには早いと思い、移植を始めた当初入れる気はありませんでした(ホントに)

けど、いれてしまいました。

3つ理由があります。

JSONエンコード時に細やかな変換をしつつ、処理速度を出したかったから

1つ目の理由は、ISUCON12本戦問題にまで遡ります。そのPerl実装で次のようなコードが入っていましたJSONエンコード時に、キーをcamelizeだったりしています。

# JSONレスポンスのキーを正規化する
#
# おこなうこと:
# 1. snake_caseのキーを、lowerCamelCaseのキーに変換する
#   例: { user_id => 123 } -> { userId => 123 }
#
# 2. データとJSONレスポンスのキー名が異なる場合に変換する
#   例: { user_card_id_1 => 123 } -> { cardId1 => 123 }
sub normalize_response_keys($data) {

    state $key_mapping = {
        user_card_id_1 => 'card_id1',
        user_card_id_2 => 'card_id2',
        user_card_id_3 => 'card_id3',
    };

    my $ref = ref $data||'';
    if ($ref eq 'HASH') {
        my $ndata = {};
        for my ($key, $val) ($data->%*) {
            my $nkey = lcfirst(camelize($key_mapping->{$key} // $key));
            my $nval = normalize_response_keys($val);
            $ndata->{$nkey} = $nval;
        }
        return $ndata;
    }
    elsif ($ref eq 'ARRAY') {
        return [ map { normalize_response_keys($_) } $data->@* ];
    }
    else {
        return $data;
    }
}

これはJSONエンコードを遅くするんですよね。競技の本筋じゃない箇所で遅くなるのは嫌なのですが、ISUCON12の時はPerl実装を大きく書き換える必要があり、泣く泣く諦めいれました。今回のISUCON13で同じ失敗をしたくなかったのと、結果的には無くなったのですが、ISUCON13でも微妙にcamelise, normalizeしないといけなさそうな箇所があり、悩んだのですが、JSONモジュールのconvert_blessedオプションを用いて、細やかな調整ができつつ、処理速度も悪くない方法を取ることにしました。

use v5.38;
use experimental qw(class);

class Isupipe::Entity::Theme {
    field $id :param = undef;
    field $user_id :param = undef;
    field $dark_mode :param = undef;

    method TO_JSON() {
        return {
            id        => $id,
            dark_mode => bool $dark_mode,
        }
    }
}

このTO_JSONのただ一つ嫌なことは、最初に書くことが面倒なことです。が、それは競技者の問題じゃなく、移植者の自分だけの問題ですし、量産コードが得意なCopilotがいい感じにサジェストしてくれました。

class構文で作られるオブジェクトが、blessで作るオブジェクトよりメモリ効率・処理速度も良かったから

2つ目の理由は、class構文で作られるオブジェクトが、blessで作るオブジェクトよりメモリ効率・処理速度も良かったからです。Perlにはクラスビルダーモジュールがたくさんありますが、それらとclass構文でパフォーマンス比較するベンチマークを以前取りました。

kfly8.hatenablog.com

すると、class構文はメモリ効率が最もよく、アクセサも配列リファレンスにblessした時と同等でした。ハッシュリファレンスを生で扱う方がパフォーマンスが良いのですが、悪い選択ではないだろうと。

えいや

理由を言語化していくと、3つ目は自分の思い切りだと感じてきました。1つ目の理由に記載の通り、normalize処理で遅くなるのを避けてるにも関わらず、オブジェクトを利用したら生のハッシュリファレンスを扱うより遅くなるわけで、一貫してないんですよね。

「class構文を利用したことがなくても修正は自明だろうし、もし剥がすにしても微修正で済むし、そもそもここがボトルネックになるなら優勝……」と、言い訳を並べ、えいや!といれてしまいました。

...

そんなこんなで、class構文を採用しました。将来的には胸を張っていれられるようにPerl本体の進化に期待です。

余談ですが、オブジェクトを作ることを前提にするならと、DBIx::Sunnyにはselect_row_asのような糖衣構文も入れてもらいました。ISUCONとは関係ないプロジェクトでも似たAPIを用意していて悪くはないだろうなーと。

# BEFORE
my $user_row = $dbh->select_row('SELECT * FROM users LIMIT 1');
unless ($user_row) {
   return NOT_FOUND
}
my $user = User->new($user_row->%*);

# AFTER
my $user = $dbh->select_row_as('User', 'SELECT * FROM users LIMIT 1');
unless ($user) {
   return NOT_FOUND
}

オプションで防御をいれた

class構文を入れるだけの話にダラダラと書いてしまいました。それ以外に、しれっと、Type::TinyとData::Lock をいれています。期待に反する値を保存しようとしたり、期待に反するフィールドにアクセスしようとした場合に、エラーになるようにしています。ただし、本番環境で取り除けるようにしています。

具体的には、こんな内容です。

# Type::Tiny
method id($new=undef) {
    # Intかどうかチェック
    if (defined $new) { assert_field(Int, $new); $id = $new) };
    return $id;
}
$user->id('hello'); # Runtime Error
# dlock
use Data::Lock qw(dlock);
my $params = { user_id => 123 };
dlock($params);
$params->{use_id} # Runtime Error

期待に反する状態になってもエラーにならず処理を続けた場合、ベンチマーカーが丁寧なフィードバックをしてくれなかったりするので、せめて実行時に気づけるようにしたいなーと思い、書きました。

簡易テストを用意し、2ヶ月前に移植をはじめた

競技とは関係ないですが、移植作業の工夫を一つ。

今回、簡易テストを用意して、移植を進めました。今までの移植では、コードを書く⇄ベンチマーカーを回す、の繰り返しで作業を進めてましたが、今回はエンドポイントを一つ書く度、レスポンスを確認するテストを書き、ベンチマーカーを使わず移植作業を進めました。もちろん最終的にベンチマーカーが通るように調整をします。例えば、このGET /api/livestream/{livestream_id:[0-9]+}/livecomment のコミットでは、コードの修正とテストの修正を同時にいれています。

github.com

こんな進め方をした理由は個人的なもので、ISUCON13 本番直前の短期間にまとまった時間を確保することが難しそうで、2ヶ月前、オリジナル問題やベンチマーカーがだいぶ生煮えの時期から少しずつ作業を進めたからです。オリジナル問題やベンチマーカーがほぼ出来上がってる時は、右から左へ移植する感じで良いですが、着手をはじめた9/30(土) だとそうもいかなかったです。

結果、この進め方は、正解だったなーと。まず開発体験が良かったです。テストしたいケースを集中してイジメられて良かったです。それと、着手が早かったので、気持ちにゆとりがありました。例えば、Kossyで、JSONリクエストを扱うためのメソッドを生やしたりルーティングの安定化をしたり、ことのついでにサポートバージョンを5.14からにしてもらったりしました。kazeburoさん取り込みありがとうございました!


最後に

ISUCONのお手伝いが少しでもできて良かったです!ISUCONって素敵なイベントですよね!ありがとうございました!

次は YAPC::Hiroshima 2024の運営!エンジニアみな広島に来てほしい!!