Masteries

技術的なことや仕事に関することを書いていきます.

PerlのJSONとJSON::PPとJSON::XSと

結論

JSON::XSが既にインストールされているなら, 明示的にJSON::XSを使わず, JSONを使ったとしても, あまり速度は変わらない.

JSONとJSON::PPとJSON::XS

PerlでJSONを扱う時に使えるモジュールの1つがJSONモジュールです.

metacpan.org

JSONモジュールは, 実際にJSONをdecode/encodeする処理を差し替えることができます. デフォルトではPure Perl実装のJSON::PPが使われ, インストール済みで利用が可能であればXSを使って実装されたより高速なJSON::XSを使うようになっています.

metacpan.org

metacpan.org

JSON (+ JSON::XS) vs JSON::XS

JSON::PPとJSON::XSなら, 当然JSON::XSを使う方が高速です. では, JSONとJSON::XSならどうでしょう. JSON::XSがインストール済みなら, JSONでもJSON::XSを使ってdecode/encodeをするはずですが, 実はそれ以外の処理があったりして, JSON::XSを直接使う方が早い... ということはないのでしょうか?

use strict;
use warnings;

use Benchmark qw/timethese cmpthese/;

use JSON ();
use JSON::PP ();
use JSON::XS ();

my $result = timethese(10000000, {
    json => sub {
        JSON::encode_json({a => 1});
    },
    xs => sub {
        JSON::XS::encode_json({a => 1});
    },
    pp => sub {
        JSON::PP::encode_json({a => 1});
    },
});

cmpthese $result;

というわけでさっくりベンチを取ると...

perl bench.pl
Benchmark: timing 10000000 iterations of json, pp, xs...
      json:  3 wallclock secs ( 1.96 usr +  0.02 sys =  1.98 CPU) @ 5050505.05/s (n=10000000)
        pp: 25 wallclock secs (24.03 usr +  0.13 sys = 24.16 CPU) @ 413907.28/s (n=10000000)
        xs:  3 wallclock secs ( 2.02 usr +  0.02 sys =  2.04 CPU) @ 4901960.78/s (n=10000000)
          Rate    pp    xs  json
pp    413907/s    --  -92%  -92%
xs   4901961/s 1084%    --   -3%
json 5050505/s 1120%    3%    --

...JSON::PPとの差は歴然ですが, JSON::XSと(内部でJSON::XSを使っている)JSONの間には, あんまり差はないですね.

余談

ちなみに, JSONモジュールは PERL_JSON_BACKEND モジュールで利用する実装を指定することができます. 例えば, PERL_JSON_BACKEND=JSON::PP として, 先程のスクリプトを実行すると...

$ PERL_JSON_BACKEND=JSON::PP perl bench.pl
Benchmark: timing 10000000 iterations of json, pp, xs...
      json: 23 wallclock secs (23.85 usr +  0.25 sys = 24.10 CPU) @ 414937.76/s (n=10000000)
        pp: 24 wallclock secs (24.10 usr +  0.22 sys = 24.32 CPU) @ 411184.21/s (n=10000000)
        xs:  3 wallclock secs ( 2.08 usr +  0.01 sys =  2.09 CPU) @ 4784689.00/s (n=10000000)
          Rate    pp  json    xs
pp    411184/s    --   -1%  -91%
json  414938/s    1%    --  -91%
xs   4784689/s 1064% 1053%    --

先程と違ってJSONはJSON::PPを使っているので, JSON::XS > JSON::PP = JSON, という結果になりました.

忘備録: Smart::Args::TypeTinyのdefaultとoptionalについて

metacpan.org

Smart::Args::TypeTinyには, defaultoptional というパラメータがあります.

default

default は, 引数が渡らなかった時のデフォルト値を指定するパラメータです.

use strict;
use warnings;

use feature qw(say);
use Smart::Args::TypeTiny qw(args);

sub test {
    args
        my $arg => { isa => 'Str', default => 'bar' };

    return $arg // 'undef';
}

say test(arg => 'foo'); # => 'foo'
say test(); # => 'bar' ( default が使われる )
say test(arg => undef); # => Type check failed in binding to parameter '$arg'; Undef did not pass type constraint "Str" (エラー)

なお, isaMaybe の場合はこうなります:

use strict;
use warnings;

use feature qw(say);
use Smart::Args::TypeTiny qw(args);

sub test {
    args
        my $arg => { isa => 'Maybe[Str]', default => 'bar' };

    return $arg // 'undef';
}

say test(arg => 'foo'); # => 'foo'
say test(); # => 'bar'
say test(arg => undef); # => 'undef'

まとめると, default のパラメータがあるときは,

  • 引数としてundef以外を渡す ... 渡した値が使われる (isaを満たさない場合は当然エラーになる)
  • 引数を渡さない ... default が使われる
  • 引数としてundefを渡す
    • isaMaybeである ... undef がそのまま使われる (default は使われない)
    • isaMaybeでない ... isa を満たさないのでエラーになる

...という挙動になります.

optional

一方, optional はその名の通り, オプショナルであることを指定するパラメータです.

use strict;
use warnings;

use feature qw(say);
use Smart::Args::TypeTiny qw(args);

sub test {
    args
        my $arg => { isa => 'Str', optional => 1 };

    return $arg // 'undef';
}

say test(arg => 'foo'); # => 'foo'
say test(); # => 'undef' (`optional` なので引数を渡さなくても問題ない. その場合は undef となる)
say test(arg => undef); # => 'undef' (明示的に引数として`undef`を渡しても問題ない)

まとめると, optional のパラメータがあるときは,

  • 引数としてundef以外を渡す ... 渡した値が使われる (isaを満たさない場合は当然エラーになる)
  • 引数を渡さない ... エラーにならず, undef として扱われる
  • 引数としてundefを渡す ... (isaを満たしていなくても)エラーにならず, そのまま undef として扱われる

defaultoptional

では, defaultoptionalのどちらも指定した場合はどうなるでしょうか.

use strict;
use warnings;

use feature qw(say);
use Smart::Args::TypeTiny qw(args);

sub test {
    args
        my $arg => { isa => 'Str', optional => 1, default => 'bar' };

    return $arg // 'undef';
}

say test(arg => 'foo'); # => 'foo'
say test(); # => 'bar' (`optional` なので引数を渡さなくても問題ない. その場合は `default` で指定された値となる)
say test(arg => undef); # => 'undef' (明示的に `undef` を渡しても問題ない)

この挙動, よくよく見ると2番目に紹介したdefaultを指定 + isaMaybeと同じですね. つまり1つの挙動(optionalな引数で, undefを受け取れて, デフォルト値を指定したい)について, 2通りの書き方がある, ということです.

papixの意見

...とはいえ同じ挙動を複数の書き方ができる, というのは混乱を呼びそうです. どちらかに寄せよう! という合意を取れるといいのかな, と思います. 自分なら, こういう場合は前者(defaultを指定 + isaMaybeにする)として, defaultoptionalは同時に使わない... というふうにするのがわかりやすいんじゃないかな, と思いました.

というわけで以上です, すいませんがオチはないです...

Perlの未定義動作100連発

PHPカンファレンス福岡2023に参加して高まってきたので, Perlの未定義動作100連発というエントリを書きます. なおperldocと実際の挙動については, 5.36.0のものを利用しています.

その1: スカラコンテキストでsort

sort はリストの中身をソートするので, 当然(ソートした結果を受け取るために)リストコンテキストで呼び出す必要があります. 「そんなことしないだろ...」と思いますが, もしスカラコンテキストで呼び出した場合の振る舞いは未定義です.

perldoc.jp

スカラコンテキストでは、sort の振る舞いは未定義です。

実際にやってみました:

コード

use strict;
use warnings;
use DDP;

my $sorted = sort { $a <=> $b } (1, 3, 2, 4);
p $sorted;

結果

Useless use of sort in scalar context at if.pl line 5.
undef

なんとなく, 「ソートした結果の先頭が入ってくるのかな...?」と思っていたけど, undef になるようでした. 「Useless use of sort in scalar context」という警告も出てきますし, まあこういうコードを書いてしまってそのままにしてしまう, ということは少ないでしょう.

その2: my + 後置if

詳しくは id:t_kyt *1さんの以下のエントリを読んでください:

developer.hatenastaff.com

perldoc.jp

注意: (my $x if ... のような) 条件構造やループ構造で修飾された my state,our 文の振る舞いは 未定義 です。 my 変数の値は undef かも知れませんし、以前に代入された値かも 知れませんし、その他の如何なる値の可能性もあります。 この値に依存してはいけません。 perl の将来のバージョンでは現在のバージョンとは何か違うかも知れません。 ここには厄介なものがいます。

実際にやってみました:

コード

use strict;
use warnings;
use DDP;

my $num1 = 123 if 1;
my $num2 = 345 if 0;

p $num1;
p $num2;

結果

123
undef

未定義動作ではありますが, 「まあそうなるよね」という結果になりました. とはいえ, 未定義動作なのでmyと後置ifは組み合わせないようにしましょう. ちなみに, 代入のない単なるmyに後置ifを書いた場合は次のようなエラーになります:

コード

use strict;
use warnings;
use DDP;

my $num1 if 1;

結果

This use of my() in false conditional is no longer allowed at if.pl line 6.

その3: forループの返り値

「forループの返り値...?」となりますよね. まずは perldoc を見てみましょう:

perldoc.jp

洞察力のある Perl ハッカーは、for ループに返り値があり、この値は ループを do ブロックで包むことによって捕捉できることに 気付くかもしれません。 この発見に対する報奨は、この警告的なアドバイスです: for ループの返り値は 未定義で、予告なしに変更されることがあります。 これに依存してはいけません。

というわけで, forをdoで包んで, 強引に返り値を取得してみましょう:

コード

use strict;
use warnings;
use DDP;

my $ret = do {
    for (1..10) {
        # ...
    }
};
p $ret;

結果

""

...というわけで, 強引にforの返り値を取得すると空文字列が返る, みたいです. 「forをdoで囲むことはないやろ...?」と思われるかもしれませんが, 次のコードは有り得そうです:

コード

use strict;
use warnings;
use DDP;

my $ret = func();
p $ret;

sub func {
    for (1..10) {
        # ...
    }
}

結果

""

この場合, 関数funcにはreturnがないので, 関数の返り値として一番最後に評価されるforの返り値が使われます. もし何かしらの理由で返り値を利用することになった場合, 意図せず空文字が返ってきてびっくり!!! ということになりそうです. こういう事例を防ぐためにも, 明示的にreturnを書くのがいいな, と最近思いつつあります.

コード

use strict;
use warnings;
use DDP;

my $ret = func();
p $ret;

sub func {
    for (1..10) {
        # ...
    }
    return undef;
}

結果

undef

いかがでしたか?

100連発といいつつ, まずは3連発でご紹介しました. Perlの未定義動作については, 今後また折を見て紹介していこうと思います.

*1:2023/06/28: 公開時, 記事の投稿者を誤って id:akiym さんと記載していました. お詫びして訂正いたします.

「吉祥寺.pm 33」でYAPC::Kyotoについて全てお話しました

吉祥寺.pm33【吉祥寺でのオフライン開催!!!】 - connpass

久々の吉祥寺.pmですね. 今回はYAPC::Kyotoを題材として, 勉強会運営の"リアル"みたいな(?)お話をしました.

ここ半年くらいは本当にYAPC::Kyoto 2023のことをずーっと考えていたので, こういう勉強会で発表する技術ネタの弾がないような気がしていて良くないですね. 次の吉祥寺.pmまでには残弾増やして披露したいなと思いました.

【Q】Tengでsingleを使って1件だけレコードを引くとき, 明示的に `LIMIT` を指定する必要はあるの?

【A】指定しなくても大丈夫です

github.com

実装を見ると, single を呼んだらその内部で自動的に LIMIT 1 になるように設定をしてくれます(強制的に上書きする). 逆に言うと, single を呼ぶ時に { limit => 100 } みたいなオプションを渡しても, LIMIT 100みたいなクエリは発行されず, LIMIT 1になるように上書きされる, ということです(まあ, そんなことする人いないでしょうが...).