前回の(1)はこちらから。
高度なことをPerlだけで行うテクニック
Perlの基本的な機能をうまく活用するだけでも高度な処理を行えます。組込み関数はもちろん、一般的にperl本体と同時にインストールされるコアモジュール[1]など、さまざまな道具がPerlには最初からそろっています。
これらの基本的な道具は書籍やWeb上に情報がすでに多くあるので、今回は使ったことがない人が多いと思われる少しマニアックな道具を紹介します。
任意のシステムコールを呼び出す
組込み関数のsyscall
を使うと、任意のシステムコールを呼び出せます。システムコールは、カーネルの持つ機能を呼び出すためのしくみです。システムコールの一覧は、Linuxであればman syscalls
などで見られます。Perlを含む多くのプログラミング言語において、主要なシステムコールは組込み関数などで言語の機能として提供されています。また、組込み関数の中にはシステムコールのインタフェースとなっているものも多く存在します。
そのため、syscall
関数を利用しなければ目的を達成できない場合は多くはありません。また、組込み関数になくともCPANモジュールとしてより使いやすい形で提供されているものも多いです。基本的にはそれらを利用して、OSの新しいシステムコールの機能をいち早く試したいときや、CPANモジュールを入れにくい環境で実行したい場合にsyscall
関数を使うとよいでしょう。
システムコール番号を得る
syscall
関数によるシステムコールの呼び出しには、システムコール番号が必要になります。システムコール番号を知るには、libc[2]のヘッダファイルなどを参照する必要があります。これはOSごとに異なり、Ubuntuの場合はlinux-libc-dev
パッケージが必要です。次のようにインストールします。
C言語では、sys/syscall.h
を読み込めば#define
によって定義されたSYS_
で始まるシステムコール番号を示す定数を得られます。この定義をたどれば、システムコール番号が書いてあるファイルにたどり着けます。
Perlからシステムコール番号をロードできるようにする
Perlでシステムコール番号を扱うにはC言語のヘッダファイルをPerlから読める形式に変換する方法が一般的です。そのためには、h2ph
コマンドを使います。詳細な使い方はperldoc h2ph
を参照してください。
成功すれば、-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
を呼び出します。
sendfile
にはファイルディスクリプタ[4]を渡す必要があるので、fileno
関数を用いてファイルハンドルからそれを得る必要があります。syscall
関数は数値と文字列を扱えるので、これは問題なく実行できます。
定型的な処理を高速化する
ちょっとした問題解決のためにスクリプトを書いていると、想定以上にパフォーマンスが必要で、チューニングの必要性に迫られる場面があると思います。ケースによりさまざまな要因と対処方法が考えられますが、ここではPerlの基本的な機能を利用した高速化に役立つテクニックを紹介します。
低レベルなファイルAPIを使う
Perlでは高レベルなファイルの取り扱いは行単位で行えますが、低レベルなAPIも備えています。低レベルなファイルAPIを使えば、Perlがファイルのどの位置から読み込むのかを変えたり、行単位ではなく一定バイト数を読み込んだりできます。そのため、場合によってはより効率的にファイルを扱えます。
例として、巨大なファイルの末尾の数行だけを読んで処理したいケースを考えてみましょう。Perlはファイルを先頭から行単位で読み込むので、末尾に至るまでの不要な行は捨てるとしても、巨大なファイルでは時間がかかります。ファイルの読み込みや書き込みを開始する位置をシーク位置と呼びますが、シーク位置を変えるには組込み関数seek
を使います。また、一定バイト数を読み込むには組込み関数readを使います。
たとえば、末尾から1,024バイトを読み込むには次のようにします。
それなりに十分なサイズを読み込んで不要な部分を捨てれば数行分は得られるので、ファイルアクセスを最小限にとどめて効率良く目的が達成できます。
seek
などを応用すると、効率良くファイルを二分探索できます。cpanmなどでCPANモジュールのインストール時にモジュールをインデックスから探索する際に使われているSearch::Dict
モジュールは、まさにこのテクニックを使っているので、参考にするとよいでしょう。
constant
プラグマと定数畳み込みで処理を最適化する
Perlはインタプリタ言語ですが、定数畳み込みによる最適化も行います。定数畳み込みとは、実行の前に定数を単純化する最適化手法です。たとえば、3+4
と定数だけで書かれているPerlコードは、7
に畳み込まれます。
定数だけで完結する分岐も、定数畳み込みにより事前に展開されます。以下に例を示します。
これが定数畳み込みで最適化されると、次のようになります。
このように、定数畳み込みによって実行時の条件分岐をなくすことができるケースがあります。
この定数は、リテラルとしての定数だけではなく、constant
プラグマで作った定数にも有効です。constant
プラグマは定数[5]を定義するためのプラグマです。これを使えば、実行時に渡した環境変数などを使って定数を作れます。
効果的な場面は限定的ですが、わかりやすい例として、デバッグログの最適化を考えてみましょう。デバッグログはデバッグのためにあらゆる箇所に埋め込む必要がありますが、スクリプトが処理するデータ量に比例してログ出力の処理も増えると、本番のデータに対しては膨大なログを出すことになります。そのため、デバッグ出力の有無をフラグで管理することになりますが、実行中にデバッグ出力の有無を切り替えることはないので、その条件分岐を各所でそれぞれ行うのは無駄です。デバッグ出力の有無を決めるフラグを環境変数にしてconstant
プラグマで定数にすることで、定数畳み込みの恩恵が受けられます。
これを応用すれば、デバッグなどの一部のケースでしか利用しない処理・分岐を実行前に最適化できます。
まとめ
Perlの基本機能をうまく活用することで、より簡潔に記述するテクニックがあること、CPANモジュールに頼らずとも高度なことができることを示しました。Perlの機能をよく理解し適材適所で応用すれば、さまざまな環境での仕事を楽にできます。これらのテクニックのほとんどはアプリケーション開発など長期的にメンテナンスするコードには向きませんが、書き捨てのスクリプトやワンライナーでは役に立つでしょう。
さて、次回の執筆者はhitode909さんで、テーマは「cpanfileのアップデート」です。お楽しみに。
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
- 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
- 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT