本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはバウンスメール解析ライブラリSisimai (シシマイ:注1 )を開発している東邦之さんで、テーマは「Perlコードの高速化」です。
本稿のサンプルコードは、本誌「WEB+DB PRESS Vol.110」のサポートサイト から入手できます。
コードは遅くなる
ソフトウェアは機能の追加やバグの修正によって次第にコードが膨れ、実行速度が遅くなる傾向にあります。YAGNI(You ain't gonna need it 、それが必要になったときに実装せよ)の原則やKISS(Keep it simple, stupid 、簡潔にしておくべし)の原則に従っているつもりでも、プロジェクトの進捗や仕様の変更に伴って複雑化し、遅くなることが多々あるでしょう。
本稿では、筆者が開発し、オープンソースで公開しているSisimaiを改善していく中で、コードの高速化に寄与した書き方を、文字列処理とデータ構造の選択を中心にベンチマークと合わせて紹介します[2] 。
速度と保守性を考える
まず、速度と保守性について考えましょう。
極端な例を出します。超高速に動くが誰も読めない魔術的なコードと、明瞭で誰でも読めるが実用に耐えない遅いコードは、どちらが優れているでしょうか。
前者は速度において優れている、後者は保守性において優れている、と言えます。一方、前者は保守性において劣る、後者は速度において劣る、とも言えます。
速度を求める
商業的見地から、保守性を犠牲にしてでも速度を求めたいことがあるでしょう。100ミリ秒単位で課金されるFaaS(Function as a Service )( 注3 )では、特に速いは正義 が強調されるかもしれません。しかし、正義は絶対的なものではなく相対的なものです。あらゆる環境において速さがすべてとは言い切れないでしょう。
保守性を求める
コードの読みやすさは慣れや経験に基づく個人差があります。組織やチームのコーディングルールで禁止されている書き方もあるでしょう。とはいえ、読みやすさにおいて個人間の差はそう大きくないと筆者は考えます。速度を著しく犠牲にしてでも保守性を求める、これも、常に良いとは言えません。
速度と保守性は両立できる
速くて読みやすいコードは存在します。いかなる状況においても、と断言はできませんが、速度と保守性は対立するものではなく、両立できるケースが多いです。コードそのものを見なおし、保守性につながる読みやすさと性能につながる速度や効率を天秤にかけて、状況に適した書き方を選ぶことが肝要です。
コードを速くする前にまず計測
では、保守性を損なわずにコードを速くするにはどうすればよいのでしょうか。幸いPerlはTMTOWTDI(There's more than one way to do it 、やり方は一つだけではない)のモットーが示すとおり、さまざまな書き方ができる言語です。つまり、今動いているPerlのコードは書き方を変えれば速くなる余地を残しています。高速化のためにアルゴリズムやロジックを変えるより、極端に言えば行単位の変更で速くなる希望があります。
しかし、やみくもに書き方を変えればよいわけではありません。まずは計測 ありきで、2個か3個の違うやり方でベンチマークを複数回とりましょう。計測なくして速度や効率の改善はありません。
部分を競うベンチマーク
ベンチマークは、正規表現を固定文字列に変える、別の関数を使うといったコードの一部分の変更を書き換え前後で競わせます。具体的には、Benchmark
モジュールとTest::More
モジュールを使ったベンチマークコードを書き、実行速度を比較します。データ構造を変更した場合は、Devel::Size
モジュールを使ってメモリ使用量の違いも見ます。
複数回実行したベンチマーク結果の中央値[4] を見て、高速化が期待できるなら、プログラム本体も部分として速いコードに書き換えます。そして、書き換えた状態でテスト[5] が通れば[6] 、負荷が一定値以下であること、メモリが一定量以上空いていることなどの条件を揃えて、Devel::NYTProf
モジュールで複数回プロファイリングを実行します。全体でも高速化ができていれば、書き換えたコードを採用します。
使用したPerlのバージョン
本稿のベンチマークは、次の2つのPerlで実行しました。
Perl 5.18.2(2014年1月リリース)
Perl 5.28.1(2018年12月リリース)
Perl 5.18.2はmacOS Sierraの/usr/binにインストールされているもの、Perl 5.28.1は執筆時点2019年3月での最新版で、同OSにソースビルドでインストールしたものです。
ただし、いずれのバージョンでもベンチマーク結果の傾向は同じでしたので、本稿に掲載するベンチマークは最新版のPerl 5.28.1 での結果のみです。Benchmark
モジュールが出力した結果は、本誌サポートサイトで配布するサンプルコードファイルの__END__
以降にそのまま載せていますので、Perl 5.18.2での結果を知りたい人はそちらで確認してください。
誌面で省略した部分
誌面のコードには、すべてのベンチマークコードで共通する部分と、酷似した部分は含めていません。コード全体を確認したい場合は、本誌サポートサイトで配布するサンプルコードを見てください。
具体的には、次のコードを省略しています。
コード冒頭のuse文
ベンチマークコードの冒頭では、いずれも次のshebang[7] とuse
文が書いてあるものとします。
#!/usr/bin/env perl
use strict;
use warnings;
use Benchmark ':all'; # ベンチマーク用モジュール
use Test::More 'no_plan'; # 比較対象結果の検査で使う
コード末尾の検証と実行部分
ベンチマークコードの末尾では、いずれも次のTest::More
モジュールのisによる結果の検証と、実行したPerlのバージョン表示、そしてBenchmark
モジュールのcmpthese
によるベンチマーク実行コードが書いてあるものとします。
is ss($Email), 'neko@nyaan.jp';
is yy($Email), 'neko@nyaan.jp';
printf("Perl %s on %s\n%s\n", $^V, $^O, '-' x 50);
cmpthese(6e6, {
's///' => sub { ss($Email) },
'y///' => sub { yy($Email) },
});
環境構築と実行方法
Benchmark
モジュールとTest::More
モジュールはPerlのコアモジュールですので、インストールは不要です。メモリ使用量を測るDevel::Size
モジュールと、プロファイリングに使うDevel::NYTProf
モジュールは、cpanm
モジュール名というコマンドを実行して、それぞれインストールしてください。
本誌サポートサイトから入手できるベンチマークコードは、次のように実行します。
$ perl ./s-vs-y.pl
Rate s/// y///
s/// 928793/s -- -77%
y/// 4054054/s 336% --
Benchmark
モジュールが出力する結果は、左からBenchmark::cmpthese
に指定した表示名(s///
、y///
) 、秒間実行回数(928793/s
、4054054/s
) 、速度比(-77%
、336%
)となり、最も遅かったものを基準に、下に行くほど速く動作したコードとなります。本稿ではベンチマーク結果を、同じ並び順で表として掲載しています。
<続きの(2)はこちら 。>
特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT