[技術継承の現場] useとrequireの違い

|

物事には順番というものがあります。

連続テレビドラマでもアニメでもコミックでも小説でもなんでもいいですが、いきなり最終回やエピローグを見せられても大抵なにがなんだかわからない。ごく単純なレベルから少しずつ複雑度が増していく過程を見ていたからこそついていける話もあるものです。

プログラミングの世界も似たようなもので、たとえばWeb関連の技術も、table タグもない状態からフレームだ JavaScript だ CSS だ Flash だ…と進んできた過程を見ているからこそ大まかにでも全体像が把握できている面は否定できず、いま1から理解するのは大変かもしれないなあ、と社内の新人さんを見ていると思います。

先日周囲で話題になっていたのは Perl での「use と requireの違い」。

まあこれも、大変大雑把に返答させてもらえば、
「use使っとけ」
の一言で済む話ではあります。Perl4のころにはrequireしかなかったからなあ、あとから出てきたuseのほうがいいんだろうなあ、と私などはすんなり納得しているわけですが、確かに今Perlの勉強を始めてrequire, useと並んでいれば「なんのために似たような機能が二つもあるの?」と思っても仕方のないところでしょう。

このあたり、my と localの使い分けも同様ですね。最近はみなさすがにmyを使っている(というかlocalをむしろ知らない)ようですが、Perl4から5へ移行する過渡期ではネット上でもよく話題になっていました。まあそちらは別の機会に書くことにして、今回はuseとrequireの話。

記法の違い

最初に気付くのはモジュールの書き方でしょうか。requireはファイルを指定するものとして設計されたフシがあり、

require "AAA/BBB.pm";

と書きます。「require AAA::BBB;」とも書いても上記のように動作するけれど、これはかなり後付け感が漂うサービスであって、パッケージ名がダイナミックに指定される場合に以下のように書いても

$pkg = 'AAA::BBB';
require $pkg; # 'AAA::BBB' という名前のファイルを探してしまう

期待どおりには動きません。require の後ろにbarewordすなわち「$」も「""」もないむき出しの文字列が来たときだけ "AAA/BBB.pm"に展開してくれる。だからこういうときは

eval "require $pkg";

と書きましょう、そうすれば「require むきだしモジュール名」という文をevalすることになるのでOK、という定番の話があります。

なお、このevalが気持ち悪い、というDamian Conwayみたいな人のために、最近の定番はUNIVERSAL::requireというモジュールを使う手があります。

Perl::Critic / UNIVERSAL::require - naoyaのはてなダイアリー

これを使うと、

use UNIVERSAL::require;
$pkg = 'AAA::BBB';
$pkg->require;

と書けるようになります。ついでにいうと「$pkg->use」もできます。

ということで、「Catalystでも使われてるし、これからはUNIVERSAL::requireの時代かな!」と思いきや、Perl 5.9, 5.10の標準添付モジュールに採用されたのは Module::Load でした。

TAKESAKO @ Yet another Cybozu Labs: LL Ring - Language Update / Perl 5.9.4 の説明に間違いがありました

Module::Loadも似たようなものです。

use Module::Load;
$pkg = 'AAA::BBB';
load $pkg;

と書けます。

評価タイミングの違い

さてここまでは、requireはもともとファイルを指定するものだった、モジュール名を指定することもできるけどひと工夫必要な場合もある、という話でした。

一方useはどうかというと、もうファイルではなくてパッケージを指定するものとして、use "AAA/BBB.pm";はダメ、use AAA::BBB; と書くしかないと決めてある。

また、ダイナミックに選びたい場合はrequireを使ってください、と割り切っている。このあたりには「requireは実行時、useはコンパイル時」という事情が関係してきています。

そもそもPerlは内部でコンパイルを行います。そしてコンパイルしてできたバイトコードをインタプリタが実行していく。この手順がPerlのスクリプトを実行した一瞬の間に起こっています。いや一瞬か一晩かかるかどうかはスクリプトによりますが。

このうち、useはコンパイルの段階(コンパイルフェーズ)で、requireは実行の段階(実行フェーズ)で評価されるというわけです。

「だからどーだっつーの?」と思いますよね。どうせどっちも評価されることには変わりはないじゃないか、と。

ただ、コンパイルフェーズで評価されるということは、ソースのどこに書いてあってもそのモジュールが実行前にロードされるということになります。use は慣習的にファイルの先頭のほうにまとめて書く流儀が(少なくとも私の周囲では)ありますが、別にソースの一番最後のほうに書いてあっても同じなんですね。

さらには、

$x = 0;
if ($x) {
   use MyModule;
}

なんて書いても真っ先にMyModuleがロードされる。これがrequireなら、

$x = 0;
if ($x) {
   require 'MyModule.pm';
}

$xが偽だからifのブロックの中は通らない、と実行フェーズで評価されMyModuleはロードされない。ということになるわけです。

そもそもuse、requireがなにをするものか思い出してください。おもな効用は一度ロードしたモジュールは覚えておいて、何度も同じものをロードする手間を省くことです。

useの場合はさらにuse行の評価、すなわち何をロードしようとしているのか確認することすらコンパイル時の1回で済ませてしまう。

一方requireの場合は

for (0 ..100) {
   require 'MyModule.pm';
}

この場合ロードそのものは1度しか行わないけれども、require自体の評価、すなわち「MyModuleはすでにロードしてたっけ?」といった確認は100回あまり実行されているのですね。

