daily dayflower

2008-06-25

perluniadvice

perluniadvice の前半部分を訳してみました。かなり意訳。

アドバイスと銘うってますが,中級者〜上級者向きです*1

抄訳 perluniadvice

Perl 付属のドキュメント perlunitut はもう読んだ?もしまだなら,そこから読み始めること :)

以下にわたし(http://juerd.nl/)からのアドバイスの要約を挙げる。perlunitut には記述しなかった内容も含んでいる。

  • もしあなたが古い Perl を使っているのなら,最新版にアップグレードしよう
  • もしあなたが古い CPAN モジュールを使っているのなら,最新版にアップグレードしよう
  • 「外界」からやってくるデータはすべて decode() しよう
  • 「外界」へ出力するデータはすべて encode() しよう
  • 「テキスト文字列」と「バイト列」をあなたのプログラムの中で完全に分離しよう
  • ある「string」が「テキスト文字列」なのか「バイト列」なのか Perl は判断できない,ということを認識しよう
  • Unicode 文字列」とは「Unicode」文字列であって「UTF-8」文字列ではない,ということを認識しよう(!!)
  • UTF8 flag なんて存在しないかのように振る舞え
  • UTF8 flag を明示的にいじるな(on したり off したり)
  • UTF8 flag の状態を取得するな(Encode::is_utf8(), utf8::is_utf8()
  • 「string」に UTF8 flag が立っている場合にそれが「テキスト文字列」であるとは言えるけれど,逆は必ずしも成り立たないことを認識しよう
  • もしあなたが「string」を utf8::downgrade() しなきゃいけないとしたら,それはたいていそれ以前の処理が間違っている
  • ソースコードUTF-8 エンコーディングで書かれているときだけ use utf8; を使え
  • use bytes; を使うな
  • bytes:: ネームスペースの関数を使うな
  • 正規表現\C を使うな
  • use encoding; を使うな
  • pack() で「バイト列」を生成する場合,「C*テンプレートを使え
  • 「テキスト文字列」を unpack() するとき「Uテンプレート以外を使うな
  • 「バイト列」を unpack() するとき「Uテンプレートを使うな
  • pack() / unpack() で「Uテンプレートと「非 Uテンプレートを混ぜて使うな
  • pack() で「U0」や「C0テンプレートを使うな
  • lc() / lcfirst() / uc() / ucfirst() する前に utf8::upgrade() しよう
  • 大文字小文字を区別しないマッチングをする前に utf8::upgrade() しよう
  • \w\s などのキャラクタクラスをマッチングで使う場合,その前に utf8::upgrade() しよう

内部のごちゃごちゃ(UTF8 flag など)を直接さわらないと解決できないことがあるかもしれない。PerlUnicode サポートと相性が悪いモジュールを使ったり,実装が間違っているモジュールを使ったりしなきゃいけない時とか。ということで,以上にあげた項目は大まかな原則である。


以下,各項目について補足です。

最新版にアップグレードしよう

いわずもがな。Unicodeネイティブに取り扱いたいのなら Perl 5.6 以前はおすすめできません。

ちなみにそういう意味では,RHEL 5 や Ubuntu 8.04 LTS で標準的に含まれている Perl 5.8.8 は充分にこなれていると(個人的には)思います。

perldelta によると Perl 5.10 の Unicode にまつわる advantage は

  • Unicode Character Database が 5.0.0 になった(Perl 5.8.8 だと UCD 4.1.0)
  • UTF8 flag つき文字列の pack(), unpack()デフォルトでバイトベースではなく文字ベースになった
  • unpack() に文字数/バイト数を数える「.」というテンプレートが追加された
  • UTF-8キャッシングがより効率的かつ頻用されるようになった(詳細不明)
  • perlunitut, perlunifaq という POD が追加された

こんなところみたいです。ほかバンドルされてるモジュールが新しくなってたりするので変更点があるかもですが,これらだけが違うのなら,まぁ Perl 5.8.8 でもそれなりに使えますよね。

「テキスト文字列」だの「バイト列」だの,分離する,だの

先日書きました*2。んが,string についてまた新しく図版をおこしてみました。

f:id:dayflower:20080625134349p:image:w480

こないだは「Unicode 文字列」とか「byte stream」とかいってたのに,「テキスト文字列」だの「バイナリ文字列」だの「バイト列」だの,なんなんだよーと思われるかもしれません(すみません*3)。これは用語を perlunitut にあわせた,というのと,こないだのような単純な「二項対立」ではお話をすすめられないので,あえて用語をかえた,というのがあります。

Encode モジュールで変換しているのに,両者の違いが「Perl にとって判別不能」というのが「そんなバカな」と思われるかもしれませんが,実際そういうインプリメントになっています(下記参照)。

UTF8 flag について*4

ほんとうは UTF8 flag についてあまり書きたくないのですが*5,「存在しないかのように振る舞え」というお達しであり「存在そのもの」を知ることを禁止されているわけではなさそうなので,簡単に説明します。

実は Perl の「テキスト文字列」には下記の2通りの内部表象があります。

  • (内部)Latin-1 encoding 文字列(UTF8 flag off; 旧来のコードとの親和性が高い)
  • (内部)UTF-8 encoding 文字列(UTF8 flag on)

f:id:dayflower:20080625134418p:image:w400

単純に "ABC" などの文字列リテラルを表記すると,通常は「内部 Latin-1 文字列」となります。いっぽう,

  • "\x{2665}", chr(9829) など U+0100 以上の Unicode Character が文字列リテラルに含まれる場合
  • ソースコードuse utf8; で書いてあり,文字列内に U+0100 以上の文字が含まれる場合)
  • Encode::decode() によって some encoding byte stream を「テキスト文字列」に decode() した場合
  • UTF8 flag on の文字列と結合した場合

には「内部 UTF-8 文字列」となります。この文字列は UTF8 flag という内部フラグが on になっています。

UTF8 flag の状態は utf8::is_utf8() 関数などによって調べることができます。

# * UTF-8 でソースコードを記述しているとします *

use strict;
use warnings;
use Encode;

sub utf8_flag_of { utf8::is_utf8(shift) ? 'UTF8 on' : 'UTF8 off' }

print utf8_flag_of( "ABC" ), "\n";
# >>> "UTF8 off"

print utf8_flag_of( "ABC\x{2665}" ), "\n";
# >>> "UTF8 on"

print utf8_flag_of( "ABC" . chr(2665) ), "\n";
# >>> "UTF8 on"

no utf8;
print utf8_flag_of( "日本語" ), "\n";
# >>> "UTF8 off"
# あくまで UTF-8 encoding byte stream なので UTF8 flag off

print utf8_flag_of( decode('UTF-8', "日本語") ), "\n";
# >>> "UTF8 on"

use utf8;
print utf8_flag_of( "日本語" ), "\n";
# >>> "UTF8 on"
# use utf8; すると UTF8 on な文字列とみなしてくれる

perluniadvice のマナーをまもってプログラミングしていれば,みずからが utf8::is_utf8() などの関数を使う必要はないはずです。

で,上記のプログラムを読めばわかると思いますが,

は,同じように表記して同じように格納されてますよね。こういうこともあって,「Perl にはテキスト文字列かバイト列か判別不能」なのです。「テキスト文字列」や「バイト列」は,あくまで,あなたの「意図」に基づく状態(意味論)なのです。だから「意図」をもって「分離」することが大事なんですね。

UTF8 flag については,以前もっと詳しい話を書きました*6ので興味があればどうぞ。

ソースコードUTF-8 エンコーディングで書かれているときだけ use utf8;

utf8::* 系の関数use utf8; をしなくても使えます。(おそらく)欧米人はソースコードISO-8859-1 で書く習性があると思うんですが,そのような場合に use utf8; してしまうと文字列リテラルが誤読されてしまうので無闇に use utf8; を使うな,という意味だと思います。日本人には(さいわい)縁のない話。日本人が書く場合,マルチバイト文字列を含むコードは UTF-8 で書くでしょうから,文字列リテラルUnicode 文字列として認識してくれる use utf8; は福音。

Do not use this pragma for anything else than telling Perl that your script is written in UTF-8. The utility functions described below are directly usable without use utf8;.

Because it is not possible to reliably tell UTF-8 from native 8 bit encodings, you need either a Byte Order Mark at the beginning of your source code, or use utf8; , to instruct perl.

utf8 - perldoc.perl.org

なななんと BOM つけると use utf8; と等価になるんですか!と思って Perl 5.8.8 で試したけどだめでした*7Perl 5.10 でどなたか試してくれるとうれしいです。perldelta には書いてない feature?

use bytes;

use bytes; すると(そのスコープでは)length() 等の文字列操作関数が,バイト単位になります。たとえば漢字を与えると length() が3を返します(UTF-8 encoding では3バイトになるので)。詳しくは bytes - perldoc.perl.org を参照してください。

これは内部エンコーディングUTF-8 であることを連想させる悪癖。かつ,内部 Latin-1 文字列の場合,1文字=1バイトのままになります(UTF-8 だと U+0080U+00FFの範囲で2バイトのはず)。などなど,状態によって挙動が変わり直感的でないのでよろしくないです。

ということで,これ系はすべて使うな,と。

use encoding; を使うな

これ残念ながら理由がわかりませんでした*8。誰か教えて。不完全なところがあるからかな。それとも use utf8; で書くべきところを use encoding 'utf8'; って書く癖がついちゃうからかな。

2008-06-27 追記:

2008年06月26日 id:vkgtaro perl, unicode 「use encoding; を使うな」は、レキシカルじゃないとか、そもそも下位互換のために用意したとかって話を YAPC 2006 で dan さんが言ってた気がする。今はスコープ単位で動くんだっけ?

http://b.hatena.ne.jp/vkgtaro/20080626#bookmark-9074627

おー。情報ありがとうございます。たしかにしくみ上レキシカルじゃなさそう。ちら見したところ,すくなくとも PerlIO の部分はレキシカルにならないと思います。エンコーディング変換部分は Filter::Util::Call ベースっぽいのでひょっとするとレキシカルに動くかも。うーん検証しなきゃですね。

utf8::upgrade() について

さきに書いたとおり Perl の「テキスト文字列」には下記の2通りの内部表象があります。

  • (内部)Latin-1 encoding 文字列(UTF8 flag off)
  • (内部)UTF-8 encoding 文字列(UTF8 flag on)

utf8::upgrade() という関数

内部 Latin-1 encoding 文字列を内部 UTF-8 encoding 文字列に変換する

ものです。

f:id:dayflower:20080625134431p:image:w360

  1. 「内部 Latin-1 文字列」を与えると,「内部 UTF-8 文字列」に変換してくれます
  2. もともと「内部 UTF-8 文字列」なものは,そのままにしておいてくれます
  3. UTF-8 encoding バイナリ文字列(バイト列)」を与えると,おかしなことになります(1番と比較すると理由はわかると思います)

さて uc() などの前に utf8::upgrade() しなくてはいけない理由とは。サンプルコードで見てみます。

use strict;
use warnings;

binmode \*STDOUT, ':utf8';      # わたしの terminal は UTF-8 なので

# \x{e9} means LATIN SMALL LETTER E WITH ACUTE
my $str = "H\x{e9}llo";

print $str, "\n";
print uc($str), "\n";

というコードを素直に実行すると,

% perl uc.pl

Héllo
HéLLO

のようになります。「é」が「É」になってない!Latin-1 (ISO-8859-1) や Unicode Character 的には「é」の大文字は「É」であってほしいですよね。

実は UTF8 flag off なテキスト文字列の場合,デフォルトでは US-ASCII の範囲*9のみ「大文字」「小文字」「アルファベット文字」などの「文字種別判別」が行われます*10

一つの抜け道としては,use locale; プラグマを用いてロケールに基づく「文字種別判別」を行ってもらうことです。

uc EXPR
uc

Returns an uppercased version of EXPR. This is the internal function implementing the "\U" escape in double-quoted strings. Respects current LC_CTYPE locale if "use locale" in force. See perllocale and perlunicode for more details about locale and Unicode support. It does not attempt to do titlecase mapping on initial letters. See "ucfirst" for that.

If EXPR is omitted, uses $_.

uc - perldoc.perl.org

とありますので,システムロケールを *.ISO-8859-1 にしつつ use locale; して実行してみましょう。以下の実行は CentOS 5.1 で行いました*11

% LANG=en_US.ISO-8859-1 perl -Mlocale uc.pl

Héllo
HÉLLO

おー。「É」になりました。

もう一つの(より推奨される)方法は,utf8::upgrade() を用いて内部 UTF8 flag on な文字列に変換することです。このときは,「文字種別判断」は Unicode Character に基づきます。

use strict;
use warnings;

binmode \*STDOUT, ':utf8';

my $str = "H\x{e9}llo";

# utf8::upgrade は in-place upgrade
# また上で述べたように use utf8; する必要がないです
utf8::upgrade($str);

print $str, "\n";
print uc($str), "\n";

これを実行すると,

% perl upgrade.pl

Héllo
HÉLLO

やはり「É」が表示されるようになりました。

実装を浅くみてみます。uc() ではなく「文字種別判断」の部分ですが,

/******* snip snip snip *******/

/* デフォルト実装 */

#define isALPHA(c)      (isUPPER(c) || isLOWER(c))
                /* snip */
#   define isUPPER(c)   ((c) >= 'A' && (c) <= 'Z')
#   define isLOWER(c)   ((c) >= 'a' && (c) <= 'z')

/******* snip snip snip *******/

/* setlocale() サポートアーキテクチャでの実装 */

#    define isALPHA_LC(c)       isalpha((unsigned char)(c))
#    define isSPACE_LC(c)       isspace((unsigned char)(c))
#    define isDIGIT_LC(c)       isdigit((unsigned char)(c))
#    define isUPPER_LC(c)       isupper((unsigned char)(c))
#    define isLOWER_LC(c)       islower((unsigned char)(c))

/******* snip snip snip *******/

/* Unicode Character 用実装 */

#define isALPHA_uni(c)          is_uni_alpha(c)
#define isSPACE_uni(c)          is_uni_space(c)
#define isDIGIT_uni(c)          is_uni_digit(c)
#define isUPPER_uni(c)          is_uni_upper(c)
#define isLOWER_uni(c)          is_uni_lower(c)

/******* snip snip snip *******/

#define isALPHA_utf8(p)         is_utf8_alpha(p)
#define isSPACE_utf8(p)         is_utf8_space(p)
#define isDIGIT_utf8(p)         is_utf8_digit(p)
#define isUPPER_utf8(p)         is_utf8_upper(p)
#define isLOWER_utf8(p)         is_utf8_lower(p)

/******* snip snip snip *******/
handy.h

ロケール無視バージョンの isUPPER() 等は,ハードコーディングされています。このために US-ASCII の範囲のみ upper case になっていたんですね。今回は uc() の挙動のみたしかめましたが,「文字種別判断」にまつわる機能は上記の項目のように lc()\w マッチングなどもあります。


さて,utf8::downgrade() という反対の挙動をする関数がありますが,これは

(○)内部 UTF-8 encoding 文字列を内部 Latin-1 encoding 文字列に変換する

ものであり,

(×)内部 UTF-8 encoding 文字列を UTF-8 encoding バイト列に変換する

ではないことに注意が必要です。U+0100 以上の文字を含む文字列を変換しようとすると怒られます。上のアドバイスにもあるとおり,utf8::downgrade() は普通使う必要がないし,もし使ってうまくいくのであればそこ以前のコーディングが間違っている可能性が大です。

内部 Latin-1 文字列と内部 UTF-8 文字列が混在する可能性

あなたが「外界」からの入力を必ず decode() していれば,内部 UTF-8 文字列になるので基本的には影響しません。ですが,CPAN モジュールを使ったり,素直なコーディングをしていても,ごちゃごちゃになる可能性はあります。

utf8::is_utf8 considered harmful - Bulknews::Subtech - subtech と同様 HTML::Entities を使った例ですが,

use strict;
use warnings;
use HTML::Entities;

binmode \*STDOUT, ':utf8';

my $s1 = decode_entities("H&eacute;llo");
print $s1, "\n";
print utf8::is_utf8($s1) ? 'UTF8 on' : 'UTF8 off', "\n";

my $s2 = decode_entities("H&eacute;llo&hearts;");
print $s2, "\n";
print utf8::is_utf8($s2) ? 'UTF8 on' : 'UTF8 off', "\n";

この実行結果は,

Héllo
UTF8 off

Héllo♥
UTF8 on

のようになります。同じ関数を同じように呼んだだけですが,$s1 は Latin-1(U+0000U+00FF)の範囲の文字で構成されるため全体が内部 Latin-1 文字列(UTF8 off)となり,$s2 は内部 UTF-8 文字列(UTF8 on)となりました。

HTML::Entities が悪いとは一概にはいえません。下記のような単純なコーディング*12でも,

use strict;
use warnings;

binmode \*STDOUT, ':utf8';

my $s1 = chr(0x0048) . chr(0x00e9) . chr(0x0079);
print $s1, "\n";
print utf8::is_utf8($s1) ? 'UTF8 on' : 'UTF8 off', "\n";
# >>> "UTF8 off"

my $s2 = chr(0x0048) . chr(0x00e9) . chr(0x0079) . chr(0x2665);
print $s2, "\n";
print utf8::is_utf8($s2) ? 'UTF8 on' : 'UTF8 off', "\n";
# >>> "UTF8 on"

前者は内部 Latin-1 文字列,後者は内部 UTF-8 文字列となります。

ここコーディング指針を決めるうえでなかなか悩むところです。

  • 外部モジュールからの戻り値utf8::upgrade() をかませる
  • uc() 等呼び出す前だけ utf8::upgrade() をかませる

の2者の指針があると思います。前者のほうが(decode() 等と)統一感を保てていい気がするんですが,上記のように実直にコーディングしていても混在する可能性もありますし,となると後者かなぁとか。文字列を結合すると UTF8 flag に(言葉は悪いですが)「汚染」されていくのであんまり深く考える必要はないかもしれませんが。

pack(), unpack()

テキスト処理がからむプログラムで使うことはまずないし,個人的にもあまり使わない関数なので省略しようと思いましたが,一応サンプルコードだけおいておきますね。

use strict;
use warnings;

sub say    { print join q{}, @_, "\n" }
sub a2hex4 { join q{ }, map { sprintf '%04X', $_ } @_ }
sub a2hex2 { join q{ }, map { sprintf '%02x', $_ } @_ }
sub str2a  { map { ord } split q{}, shift }
sub utf8_flag_of { utf8::is_utf8(shift) ? 'UTF8 on' : 'UTF8 off' }

# "日本語" as Unicode String
my $uStr = "\x{65E5}\x{672C}\x{8A9E}";
# "日本語" in UTF-8 as byte stream
my $bStr = join q{}, map { chr hex } qw( e6 97 a5 e6 9c ac e8 aa 9e );

# Unicode Character Array
my @uaCh = str2a( $uStr );
# UTF-8 Byte Array
my @baCh = str2a( $bStr );


say a2hex4( unpack('U*', $uStr) );
# >>> "65E5 672C 8A9E"
# Unicode 文字列の Character Array 化

say a2hex2( unpack('C*', $bStr) );
# >>> "e6 97 a5 e6 9c ac e8 aa 9e"
# バイト列の Byte Array 化

say a2hex4( str2a( pack('U*', @uaCh) ) );
# >>> "65E5 672C 8A9E"
# (Character Array の Unicode 文字列化)

say a2hex2( str2a( pack('C*', @baCh) ) );
# >>> "e6 97 a5 e6 9c ac e8 aa 9e"
# (Byte Array のバイト列化)


######## 以下非推奨機能

say a2hex2( unpack('C*', $uStr) );
# >>> "e6 97 a5 e6 9c ac e8 aa 9e"
# (Unicode 文字列が UTF-8 byte stream に自動変換して出力される)
# (素直に自力で Encode::encode('UTF-8', $uStr) してから使いましょうね)

say a2hex4( unpack('U*', $bStr) );
# >>> "00E6 0097 00A5 00E6 009C 00AC 00E8 00AA 009E"
# (UTF-8 byte stream の各文字が Latin-1 byte stream としてみなされてしまった)
# (素直に自力で Encode::decode('UTF-8', $bStr) してから使いましょうね)

say a2hex4( unpack('U*', "H\x{e9}llo") );
# >>> "0048 00E9 006C 006C 006F"
# (いっぽうこちらはわかっていて使うなら問題ない)
# (Latin-1 文字列の各 Unicode Character を取得している)
# (コードとしては上と同じだが,まさに「意味論」的問題です)


say utf8_flag_of( pack('C*', @baCh) );
# >>> "UTF8 off"
# (Byte Array→バイト列,なので当然 UTF8 flag off だが……)

say a2hex4( str2a( pack('U0C*', @baCh) ) );
# >>> "65E5 672C 8A9E"
say utf8_flag_of( pack('U0C*', @baCh) );
# >>> "UTF8 on"
# (UTF-8 文字列に自動変換され Unicode 文字列となり UTF8 flag on となる)


say utf8_flag_of( pack('U*', @uaCh) );
# >>> "UTF8 on"
# (Character Array→Unicode 文字列,で UTF8 flag on だが……)

say a2hex2( str2a( pack('C0U*', @uaCh) ) );
# >>> "e6 97 a5 e6 9c ac e8 aa 9e"
say utf8_flag_of( pack('C0U*', @uaCh) );
# >>> "UTF8 off"
# (UTF-8 byte stream に自動変換され UTF8 flag off となる)

# (こいつらも素直に自力で事後に encode() / decode() しましょうね)

str2a() とか車輪の再発明もいいところですが*13,意味合いをはっきりさせるためにもあえて再発明してます。

後半の U0C*, C0U* については注釈が必要かも。

pack()テンプレートU を使うと,結果は UTF8 flag 付き文字列になります。このことを利用して,あたまに U0 をもってくると,UTF8 flag 付き文字列返すモードになります。そのあと C* で byte array を食わせてやれば,UTF-8 byte stream array を UTF8 flag 付き Unicode 文字列に変換できる,ということです。なかば裏技的用法ですな。

If the pattern begins with a "U", the resulting string will be treated as UTF-8-encoded Unicode. You can force UTF-8 encoding on in a string with an initial "U0", and the bytes that follow will be interpreted as Unicode characters. If you don’t want this to happen, you can begin your pattern with "C0" (or anything else) to force Perl not to UTF-8 encode your string, and then follow this with a "U*" somewhere in your pattern.

pack - perldoc.perl.org

なお Perl 5.10 では,unpack() における U0C0 テンプレートが,出力結果ではなく入力結果オリエンテッドになっています。これは Perl 5.8 のときと,挙動が逆になっています。

To be consistent with pack(), the C0 in unpack() templates indicates that the data is to be processed in character mode, i.e. character by character; on the contrary, U0 in unpack() indicates UTF-8 mode, where the packed string is processed in its UTF-8-encoded Unicode form on a byte by byte basis. This is reversed with regard to perl 5.8.X, but now consistent between pack() and unpack().

perldelta - perldoc.perl.org

なんにせよ,これらの feature は現在では非推奨ということで。

つーか,pack()unpack()テンプレートC だけ使い,そこからの「入出力」に Encode::decode(), Encode::encode() を使うようにすればいいんじゃないかと思います。あと文字列を文字ごとに分解して文字コード群を取得したい場合には unpack('U*') は便利かな。

*1:(わたしが)用語定義もしてないし同義語をごちゃごちゃまぜこんだり(Latin-1 と ISO-8859-1 とか)してますしね。ごめんなさい。

*2 図解: Perl と Unicode 文字列 - daily dayflower

*3:こないだの記事は図版だけは気合いれて描きましたが,読み返してみると,文章が納得いくクオリティじゃないですね。

*4:ここは特に文章練れてないです。理解できなくても,おそらく文章のせいです。悲しまないで。

*5:ボロが出そうだし,一応この機能を意識しないでプログラミングしましょうということになってるし。

*6 UTF8 フラグあれこれ - daily dayflower Perl (5.8) での文字列の内部表象について返信 - daily dayflower

*7:自信がないので追試希望。

*8:素敵な feature だと思うんですが。わたしは使わないけど。

*9U+0000U+007F

*10:たぶん後方互換性のため。

*11Ubuntu 8.04 にはデフォルトで en_US.ISO-8859-1 などの ISO-8859-1 エンコーディングロケールインストールされていませんでしたので CentOS 5.1 を使いました。

*12:もっと単純には "H\x{e9}y\x{2665}" でいいんですが,なんというかロジックを介在させて「らしく」見せました。

*13:それこそ unpack() のやってくれることですから。

2006-08-16

mod_perl と Perl インタプリタプール

前々回も書きましたが,worker MPMmod_perl を使うと,インタプリタプールなるものが作成されます。このインタプリタプールというのはまさに Apache におけるプロセスプール(prefork MPM の場合)やスレッドプール(worker MPM の場合)の Perl インタプリタ版のようなものです。

このインタプリタプールでの PerlInterpMax(総数の最大)はデフォルトで 5 になっています。ですから,「prefork MPM から worker MPM にしたらメモリ消費量がガツンと減ったぜ!httpd の thread 数 150 でぶんまわしてるのに!スレッドマンセー!」とぬか喜びしていると,実は Perl は並列に 5 つしか走っていなかった,ということがありえます。

2007/09/27 追記。adiary 作者さんからトラバいただきました(⇒PerlInterpMax の示すもの - adiary開発日誌)。PerlInterpMax というのは Apache 全体のインタプリタ数を示すのではなく,各プロセス毎のインタプリタ数を示すようです。worker モデルでもデフォルトの ServerLimit は 16 個なのでプロセスが最大 16 個になってしまい,結構大量のインタプリターを生成するようです。つーか詳しくは当該トラバ参照。トラックバックありがとうございました。わたしも激しく勘違いしていました。

mod_perl におけるスレッドモデルについてはいくつかの場所に記述が分散しているのですが,今日は http://perl.apache.org/docs/2.0/user/design/design.html#Interpreter_Management あたりを訳してみました。といってもいつもの通り超意訳&抄訳,PerlInterpScope のところは未訳(Apache の subrequest というものがなんぞや,というイメージがまだつかめていないため)というていたらくです。

インタプリタの管理(抄訳,中途)

マルチスレッド環境で mod_perl をサポートするために,mod_perl-2.0 は Perl の ithread(Perl 5.6.0 からの新機能)*1を利用しています。この機能は Perl ランタイムスレッドセーフな PerlInterpreter 構造へとカプセル化するものです。mod_perl でリクエストを処理するスレッドは,各々 PerlInterpreter インスタンスが必要となります。

PerlInterpreter を各 httpd スレッド毎に一対一対応させる代わりに,mod_perl では設定自在なインタプリタプールを管理しています。この方式により,必要最小限な数のインタプリタを用意することでメモリ使用量を押さえることができます。また,各インタプリタで既に使用されたメモリアロケーションを再利用することができます。これは 1.3.x 時代のモデルでは不可能なことでした。

インタプリタプールは Perl を -Dusethreads オプション付きでビルドした場合のみ有効です。さもなくば mod-perl 1.0 のように振る舞います(つまり単一のインタプリタを利用する)。

httpd サーバがスタートするとき,Perl インタプリタが構築され,設定ファイルで指定されたすべてのコードがプリコンパイルされます(ここまでは 1.0 と同様の挙動です)。ここで作成されたインタプリタは「親」インタプリタと呼ばれます。そして親インタプリタの(スレッドセーフな)クローンを PerlInterpStart 設定子で指定された数だけ perl_clone() によって作り,インタプリタプールに追加します。このクローンは書き込み可能なデータ(シンボルテーブル等)を親からコピーし,コンパイルされたシンタックスツリーを共有します。

たとえば以下のようにいくつかの適当なモジュールを読み込む startup.pl スクリプトで計測してみたところ,

use CGI ();
use POSIX ();
use IO ();
use SelfLoader ();
use AutoLoader ();
use B::Deparse ();
use B::Terse ();
use B ();
use B::C ();

インタプリタは 6M のメモリを消費しましたが,子インタプリタは各々その半分以下のサイズ,約 2.3M しか消費しませんでした(シンタックスツリーを共有できているおかげですね)。

注: この結果は Perl 5.6.0 の perl_clone() と GvSHARED 最適化メモリリークがあることが発見される前に測定したものです。

Perl*Handler が設定されている場合,リクエスト時に,利用可能なインタプリタがプールから割り当てられます。conn_rec 構造体と request_rec 構造体はスレッド毎に存在しており,ポインタはconn_rec->pool や request_rec->pool に保持されます(これらはリクエスト生存中に利用されたりします)。

スレッドが実行されていない間に呼ばれるハンドラ(PerlChildInitHandler と PerlChildExitHandler のことです)では,親インタプリタが使用されます。インタプリタプールを制御するためのいくつかの設定子があります。

PerlInterpStart

起動時にクローンするインタプリタの数です。

(訳注: デフォルト 3)

(訳注: StartServers のインタプリタプール版みたいなもの)

PerlInterpMax

(訳注: デフォルト 5)

(訳注: ServerLimit のインタプリタプール版みたいなもの)

プール内のインタプリタが全部使用されている場合,mod_perl はリクエストを処理するために新しいインタプリタクローンしますが,この設定子インタプリタ総数の最大値となります。PerlInterpMax に達した場合,mod_perl は(COND_WAIT() により)ブロックします。インタプリタが余って利用可能になるとブロックから戻ります(COND_SIGNAL() によってシグナルされることで)。

PerlInterpMinSpare

(訳注: デフォルト 3)

(訳注: MinSpareServers のインタプリタプール版みたいなもの)

PerlInterpMaxSpare

(訳注: デフォルト 3)

(訳注: MaxSpareServers のインタプリタプール版みたいなもの)

PerlInterpMaxRequests

(訳注: デフォルト 2000)

(訳注: MaxRequestsPerChild のインタプリタプール版みたいなもの)

一つのインタプリタが処理するリクエストの最大数です。処理済リクエストがこの数に達するとインタプリタは終了させられ,新しいインタプリタクローン)で置き換えられます。

PerlInterpScope

(訳注: デフォルト request)


訳注

PerlInterpScope はディレクトリ単位(<Location> 等),それ以外は Server 単位の設定(VirtualHost 等)になります。

*1Perl の ithread については http://www.donzoko.net/doc/memo/perlithreads.html がまとまっています

2006-06-06

mod_perl 2.0 の Server Life Cycle

mod_perl 2.0 のサーバ起動にまつわる文書を読み込んでいました。

  • サーバスタートアップスクリプトは,1.0 時代のドキュメントでは「PerlRequire」記述子で読み込むように書かれていることが多いが,実行される時点が中途半端。なので,PerlPostConfigRequire を使う方が吉。もし設定ファイル自体で Perl の機能を利用しているのであれば(普通そこまでコアなことやらなくて済むんだけど),PerlConfigRequire を使うとサーバ設定フェイズ(すなわちかなり早い段階)で実行される。
  • Apache 2.x では,graceful restart がうまくいくことの確証を得るために,一度サーバ設定フェイズが終わると,Apache 自身を再起動する。ということは,サーバ起動時に,スタートアップスクリプト等は 2 回実行される。このことで困るってことはたいていないんだけど,スタートアップスクリプトでログ吐きとかやってるとあれれ?と思うかも。
  • かくなるように,スタートアップスクリプトは基本的にマスタサーバプロセスの起動時,再起動時にしか実行されない(と思う)。どういうことかというと,MaxRequestsPerChild を超えたために子プロセス等が再起動した場合でも実行はされない(と解釈しました)。
  • だから,プロダクションサーバ等で DBIx::Class::Schema を継承したクラスみたいに重い*1モジュールはスタートアップスクリプトのほうで読み込んでおくとよい,と,思,い,ます。

