Cの拡張ライブラリを作ってみた

Cを使った拡張ライブラリが簡単に書けるという噂があるので、昨日つくった「各桁に1から9が必ず1度だけ現れる」ということを判定する関数の部分だけ、RubyからCに置き換えたら高速化するかどうか試してみた。

元のコードは、以下。

def has9digits? (num)
  digits = num.to_s.split(//).sort.join
  digits == "123456789"
end

10000.upto(33333) do |i|
  print i," * ",i," = ",i**2,"\n" if has9digits?(i**2)
end

このhas9digits?がいかにも遅い。Ruby用拡張ライブラリをCで作るというページを参考に、以下の2つのファイルを用意した。

#include <ruby.h>

VALUE uniq(VALUE self, VALUE n)
{
  VALUE r;

  int i, num, amari;
  int flag = 1;
  int d[10] = {0};

  num = NUM2INT(n);

  for (i = 1; i < 10; i++)
    {
      amari = num % 10;
      d[amari]++;
      num = num / 10;
    }

  for (i = 1; i < 10; i++)
    flag *= d[i];

  r = INT2FIX(flag);
  return r;
}

void Init_Digit(void){
  VALUE rb_cDigit;

  rb_cDigit = rb_define_class("Digit", rb_cObject);

  rb_define_method(rb_cDigit, "uniq", uniq, 1);
}

(digit.c)
require "mkmf"
create_makefile("Digit")

(extconf.rb)

それで、ruby extconf.rbとやるとMakefileができて、makeするとDigit.soができた。これを使って、

require 'Digit.so'

d = Digit.new

10000.upto(33333) do |i|
  print i," * ",i," = ",i**2,"\n" if (d.uniq(i**2) == 1)
end

(c9digits.rb)

を実行すると、ちゃんと動いた。Rubyのみで書いたものと、Cの拡張ライブラリを使って計算したもので実行時間を測ると、

$ time ruby 9digits.rb >/dev/null

real	0m0.678s
user	0m0.664s
sys	0m0.020s

$ time ruby c9digits.rb >/dev/null

real	0m0.175s
user	0m0.152s
sys	0m0.024s

おおぉ、速い! 当たり前だけど。

こうやって自分で書いてみると、RHGを読んでいたときには「そんなもんか、ふーん」と思っていたVALUEやらrb_define_methodも、なるほどという感じ。NUM2INT辺りのマクロは型チェックもしてくれているらしいけど、便利にできてるなぁ。