B::Deparseで確認

こうした動きを簡単に確認する方法があります。404 Blog Not Found:perl - B::Deparseでも紹介されていますね、B::Deparseを使います。

これは、コンパイルでできたコードをまたいったんPerlのソースに戻す(Parseの逆だからDeparseと呼びます)ということをしてくれます。

「単に元に戻るだけじゃ?」と思うかもしれませんが、エキサイトあたりの自動翻訳で英訳した文をまた日本語に戻すとじつに奇天烈で面白い、といった遊びを誰しも子供の頃体験したはずです。決して同じソースに戻るとは限らない。最適化などが行われる場合もあります。

つまり、「Perlがあなたのソースをどう解釈したか」ということを知る手がかりになります。

やってみますか、useの場合。あ、あらかじめMyModule.pmは作っておかないとエラーになりますよ。

$ perl -MO=Deparse -e 'for (0 .. 100) {use MyModule;}'
use MyModule;
foreach $_ (0 .. 100) {
    ();
}
-e syntax OK

ほら。useの文はforブロックの外に出てきてしまいました。結果としてブロックの中は「();」、なにもしないという結果に。

requireの場合。

$ perl -MO=Deparse -e 'for (0 .. 100) {require MyModule;}'
foreach $_ (0 .. 100) {
    require MyModule;
}
-e syntax OK

変化なしです。「useはあらかじめ全部評価され、requireは実行時まで評価されない」という違いがこんな風に現われています。

オーバーヘッドの比較

ところで、いくつかモジュールをロードするやり方はわかったけど、速さはどうなのか。比較してみましょう。まずダミーのモジュールを作っておきます。

package AAA;

sub new {
    bless {}, shift;
}

sub run {
    shift @_;
    return 1+1;
}

1;

まあなにもしないのもナンだから1+1ぐらい計算させておきましょうか、ぐらいの中身ですね。

Benchmarkを使って今まで出てきたロード方法を並べてみます。

require UNIVERSAL::require;
use Module::Load;
use Benchmark qw (:all);
my $requires = {
                ByUse        => sub {use AAA;        AAA->new->run},
                ByRequire    => sub {require AAA;    AAA->new->run},
                ByUR_use     => sub {"AAA"->use;     AAA->new->run},
                ByUR_require => sub {"AAA"->require; AAA->new->run},
                ByML         => sub {load "AAA";     AAA->new->run}
               };

cmpthese(1000000, $requires);

それぞれのサブルーチンを百万回まわしてやろうというわけですが、実のところ「use AAA」は先述したようにコンパイル時に実行されてしまうので、deparseしてみると

$ perl -MO=Deparse test.pl
require UNIVERSAL::require;
use Module::Load;
use Benchmark (':all');
use AAA;
my $requires = {'ByUse', sub {
    'AAA'->new->run;
}
, 'ByRequire', sub {
    require AAA;
    'AAA'->new->run;
}
, 'ByUR_use', sub {
    'AAA'->use;
    'AAA'->new->run;
}
, 'ByUR_require', sub {
    'AAA'->require;
    'AAA'->new->run;
}
, 'ByML', sub {
    &load('AAA');
    'AAA'->new->run;
}
};
cmpthese(1000000, $requires);
test.pl syntax OK

とサブルーチンの外に出てしまいByUseは実質AAA->new->runの計測しかしていないことになります。

                 Rate   ByUR_use        ByML ByUR_require  ByRequire       ByUse
ByUR_use      14565/s         --        -58%         -86%       -97%        -97%
ByML          34686/s       138%          --         -66%       -92%        -93%
ByUR_require 103199/s       609%        198%           --       -77%        -79%
ByRequire    442478/s      2938%       1176%         329%         --        -11%
ByUse        497512/s      3316%       1334%         382%        12%          --

require、遜色ないですね。use(というか、何もしないとき)の11%減程度のロスで済んでいます。さきほどはごちゃごちゃ書きましたが、速度的には特にオーバーヘッドを気にすることはないのではないかと。

その他はやはりがくんと落ちて、Module::Loadだと10数倍、UNIVERSAL::requireのuseに至っては30倍以上違うという、予想通りの結果になりました。

UNIVERSAL::requireのuseメソッドはやりすぎとしてもrequireメソッドなら1秒で44万回、msecあたり440回実行できるわけですから、それでも十分早いとは思います…あまりこういうことを言ってるとだんだんなんのためにベンチマークとったのかわからなくなるのでこのへんにしておきますが、まあ参考までに。

もうひとつの大きな違い

ここまでをまとめると、
  1. 1度ロードすれば十分な場合はuse。
  2. パッケージを状況に応じてダイナミックに選んでロードしたいならrequireやUNIVERSAL::require等を使う
こういう使い分けになります。後者であっても、選ばれるのが軽いモジュールばかりならあらかじめ片っぱしからuseしておけばいいのではないかと思います。副作用があるとか重厚長大なモジュールなのでできるだけロードしたくないとか、できるだけメモリを節約したいとか、そういうときじゃないかな、requireは。

副作用。そう、実はさらに、useにはimportする、という大きな特徴があるのです…といったところで、長くなりましたのでひとまず今回はここまでとして、次回をお楽しみに。

このブログ記事について

ひとつ前のブログ記事は「範囲演算子について丁寧に説明してみる」です。

次のブログ記事は「use = require + import」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。