CでRubyを低速化
「Rubyによる情報科学入門」(久野靖著)に、グラフィックバッファの例が出てたので、その演習の一部をやってみた。Rubyの構造体(Structクラス)を2次元配列にしてバッファを確保して、ピクセル単位でRGB値をいじるという例。PPMフォーマットがえらく手軽なのでやってみたくなったというところ。せっかくなので、既存グラフィックの読み込み(load)も入れてみた。
#!/usr/local/bin/ruby # Pixel = Struct.new(:r, :g, :b) class Buffer attr_accessor :buf attr_reader :x, :y def initialize (x, y) @x, @y = x, y @buf = Array.new(y) do Array.new(x) {Pixel.new(255,255,255) } end end def Buffer.load (filename) File.open(filename, "r") do |f| f.gets # assuming it's P6 header, and do nothing. width, height = f.gets.split(" ") b = new(width.to_i, height.to_i) f.gets # dump 255(8bit depth color). (0...b.y).each do |i| (0...b.x).each do |j| pixel = f.read(3).unpack('CCC') b.buf[i][j] = Pixel.new(*pixel) end end b end end def save_as (name) File.open(name, "w") do |f| f.puts("P6\n"+@x.to_s+" "+@y.to_s+"\n255") # header for ppm format @buf.each do |line| line.each do |p| f.write(p.to_a.pack('CCC')) end end end end end def fill_circle (buf, x, y, r, color, *opc) if opc.size == 1 then return fill_opaque_circle(buf, x, y, r, color, *opc) end (0...buf.y).each do |i| (0...buf.x).each do |j| if (y - i)**2 + (x -j)**2 < r**2 then buf.buf[i][j] = color end end end end def fill_opaque_circle (buf, x, y, r, color, opc) back = (opc.to_f / 100) fore = ((100 - opc).to_f / 100) (0...buf.y).each do |i| (0...buf.x).each do |j| if (y - i)**2 + (x -j)**2 < r**2 then old = buf.buf[i][j].r buf.buf[i][j].r = (color.r * fore) + (old * back) old = buf.buf[i][j].g buf.buf[i][j].g = (color.g * fore) + (old * back) old = buf.buf[i][j].b buf.buf[i][j].b = (color.b * fore) + (old * back) end end end end if __FILE__ == $0 # b = Buffer.new(320, 240) b = Buffer.load("toy.ppm") 0.upto(30) do x = rand(b.x) y = rand(b.y) r = rand(40) + 10 col = Pixel.new(rand(255), rand(255), rand(255)) fill_circle(b, x, y, r, col, 60) end b.save_as("test.ppm") end
「b = Buffer.new(320, 240)」と、まっさらなバッファを用意して円を塗りつぶした例は、
これをアルファブレンドにしたものは、
「b = Buffer.load("toy.ppm")」として既存画像を読み込んで、その上に円を重ねていく。
ピクセル単位で扱えるようになったので、何か面白いことをやろうと思って、100個とか1万個の円オブジェクトをランダムに変化させて、既存画像との差分を縮めていくというテーマを思いついた。差分は2画像の同一位置のピクセルの色差で計算できる。ランダムに2つ変化させて、より既存画像に近いほうを選択する、という操作を繰り返していけば、きっと円が重なって何かボンヤリを写真が浮かび上がるアートが作れるような気がした。
しかし、100個の円の塗りつぶしで6秒とかだと、あまりに遅いので、Rubyでは無理っぽい。まずプロファイルを取ってみた。
% ruby -r profile paint.rb % cumulative self self total time seconds seconds calls ms/call ms/call name 61.25 96.40 96.40 2892 33.33 101.57 Range#each 10.97 113.66 17.26 2534400 0.01 0.01 Fixnum#** 7.11 124.85 11.19 1689611 0.01 0.01 Fixnum#- 3.91 131.00 6.15 844800 0.01 0.01 Fixnum#< 3.37 136.31 5.31 844811 0.01 0.01 Fixnum#+ 2.48 140.22 3.91 241 16.22 62.66 Array#each 2.15 143.61 3.39 451104 0.01 0.01 Array#[] 1.91 146.62 3.01 153611 0.02 0.02 Pixel#new 1.11 148.37 1.75 76800 0.02 0.03 Array#pack 0.84 149.69 1.32 172527 0.01 0.01 Fixnum#* 0.69 150.78 1.09 241 4.52 24.19 Array#initialize 0.49 151.55 0.77 153611 0.01 0.01 Struct#initialize 0.44 152.25 0.70 78951 0.01 0.01 Float#to_int 0.41 152.89 0.64 76802 0.01 0.01 IO#write 0.38 153.49 0.60 93576 0.01 0.01 Float#+ 0.37 154.07 0.58 76800 0.01 0.01 IO#read 0.37 154.65 0.58 62384 0.01 0.01 Pixel#g 0.35 155.20 0.55 76800 0.01 0.01 Struct#to_a 0.32 155.71 0.51 76800 0.01 0.01 String#unpack 0.26 156.12 0.41 62384 0.01 0.01 Pixel#b 0.20 156.44 0.32 62384 0.01 0.01 Pixel#r 0.20 156.75 0.31 76800 0.00 0.00 Array#[]= 0.13 156.95 0.20 31192 0.01 0.01 Pixel#r= 0.11 157.13 0.18 31192 0.01 0.01 Pixel#b= 0.10 157.29 0.16 31192 0.01 0.01 Pixel#g= 0.06 157.39 0.10 14625 0.01 0.01 Float#* 0.01 157.40 0.01 242 0.04 36.20 Class#new 0.00 157.40 0.00 11 0.00 0.00 Array#size 0.00 157.40 0.00 11 0.00 0.00 Fixnum#== 0.00 157.40 0.00 22 0.00 0.00 Fixnum#to_f 0.00 157.40 0.00 22 0.00 0.00 Float#/ 0.00 157.40 0.00 66 0.00 0.00 Kernel.rand 0.00 157.40 0.00 1 0.00 11490.00 Buffer#load 0.00 157.40 0.00 2 0.00 9520.00 IO#open 0.00 157.40 0.00 2 0.00 0.00 IO#close 0.00 157.40 0.00 1 0.00 2920.00 Buffer#initialize 0.00 157.40 0.00 2 0.00 0.00 IO#set_encoding 0.00 157.40 0.00 2 0.00 0.00 String#to_i 0.00 157.40 0.00 1 0.00 0.00 String#split 0.00 157.40 0.00 3 0.00 0.00 IO#gets 0.00 157.40 0.00 2 0.00 0.00 File#initialize 0.00 157.40 0.00 1 0.00 0.00 String#== 0.00 157.40 0.00 11 0.00 12576.36 Object#fill_opaque_circle 0.00 157.40 0.00 1 0.00 0.00 Module#attr_reader 0.00 157.40 0.00 1 0.00 16440.00 Integer#upto 0.00 157.40 0.00 2 0.00 0.00 Fixnum#to_s 0.00 157.40 0.00 4 0.00 0.00 String#+ 0.00 157.40 0.00 1 0.00 0.00 Module#attr_accessor 0.00 157.40 0.00 1 0.00 0.00 IO#puts 0.00 157.40 0.00 1 0.00 0.00 Struct#new 0.00 157.40 0.00 14 0.00 0.00 Module#method_added 0.00 157.40 0.00 4 0.00 0.00 BasicObject#singleton_method_added 0.00 157.40 0.00 2 0.00 0.00 Class#inherited 0.00 157.40 0.00 1 0.00 7550.00 Buffer#save_as 0.00 157.40 0.00 1 0.00 157400.00 #toplevel
だんぜんピタゴラスの定理の計算に時間がかかってるっぽい。なので、Rubyinlineを使って判定の部分だけCにしてみた。
requre "inline" class Test inline do |builder| builder.c <<-EOF int within_circle (int x, int y, int i, int j, int r) { int res; res = r^2 - (y - i)^2 - (x - j)^2; return INT2FIX(res); } EOF end end def fill_opaque_circle (buf, x, y, r, color, opc) back = (opc.to_f / 100) fore = ((100 - opc).to_f / 100) test = Test.new (0...buf.y).each do |i| (0...buf.x).each do |j| # if (y - i)**2 + (x -j)**2 < r**2 then if test.within_circle(x, y, i, j, r) > 0 then old = buf.buf[i][j].r buf.buf[i][j].r = (color.r * fore) + (old * back) : : :
ところが、純Rubyで100個の円を描くのに6秒かかっていたのに対して、inlineでCにしたはずが、22秒とはるかに遅くなってしまった。何かが根本的にダメらしい。ていうか、バイト列を扱うだけなら、最初からCでいいやんかという気もしてきた。
追記:Cのべき乗の演算子を間違えていた……。「x^2」はXORで、正しくは「x*x」とするか、math.hのpow()を使うか。x*xのように展開したら、ちゃんと動いて、100個の円の塗りつぶしに9秒ほどかかるところが6秒に縮まった。劇的ではないけど高速化してる。