Perl Hackers Hub

第55回 Perlコードの高速化―文字列処理の時間短縮とデータ構造の効率化(1)

この記事を読むのに必要な時間:およそ 2 分

本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーはバウンスメール解析ライブラリSisimai(シシマイ:注1を開発している東邦之さんで,テーマは「Perlコードの高速化」です。

本稿のサンプルコードは,本誌「WEB+DB PRESS Vol.110」サポートサイトから入手できます。

注1)
バウンスメール(エラーメールとも言う)を解析して構造を持つデータとして結果を出力するPerlモジュールです。

コードは遅くなる

ソフトウェアは機能の追加やバグの修正によって次第にコードが膨れ,実行速度が遅くなる傾向にあります。YAGNIYou ain't gonna need itそれが必要になったときに実装せよ)の原則やKISSKeep it simple, stupid簡潔にしておくべし)の原則に従っているつもりでも,プロジェクトの進捗や仕様の変更に伴って複雑化し,遅くなることが多々あるでしょう。

本稿では,筆者が開発し,オープンソースで公開しているSisimaiを改善していく中で,コードの高速化に寄与した書き方を,文字列処理とデータ構造の選択を中心にベンチマークと合わせて紹介します注2⁠。

注2)
バウンスメールは電子メールなので,処理のほとんどはメールアドレスの取り出しやエラーメッセージの照合などの文字列処理です。

速度と保守性を考える

まず,速度と保守性について考えましょう。

極端な例を出します。超高速に動くが誰も読めない魔術的なコードと,明瞭で誰でも読めるが実用に耐えない遅いコードは,どちらが優れているでしょうか。

前者は速度において優れている,後者は保守性において優れている,と言えます。一方,前者は保守性において劣る,後者は速度において劣る,とも言えます。

速度を求める

商業的見地から,保守性を犠牲にしてでも速度を求めたいことがあるでしょう。100ミリ秒単位で課金されるFaaSFunction as a Service注3では,特に速いは正義が強調されるかもしれません。しかし,正義は絶対的なものではなく相対的なものです。あらゆる環境において速さがすべてとは言い切れないでしょう。

注3)
AWS LambdaやAzure Functionsに代表されます。

保守性を求める

コードの読みやすさは慣れや経験に基づく個人差があります。組織やチームのコーディングルールで禁止されている書き方もあるでしょう。とはいえ,読みやすさにおいて個人間の差はそう大きくないと筆者は考えます。速度を著しく犠牲にしてでも保守性を求める,これも,常に良いとは言えません。

速度と保守性は両立できる

速くて読みやすいコードは存在します。いかなる状況においても,と断言はできませんが,速度と保守性は対立するものではなく,両立できるケースが多いです。コードそのものを見なおし,保守性につながる読みやすさと性能につながる速度や効率を天秤にかけて,状況に適した書き方を選ぶことが肝要です。

コードを速くする前にまず計測

では,保守性を損なわずにコードを速くするにはどうすればよいのでしょうか。幸いPerlはTMTOWTDIThere's more than one way to do itやり方は一つだけではない)のモットーが示すとおり,さまざまな書き方ができる言語です。つまり,今動いているPerlのコードは書き方を変えれば速くなる余地を残しています。高速化のためにアルゴリズムやロジックを変えるより,極端に言えば行単位の変更で速くなる希望があります。

しかし,やみくもに書き方を変えればよいわけではありません。まずは計測ありきで,2個か3個の違うやり方でベンチマークを複数回とりましょう。計測なくして速度や効率の改善はありません。

部分を競うベンチマーク

ベンチマークは,正規表現を固定文字列に変える,別の関数を使うといったコードの一部分の変更を書き換え前後で競わせます。具体的には,BenchmarkモジュールとTest::Moreモジュールを使ったベンチマークコードを書き,実行速度を比較します。データ構造を変更した場合は,Devel::Sizeモジュールを使ってメモリ使用量の違いも見ます。

複数回実行したベンチマーク結果の中央値注4を見て,高速化が期待できるなら,プログラム本体も部分として速いコードに書き換えます。そして,書き換えた状態でテスト注5が通れば注6⁠,負荷が一定値以下であること,メモリが一定量以上空いていることなどの条件を揃えて,Devel::NYTProfモジュールで複数回プロファイリングを実行します。全体でも高速化ができていれば,書き換えたコードを採用します。

注4)
極端に遅い,速い外れ値を除くために用います。
注5)
モジュールのtやxtディレクトリにあるテストコードです。
注6)
テストコードがあればコードの書き換えに対する心理的抵抗が軽減されます。

使用した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注7use文が書いてあるものとします。

#!/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) },
});
注7)
スクリプトを実行するインタプリタを指定する行です。

環境構築と実行方法

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/s4054054/s⁠,速度比-77%336%となり,最も遅かったものを基準に,下に行くほど速く動作したコードとなります。本稿ではベンチマーク結果を,同じ並び順で表として掲載しています。

<続きの(2)こちら。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.113

2019年10月24日発売
B5判/160ページ
定価(本体1,480円+税)
ISBN978-4-297-10905-9

  • 特集1
    接続エラー,性能低下,権限エラー,クラウド障害
    AWSトラブル解決
    原因調査・対応・予防のノウハウ
  • 特集2
    Ruby書き方ドリル
    要点解説と例題で身に付く!
  • 特集3
    体験
    ドメイン駆動設計
    モデリングから実装までを一気に制覇
  • 一般記事
    FigmaによるUIデザイン
    デザイナーとエンジニアがオンラインで協業できる!
  • 一般記事
    入門
    SwooleによるPHP非同期処理
    高速化のための並列実行はどのように書くのか

著者プロフィール

東邦之(あずまくにゆき a.k.a. azumakuniyuki)

データセンターには行かないインフラエンジニアとプログラマでPerlとRubyを書く。特にSMTPと縁がある。

2008年から株式会社Cubicrootに所属,2010年にbounceHammerを開発,2012年は大阪でのPerl入学式お手伝い,2014年にPerl版Sisimaiを,2016年にはRuby版Sisimaiを開発してOSSとして公開,現在も開発中。2018年は迷い猫を保護してお世話をした。

一年のうち360日は京都にいる。趣味は外猫の観察と酒場と銭湯。