Perl: リファレンスを使って長い文字列のコピーを回避できるか?

Perl は幼児レベルしかわからないので、今日も人に質問されてわからなかった。

  • 「サブルーチンに文字列を渡した後、サブルーチン内で substr を使って部分文字列を取得しようとしています。この場合、文字列をそのままサブルーチンに渡すのではなくリファレンスを渡した方が、値のコピーを防げるために性能面で有利だと聞いたのですが、本当ですか。substr に渡すためにデリファレンスする時点でどうせ値がコピーされてしまうのでしょうから、たいして変わらないのではないですか」

以下、帰宅してから Mac の 5.12.4 で試したメモ。作業記録であって結論はない。


こういう場合に Perl で使われる標準的な技法というのがあるんだろうけど、知らないので原始的なデバッグ文に頼ってみることにした。

リファレンスを使わない場合

まず、やろうとしていることは、こんな感じ。

#!/usr/bin/perl

sub foo {
	my ($s) = @_;
	print substr($s, 100, 5), "\n";
}

$a = "hello" x (1024 * 1024 * 100);
&foo($a);

これに、print と sleep を適宜混ぜ込んだ。

#!/usr/bin/perl

sub foo {
	my ($s) = @_;
	print "my\n";
	sleep 5;
	print substr($s, 100, 5), "\n";
	print "print\n";
	sleep 5;
}

$a = "hello" x (1024 * 1024 * 100);
print "a\n";
sleep 5;
&foo($a);
print "return\n";
sleep 5;

実行すると、以下のように、print される。sleep も入れたので、とりあえず今どこまで進んだのかは確認できる。

$ ./noref.pl 
a
my
hello
print
return
$

ここで、Perl を実行するのとは別に、もうひとつウィンドウを開いて top を実行し、何をやった時点でどれだけメモリを消費しているかを調べた。

僕の事前の予想はこうだった。

どこまでやったか メモリ消費 なぜそう思うか
$a = "hello" x (1024 * 1024 * 100); 500MB 文字列の長さが 500 * 1024 * 1024 だから
my ($s) = @_; 1000MB Perl ではサブルーチンの引数が参照渡し (変数渡し) だけど my を使うとローカル変数を作れると聞いたから
print substr($s, 100, 5), "\n"; 1500MB 関数に渡す前に値がコピーされそうだから

ところが、実際にやってみると、いきなり予想と違っていた。

どこまでやったか メモリ消費
$a = "hello" x (1024 * 1024 * 100); 1000MB
my ($s) = @_; 1500MB
print substr($s, 100, 5), "\n"; 1500MB

文字列を作って変数に代入した時点ですでに文字列 2 つ分のメモリが使われている。これはなんでだろう。

そして、サブルーチン内のローカル変数に値をコピーしたところで +500MB なのは予想通りだったけど、substr しているところではメモリが増えなかった (値がコピーされなかった) ように見える。ただ、top は一定期間ごとに表示を更新する仕組みだから、実は値はコピーされたんだけど、一瞬なので top には映らなかっただけ、という可能性もある。

うむ、よくわからん。

リファレンスを使ってみる

サブルーチンに渡すのをリファレンスにして、substr に渡す直前でデリファレンスしてみる。

#!/usr/bin/perl

sub foo {
	my ($s) = @_;
	print "my\n";
	sleep 5;
	print substr($$s, 100, 5), "\n";
	print "print\n";
	sleep 5;
}

$a = "hello" x (1024 * 1024 * 100);
print "a\n";
sleep 5;
&foo(\$a);
print "return\n";
sleep 5;

再度 top で確認すると、

どこまでやったか メモリ消費
$a = "hello" x (1024 * 1024 * 100); 1000MB
my ($s) = @_; 1000MB
print substr($$s, 100, 5), "\n"; 1000MB

リファレンスを渡しているから、ローカル変数へのコピーでは参照 1 つ分しかメモリ消費が増えないのはわかる。

substr の行でメモリ消費が増えたのか減ったのかが、やはりわからない。増えていないように見えるが、top だけではなんとも……。

Linux だったら strace でシステムコールを見ればわかるかな。