おまけで,中途半端ながら上記文書の「mod_perl Startup」節の抄訳をあげておきます。今回は全然推敲してないです。

mod_perl Startup

以下の節では mod_perl のスタートアップについて詳述します。

起動直後の再起動

Server Life Cycle 節で述べたように,Apache は起動時にサーバ設定フェイズに移行し,次に PerlOpenLogsHandler フェイズに移行,そして PerlPostConfigHandler フェイズに移行した後に,すぐに自分自身を再起動します。ですから,サーバスタートアップで行われるすべての挙動は二度実行されます。Apache の再起動によって,Perl エンジンは完全に終了し,再びスタートします。

Perl エンジンはいつスタートするのか

Apache を 'httpd -t' (これは 'apachectl configtest' するのと同じ意味です)あるいは 'httpd -S' (訳注: 解釈後の設定ファイルのダンプ;現状だと VirtualHost 部分だけ)というコマンドラインで起動したとき,サーバ設定フェイズのみ実行し,終了します。設定ファイルによっては,Perl エンジンが起動することもありますし,しないこともありえます。詳しくは以下をご覧下さい。

通常のスタートアップでは,mod_perl 2.0 では Perl エンジンの始動をサーバ設定フェイズが終了するまで遅らせます。これは PerlSwitch 記述子を利用できるようにするためです。Perl エンジンが始動した後ではもう Perl のスイッチを設定できませんから。

