Perl Hackers Hub

第64回 少しマニアックなPerlのテクニック―特殊変数,低レベルの標準関数を使いこなす(2)

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

前回の(1)こちらから。

高度なことをPerlだけで行うテクニック

Perlの基本的な機能をうまく活用するだけでも高度な処理を行えます。組込み関数はもちろん,一般的にperl本体と同時にインストールされるコアモジュール注1など,さまざまな道具がPerlには最初からそろっています。

これらの基本的な道具は書籍やWeb上に情報がすでに多くあるので,今回は使ったことがない人が多いと思われる少しマニアックな道具を紹介します。

注1)
CentOSではコアモジュールはperlとは別のperl-coreという別パッケージでインストールする必要があります。

任意のシステムコールを呼び出す

組込み関数のsyscallを使うと,任意のシステムコールを呼び出せます。システムコールは,カーネルの持つ機能を呼び出すためのしくみです。システムコールの一覧は,Linuxであればman syscallsなどで見られます。Perlを含む多くのプログラミング言語において,主要なシステムコールは組込み関数などで言語の機能として提供されています。また,組込み関数の中にはシステムコールのインタフェースとなっているものも多く存在します。

そのため,syscall関数を利用しなければ目的を達成できない場合は多くはありません。また,組込み関数になくともCPANモジュールとしてより使いやすい形で提供されているものも多いです。基本的にはそれらを利用して,OSの新しいシステムコールの機能をいち早く試したいときや,CPANモジュールを入れにくい環境で実行したい場合にsyscall関数を使うとよいでしょう。

システムコール番号を得る

syscall関数によるシステムコールの呼び出しには,システムコール番号が必要になります。システムコール番号を知るには,libc注2のヘッダファイルなどを参照する必要があります。これはOSごとに異なり,Ubuntuの場合はlinux-libc-devパッケージが必要です。次のようにインストールします。

$ apt-get install linux-libc-dev

C言語では,sys/syscall.hを読み込めば#defineによって定義されたSYS_で始まるシステムコール番号を示す定数を得られます。この定義をたどれば,システムコール番号が書いてあるファイルにたどり着けます。

Perlからシステムコール番号をロードできるようにする

Perlでシステムコール番号を扱うにはC言語のヘッダファイルをPerlから読める形式に変換する方法が一般的です。そのためには,h2phコマンドを使います。詳細な使い方はperldoc h2phを参照してください。

sys/syscall.hのロードできるディレクトリに移動して実行する
$ cd /usr/include/x86_64-linux-gnu # 環境による
$ h2ph -d path/to/lib -a -l sys/syscall.h

成功すれば,-dオプションで指定したパスに.ph拡張子のファイルが生成されます。たとえば,sys/syscall.hであればそれからsys/syscall.phが生成されます。これをrequireで読み込めば,C言語のヘッダファイル上で#definedによって定義された定数をPerlからも使えます。

もちろん,システムコール番号は単なる数値なので定数値をハードコードしても同じカーネルであれば同様に動作します。

sendfileシステムコールで高速にファイルをコピーする

実際にsyscall関数を使って,sendfileシステムコールを利用して高速にファイルコピーを行う処理を書いてみます。sendfileは,カーネル空間でファイル間のデータをコピーするため,ユーザー空間を経由するオーバーヘッドがなく,高速です。

Linuxにおいてはすべての入出力をファイルとして扱うため,TCP通信などでもsendfileを利用できます注3⁠。なお,Sys::SendfileなどのCPANモジュールでもsendfileを利用できます。

先述したh2phで生成したsys/syscall.phを読み込み,SYS_sendfile()に続けて引数を渡すことで,sendfileを呼び出します。

本稿のサンプルコードから重要な箇所のみ抜粋
require 'sys/syscall.ph';

my $size = -s $in_fh;
my $ret = syscall(
    SYS_sendfile(),  # システムコール番号
    fileno($out_fh), # システムコールの引数
    fileno($in_fh),  # (同上)
    0,               # (同上)
    $size,           # (同上)
);

sendfileにはファイルディスクリプタ注4を渡す必要があるので,fileno関数を用いてファイルハンドルからそれを得る必要があります。syscall関数は数値と文字列を扱えるので,これは問題なく実行できます。

注2)
C言語の標準ライブラリです。
注3)
実用上,sendfileシステムコールはファイルシステムなどによっては問題が発生する場合があるので,慎重に利用してください。
注4)
オープン済みのファイルを識別するための識別子となる数値です。

定型的な処理を高速化する

ちょっとした問題解決のためにスクリプトを書いていると,想定以上にパフォーマンスが必要で,チューニングの必要性に迫られる場面があると思います。ケースによりさまざまな要因と対処方法が考えられますが,ここではPerlの基本的な機能を利用した高速化に役立つテクニックを紹介します。

低レベルなファイルAPIを使う

