miauのブログ

はてなダイアリー「miauの避難所」をはてなブログに移行しました

Windows での ruby の動作を速くしたい (2)

ということで、

の続きです。前回はファイルアクセスまわりに絞って動作検証しましたが、今回はビルド方法が違う ruby との比較がメインで。

ちなみに今回 Windows 上の Perl もとばっちりを食っています。

ruby-list のスレッドを読み返してみる

前回はファイルシステムを疑ったりファイル I/O を疑ったりしてましたが、ちゃんと読み返してみると的を外してる気がしてきました。

ここに書かれていることをまとめると、

  • mswin32 版では 1.9 が 1.8 系より 3 倍程度遅い
  • linux 版では 1.9 のほうが 2 倍程度速い
  • mingw 版では 1.9 が 3 倍程度遅い(mswin32 と同様)
  • バイナリモードで読み書きすることで 1.9 が遅い問題は解消する

という感じ。

profileをとっても、いまいち理解できませんでしたが、何となくファイルから
の読み込み、書き込みが遅くなっているような気がします。

とあるけど、あくまでも 1.9 系で遅くなっている原因が I/O 周りだと言ってるだけだったり。

こちらのスレッドからも気になる発言を引用。

WindowsファイルシステムNTFSの問題かとも思い別のマシンでFAT32との
比較もしてみましたが差はありませんでした。

個人的な感覚としては、プロセス生成もWindowsは遅い気がしています。

改めてバイナリモードでもテストしてみましたが、差はありませんでした。

Cygwinでもテストしてみましたが、早くはありませんね。
また、Windows上にインストール(wbui.exeで)したUbuntuや、andLinuxでも
同様のテストをしてみましたが、NTFS上で実行した場合には遅く、NTFS
に存在する仮想ディスク(ext3)では動作が速いので、正確にはファイルIOでは
なくて、ディレクトリ操作のスピードがWindowsでは遅いという事になるの
かも知れません。

ちゃんと読んでなかったんだけど、ext3 での検証は UbuntuandLinux で実施しただけなので、I/O が原因だとは言い切れない。もしかしたら I/O 関係なく遅い原因があるのかも?

MinGW 版も試してみる

上記のスレッドでは議論の対象外ですし、実行している環境もまちまちなのでなんとも言えないのですが、1.8.x 系での実行時間はおおよそ

  • mswin32: 3.33 秒
  • linux: 0.86 秒
  • mingw: 0.64 秒

とのことで、mswin32 の動作が遅い現象が見て取れます。mingw 版で動作させたら解決しないかな?という期待が少し持てます。

ビルド方法は

を参考に・・・と思ったけど、

にファイルがあったので、こちらを使ってみました。動作速度の詳細は後述しますが、mswin32 版と大差ありませんでした・・・。

いろんなビルドで速度比較してみる

MinGW 版との比較だけではなく、Ubuntu 上の ruby 等と動作を比較してみることにします。

環境

使用する PC は Thinkpad X61Intel Core2 Duo T8100 @ 2.10GHz、3.00 GB RAM、SSD(S64GSSD25-M)使用)で、Windows 7 Ultimate 32 bit が稼働中。ここに wubiUbuntu を入れて、そこでインストールした ruby と速度を比較します。ついでに OSx86(hackintosh)も入れっぱなしだったので、そこでも動作確認してみます。こちらはバージョンも大きく違ったりしますので、参考値ということで。

詳細なバージョンは下記のとおりです。

Windows 環境にはアンチウィルスソフト等も入っているので、全部入りの状態とセーフモードの両方で計測しています。

使用スクリプト

元の ML で検証に使われていたスクリプト

を使って、ファイル入出力周りの所要時間を計測します。

ファイル入出力以外の処理も遅い疑いがあるので、単純な再帰呼び出しの所要時間もついでに計測します。

に載っていた処理を拝借して、tak(12, 6, 0) を計算しました。

結果

それぞれ

  • (1) print strings to file(ファイル出力)
  • (2) read from file, and regexp match, and print to file(ファイル入出力+マッチング)
  • (3) tak(12, 6, 0)(再帰呼び出し

の所要時間(秒)です。

- (1) (2) (3) 備考
mswin32 1.40(0.90) 9.55(9.17) 9.58(10.08) 括弧内はセーフモード時
mingw32 1.17(0.77) 8.25(6.97) 6.33( 6.69) 括弧内はセーフモード時
ubuntu 0.71 5.95 10.11
osx86 0.87 6.47 21.32

ということで、

  • (1) のファイル出力については Windows が 2 倍程度遅いように見えるが、セーフモード時には大差ないのでアンチウィルスソフト等の影響が大きい
  • (2) では (1) と同じような傾向があるが、mswin32 ではセーフモードにしても Ubuntu との差があまり埋まっていない
  • (3) のように入出力を伴わないケースでは mingw32 > mswin32 = ubuntu のような状態。セーフモードのほうが動作が遅いのは???

という感じです。

前回リンクを貼った id:ashelベンチマークによると、ファイルを探す処理に時間がかかっているとのことなので、この結果はそれなりに納得です。

ついでに Perl も計測してみる

今回「Windows では動作が遅い」というのが気になって調査しているわけですが、こんな現象は Ruby 以外では聞いたことないような気がします。試しに Perl でも似たようなスクリプトを用意して計測してみました。

これでもし WindowsUbuntu で動作速度に差異がなかったら、Windows 上の ruby も改善の余地があるということになるかも、と期待しつつ。

環境
使用スクリプト

Ruby のを真似て作ってみました。ファイル入出力その他とたらいまわしですね。

結果

それぞれ

  • (1) print strings to file(ファイル出力)
  • (2) read from file, and regexp match, and print to file(ファイル入出力+マッチング)
  • (3) tak(12, 6, 0)(再帰呼び出し

の所要時間(秒)です。

- (1) (2) (3) 備考
mswin32 0.35(0.37) 8.21(7.89) 221.77(216.43) 括弧内はセーフモード時
ubuntu 0.21 3.96 7.96
osx86 0.51 4.65 14.48

・・・うん。予想外の結果で困りました。

(1) でファイル入出力の差が 1.5 倍程度ありますが、そんなのは誤差の範囲で。Ubuntu で 8 秒で終わるたらいまわしWindows 上で 216 秒ってどういうこと?今まで Windows 上の PerlGoogle Code Jam に参加して「時間内に計算終わらない〜」と苦しんだりして、「やっぱり LL だけじゃダメかも・・・」と方向転換してたのに・・・Perl 側の問題だった?

とりあえず Ruby との比較という意味では、何の参考にもなりませんでした。

Windows での rubyコンパイル

ファイルアクセス周りを疑ってた頃に試したことなんですけど、ついでに書いておきます。

rubyソースコードをちょっといじって、ファイルアクセスの部分を仮想ディスクへの読み書きに変えたりすることで高速化できないかなー?と妄想して、rubyコンパイルをやってみました。

手元にあった Visual C++ 2005 Express Edition を使ったわけですが、

にあるように、byacc をダウンロードしておく必要があります。

コンパイル時に「revision.h: No such file or directory」みたいなエラーが出たのでググってみると、自分でファイルを作る必要があるみたいですね。

また、コンパイル中に

ruby.exe - エントリ ポイントが見つかりません

プロシージャ エントリ ポイント rb_thread_status がダイナミック リンク ライブラリ msvcrt-ruby18.dll から見つかりませんでした。 

というエラーが発生することがありました。これは ruby\lib\ruby\1.8\i386-mswin32\thread.so が rb_thread_status を探すようになっているのが原因のようですが、

によると、

One Click Ruby Installer版ではなく ActiveScriptRubyを使う

ことで解決するとかなんとか?なので、とりあえず ActiveScriptRubyから thread.so を拝借して動作させました。

コンパイルしたいソースと同じバージョンの ActiveScriptRuby から持ってこないと、

ruby/lib/ruby/1.8/i386-mswin32/rbconfig.rb:7: ruby lib version (1.8.6) doesn't match executable version (1.8.7) (RuntimeError)

みたいなエラーになるので注意です。

元々使っていたバージョンが

ruby 1.8.6 (2007-09-24 patchlevel 111) [i386-mswin32]

なので、このバージョン持ってきて・・・

svn co http://svn.ruby-lang.org/repos/ruby/tags/v1_8_6_111
mkdir build
cd build
..\win32\configure.bat --with-winsock2
nmake

こんな感じでコンパイル→できあがった ruby.exe と msvcrt-ruby18.dll で InstantRais の配下のものを置き換えて起動、と。

すると・・・もともと 2〜3 秒で終わっていたページ表示が 7〜8 秒かかるようになってしまいましたorz
これじゃ検証にならない、ということでこの道はあきらめたのでした。

ただ、単純なスクリプトでは自前ビルドの ruby でもそんなに速度が変わらなかったりして、かなり不思議な挙動になっていました。Windows で C をコンパイルしたときってたまにこういう現象があって、Hello, world レベルのプログラムを起動するのに 1 秒くらいかかることがあるんだけど、何が原因なんだろう・・・。

その他試したかったこととか

  • ちゃんと Windows/Linus 両方の環境でプロファイラを使って計測して、どこの処理が Windows で遅くなっているか確認したかったんだけど、Windows 上で使えるフリーのプロファイラが見当たらなくて諦めた。(Dev Partner Profile Community Edition ってのがあったらしいんだけど、今では公開されていない模様。)
  • Rubinius とやらの動作も確認したかったけど、Windows で動作しないようだから特に何もせず。

私は開発効率にこだわりがある人間なので、今までは「ネイティブ動作させて少しでも高速に動作させよう」というスタンスだったんですが・・・Windows 上での動作がどうしても遅くなってしまうというのなら、次回以降 Ruby の開発では VM 上で行うのもやむなしですね。

これを口実に経費で MacBook を買う手もあるかもしれないけど・・・開発者の人数が読めないし、現実的じゃないよなぁ・・・。

実際のスクリプト

最後に、計測に使ったスクリプトを貼っておきます。計測に影響でそうな処理があればご指摘いただけると。

bench-ruby.rb - Ruby のファイル I/O 等
t_o = Time.new
str = "abcdef\nghijklmno\npqrstu\nvwxyz\n0123456879"

puts "print string to file"
t = Time.new
open("test.txt","w") do |file|
  1.upto(500000) do |n|
    file.print str,"\n"
  end
end
print Time.new-t,"\n\n"

puts "read from file ,and regexp match ,and print to file"
t = Time.new
reg = Regexp.new("abcdef|nvwxyz|5687")
open("test2.txt","w") do |file2|
  open("test.txt") do |file3|
    file3.each_line do |line|
      line =~ reg
      file2.print line
    end
  end
end
print Time.new-t,"\n\n"

File.unlink("test.txt")
File.unlink("test2.txt")

puts "Finished,press any key"
key = gets
tak-ruby.rb - Rubyたらいまわし
puts "calculating tak"
t = Time.new

def tak(x, y, z)
  if x <= y
    y
  else
    tak(tak(x-1, y, z),
        tak(y-1, z, x),
        tak(z-1, x, y))
  end
end
x, y, z = ARGV.map{|i| Integer(i) }
tak = tak(x, y, z)

print Time.new-t,"\n\n"
puts "tak(#{x}, #{y}, #{z}) = #{tak}"
bench-perl.pl - Perl のファイル I/O 等
use strict;
use warnings;
use Time::HiRes;

my $str = "abcdef\nghijklmno\npqrstu\nvwxyz\n0123456879";

print "print string to file\n";
my $t = Time::HiRes::time();
open my $out, '>', "test.txt" or die "$!";
for (my $i = 1; $i <= 500000; $i++) {
	print $out "$str\n";
}
close $out;
print+ (Time::HiRes::time() - $t) . "\n\n";

print "read from file ,and regexp match ,and print to file\n";
$t = Time::HiRes::time();
my $re = qr/abcdef|nvwxyz|5687/;
open my $out2, '>', "test2.txt" or die "$!";
open my $in, '<', "test.txt" or die "$!";
my $line;
while ($line = <$in>, defined $line) {
	$line =~ $re;
	print $out2 $line;
}
close $in;
close $out2;

print+ (Time::HiRes::time() - $t) . "\n\n";

unlink("test.txt");
unlink("test2.txt");

print "Finished,press any key\n";
my $key = <STDIN>;
tak-perl.pl - Perlたらいまわし
use strict;
use warnings;
use Time::HiRes;

print "calculating tak\n";

my $t = Time::HiRes::time();

sub tak {
	my ($x, $y, $z) = @_;

	if ($x <= $y) {
		return $y;
	}
	else {
		return tak(
			tak($x - 1, $y, $z),
			tak($y - 1, $z, $x),
			tak($z - 1, $x, $y)
		);
	}
}

my ($x, $y, $z) = map { int $_ } @ARGV;
my $tak = tak($x, $y, $z);

print+ (Time::HiRes::time() - $t) . "\n\n";

print "tak($x, $y, $z) = $tak\n";