サーバ設定フェイズが終了すると,mod_perl は post_config フェイズ の一番最初で Perl エンジンを起動し,登録されたすべての PerlRequire エントリと PerlModule エントリを実行します。

post_config フェイズの終了間際で,PerlPostConfigRequire エントリを実行します。

サーバ設定フェイズのあいだに下記の記述子に遭遇した場合,mod_perl 2.0 はただちに Perl エンジンを起動します(これらの記述子では Perl が起動している必要があるので)。

  • PerlLoadModule
  • <Perl> セクション
  • PerlConfigRequire

ですから,Perl エンジンをできるだけ速く起動したいのであれば,httpd.conf に空の <Perl> セクションを記述すればできます。

<Perl>
# この記述しによって Perl エンジンは通常より早めに起動します
</Perl>

という記述を DSO 版の mod_perl を使っているのであれば LoadModule した直後に,static 版の mod_perl を使っているのであれば mod_perl の設定が書かれているより前に,加えてください。ですが,ほとんどの場合,かわりに PerlConfigRequire 記述子を使うことになると思います。

スタートアップファイル

サーバ起動時に PerlPostConfigRequire 記述子を使って,スタートアップ用 Perl スクリプトを実行できます。たとえば,

PerlPostConfigRequire /home/httpd/Perl/lib/startup.pl