Perlでは高レベルなファイルの取り扱いは行単位で行えますが,低レベルなAPIも備えています。低レベルなファイルAPIを使えば,Perlがファイルのどの位置から読み込むのかを変えたり,行単位ではなく一定バイト数を読み込んだりできます。そのため,場合によってはより効率的にファイルを扱えます。

例として,巨大なファイルの末尾の数行だけを読んで処理したいケースを考えてみましょう。Perlはファイルを先頭から行単位で読み込むので,末尾に至るまでの不要な行は捨てるとしても,巨大なファイルでは時間がかかります。ファイルの読み込みや書き込みを開始する位置をシーク位置と呼びますが,シーク位置を変えるには組込み関数seekを使います。また,一定バイト数を読み込むには組込み関数readを使います。

たとえば,末尾から1,024バイトを読み込むには次のようにします。

# SEEK_ENDなどの定数をインポート
use POSIX qw/:fcntl_h/;

# 末尾から1,024バイト前の位置に移動
seek $fh, -1024, SEEK_END
  or die "failed to seek: $!";

# $bufに1,024バイト分読み込む
read $fh, my $buf, 1024
  or die "failed to read: $!";

それなりに十分なサイズを読み込んで不要な部分を捨てれば数行分は得られるので,ファイルアクセスを最小限にとどめて効率良く目的が達成できます。

seekなどを応用すると,効率良くファイルを二分探索できます。cpanmなどでCPANモジュールのインストール時にモジュールをインデックスから探索する際に使われているSearch::Dictモジュールは,まさにこのテクニックを使っているので,参考にするとよいでしょう。

constantプラグマと定数畳み込みで処理を最適化する

Perlはインタプリタ言語ですが,定数畳み込みによる最適化も行います。定数畳み込みとは,実行の前に定数を単純化する最適化手法です。たとえば,3+4と定数だけで書かれているPerlコードは,7に畳み込まれます。

定数だけで完結する分岐も,定数畳み込みにより事前に展開されます。以下に例を示します。

常に真なのでifが消えてブロックの内側の処理のみ残る
if (1) {
    print "Always run it!";
}

常に偽なのでifがブロックごと消える
if (0) {
    print "Remove it!";
}

これが定数畳み込みで最適化されると,次のようになります。

print "Always run it!";

このように,定数畳み込みによって実行時の条件分岐をなくすことができるケースがあります。

この定数は,リテラルとしての定数だけではなく,constantプラグマで作った定数にも有効です。constantプラグマは定数注5を定義するためのプラグマです。これを使えば,実行時に渡した環境変数などを使って定数を作れます。

効果的な場面は限定的ですが,わかりやすい例として,デバッグログの最適化を考えてみましょう。デバッグログはデバッグのためにあらゆる箇所に埋め込む必要がありますが,スクリプトが処理するデータ量に比例してログ出力の処理も増えると,本番のデータに対しては膨大なログを出すことになります。そのため,デバッグ出力の有無をフラグで管理することになりますが,実行中にデバッグ出力の有無を切り替えることはないので,その条件分岐を各所でそれぞれ行うのは無駄です。デバッグ出力の有無を決めるフラグを環境変数にしてconstantプラグマで定数にすることで,定数畳み込みの恩恵が受けられます。

use constant DEBUG => $ENV{DEBUG};

# 環境変数の値に応じて定数畳み込みが行われる
printf STDERR "DEBUG: ..." if DEBUG;

これを応用すれば,デバッグなどの一部のケースでしか利用しない処理・分岐を実行前に最適化できます。

注5)
実際には定数サブルーチンと呼ばれる引数を取らない特殊なサブルーチンになります。

まとめ

Perlの基本機能をうまく活用することで,より簡潔に記述するテクニックがあること,CPANモジュールに頼らずとも高度なことができることを示しました。Perlの機能をよく理解し適材適所で応用すれば,さまざまな環境での仕事を楽にできます。これらのテクニックのほとんどはアプリケーション開発など長期的にメンテナンスするコードには向きませんが,書き捨てのスクリプトやワンライナーでは役に立つでしょう。

さて,次回の執筆者はhitode909さんで,テーマは「cpanfileのアップデート」です。お楽しみに。

WEB+DB PRESS

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

2021年6月24日発売
B5判/168ページ
定価1,628円
(本体1,480円+税10%)
ISBN978-4-297-12207-2

  • 特集1
    ついに登場!!! より速く,より安全に,より効率的に
    HTTP/3入門
  • 特集2
    Reactベースの柔軟・省設定フレームワーク
    いまどきNext.js
  • 特集3
    脆弱性への緊急対応の手立て
    実践WAF

著者プロフィール

佐藤健太(さとうけんた)

1990年,千葉県生まれ。DeNAにてソフトウェア開発及び運用に従事,Japan Perl Association代表理事も務める。

好きな言語はPerlとGo。日本酒とうどんとロックンロールが好物。バンド活動も行っている。

URL:https://karupas.org/