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秒に縮まった。劇的ではないけど高速化してる。