のように。

このスタートアップスクリプトは,Perl モジュールの検索パス(@INC)を設定したり,よく使われるモジュールをプリロードしたり,よく使われる定数をプリコンパイルしたり,などのために使うことができます。典型的な mod_perl 2.0 用のstartup.pl ファイルの例を示します。

省略(http://perl.apache.org/docs/2.0/user/handlers/server.html 参照)

このファイル例では,まず,非標準のディレクトリPerl モジュール検索パス @INC に追加しています。

use lib qw(/home/httpd/perl);

もし mod_perl 1.0 との互換レイヤが必要なら下記のように,該当部分のコメントをはずしてください。

use Apache2::compat ();

次によく使う mod_perl 2.0 のモジュールと定数をプリロードしています。

最後に,一般のモジュールと同じように startup.pl ファイルはステータス 1 で終了しなくてはいけません。

リスタートに対処する

サーバのスタートアップで実行される Perl モジュール / スクリプトは,理想を言うと Apache の自動再起動に影響をうけないものである必要があります(訳注: スタートアップスクリプトサーバ起動時に一度だけしか実行されないと思いこんだコーディングをしないほうがよい,ということ)。しかしながら,再起動問題に対処したい場合には,Apache2::ServerUtil::restart_count を使うことができます。

*1:いやほんとに重いですね。プロファイリングしたところ Class::C3 のせいっぽい。ということは Perl 6 になると改善されるのでしょうか。

2006-05-22

DBIx::Class での JOIN

DBIx::Class::Manul::Cookbook の "Using joins and prefetch" の前半の抄訳です。仕事上必要になってラフに読んだので参考程度に。

リレーション対象のテーブルの1つ以上のカラムを取得したり,それでソートしたりするには,join アトリビュートを使います。あるアーティスト名にマッチするすべての CD を得るには以下のようにします:

my $rs = $schema->resultset('CD')->search(
  {
    'artist.name' => 'Bob Marley'    
  },
  {
    join => [qw/artist/], # join the artist table
  }
);

# 以下の SQL と等価です:
# SELECT cd.* FROM cd
# JOIN artist ON cd.artist = artist.id
# WHERE artist.name = 'Bob Marley'

必要ならば order_by アトリビュートに含めることで,リレーション対象のテーブルのお好みのカラムでソートすることもできます。

my $rs = $schema->resultset('CD')->search(
  {
    'artist.name' => 'Bob Marley'
  },
  {
    join     => [qw/ artist /],
    order_by => [qw/ artist.name /]
  }
};

# 以下の SQL と等価です:
# SELECT cd.* FROM cd
# JOIN artist ON cd.artist = artist.id
# WHERE artist.name = 'Bob Marley'
# ORDER BY artist.name

join アトリビュートはリレーション対象のテーブルに存在するカラムを取得したりソートしたりするときにだけ使うようにしてください。(元の)メインテーブルのカラムだけ必要な場合にテーブルを結合するとパフォーマンスが悪化します!

さて,CD のリストをアーティスト名つきで表示したいことでしょう。次のようにするとうまくいきます:

while (my $cd = $rs->next) {
  print "CD: " . $cd->title . ", Artist: " . $cd->artist->name;
}

しかしながら問題があります。主クエリで CD テーブルとアーティストテーブルを探索しましたが,データ自体は CD テーブル からしか取得していないのです。取得した CD オブジェクトに対応するアーティスト名を得るために,DBIx::Class はふたたびデータベースを検索しなくてはなりません:

SELECT artist.* FROM artist WHERE artist.id = ?

主クエリで返される CD オブジェクトすべてについて,それぞれ上記のような SQL が実行されるのです。5 つの CD にたいしては 5 つのクエリが実行されます。100 枚の CD に対しては 100 ものクエリが追加実行されるのです!

ありがたいことに,この問題を解決するための prefetch アトリビュートというものが DBIx::Class に含まれています。このアトリビュートを用いるとリレーション先のテーブルの結果をあらかじめ取得することができます:

my $rs = $schema->resultset('CD')->search(
  {
    'artist.name' => 'Bob Marley'
  },
  {
    join     => [qw/ artist /],
    order_by => [qw/ artist.name /],
    prefetch => [qw/ artist /] # return artist data too!
  }
);

# 以下の SQL と等価です("cd" と "artist" 両者を SELECT していることに注目してください):
# SELECT cd.*, artist.* FROM cd
# JOIN artist ON cd.artist = artist.id
# WHERE artist.name = 'Bob Marley'
# ORDER BY artist.name

CD の(訳注: アーティスト名つき)リストを出力するコードはそのまま使えます:

while (my $cd = $rs->next) {
  print "CD: " . $cd->title . ", Artist: " . $cd->artist->name;
}

DBIx::Class はあらかじめアーティストテーブルから合致するデータを読み込みます。ですから追加実行される SQL 文はありません。改変前より,とても効率的なクエリが発行されます。

DBIx::Class 0.05999_01 以降より,prefetchhas_many リレーションと組み合わせて使えるようになりました。

prefetch はリレーション先のテーブルのデータを確実に使う予定があるときだけ使用してください。メインテーブルの結果だけ必要なのに,リレーション先のテーブルをプリフェッチするとやっぱりパフォーマンスは悪化しますよ!