行列乗算(matmul)ベンチマークを Squeak Smalltalkで


Programming Languages Benchmarks にある、行列の乗算を使ったベンチマーク(matmul)をいくつかの方法で書いて速度を測ってみる試みです。

まずは、Ruby版の matmul_v1.rb機械的Squeak Smalltalk 向けに変換。

| ans |
{[ | matmul matgen N A B C |
   matmul := [:a :b |
      | m n p b2 c1 |
      m := a size.
      n := (a at: 1) size.
      p := (b at: 1) size.

      b2 := Array new: n streamContents: [:ss |
         n timesRepeat: [ss nextPut: (Array new: p withAll: 0)]].
      1 to: n do: [:i |
         1 to: p do: [:j |
            (b2 at: j) at: i put: ((b at: i) at: j).
         ]
      ].

      c1 := Array new: m streamContents: [:ss |
         m timesRepeat: [ss nextPut: (Array new: p withAll: 0)]].
      1 to: m do: [:i |
         1 to: p do: [:j |
            | s ai b2j |
            s := 0.
            ai := a at: i.
            b2j := b2 at: j.
            1 to: n do: [:k |
               s := (ai at: k) * (b2j at: k) + s.
            ].
            (c1 at: i) at: j put: s.
         ]
      ].
      c1
   ].

   matgen := [:n |
      | tmp a |
      tmp := 1.0 / n / n.
      a := Array new: n streamContents: [:ss |
         n timesRepeat: [ss nextPut: (Array new: n withAll: 0)]].
      1 to: n do: [:i |
         1 to: n do: [:j |
            (a at: i) at: j put: tmp * (i - j) * (i + j - 2).
         ]
      ].
      a
   ].

   N := 500.
   N := N // 2 * 2.
   A := matgen value: N.
   B := matgen value: N.
   C := matmul value: A value: B.
   ans := (C at: N//2+1) at: N//2+1
] timeToRun milliSeconds. ans}

"=> {0:00:00:12.436 . -47.6671666664} "


計測の結果は 12.4秒ほど。ちなみに同条件(n=500)で Ruby1.9 は 98.2秒。


さて。コードのほうですが、速度重視で書くとこうなってしまうのか、メモリを確保し、ループで手続き的に愚直に回しています。ただやっぱりもう少し Smalltalk っぽくしたいので、こんなふうに書き直してみました。

| ans |
{[ | matmul matgen N A B C |
   matmul := [:a :b |
      | n b2 |
      n := a first size.
      b2 := (1 to: n) collect: [:i | b collect: [:each | each at: i]].
      a collect: [:ai | b2 collect: [:b2j |
         " (ai * b2j) sum "
         | s |
         s := 0.
         ai with: b2j do: [:ak :bk | s := ak * bk + s].
         s
      ]]
   ].

   matgen := [:n |
      | tmp |
      tmp := 1.0 / n / n.
      (1 to: n) collect: [:i | (1 to: n) collect: [:j | tmp * (i - j) * (i + j - 2)]]
   ].

   N := 500.
   N := N truncateTo: 2.
   A := matgen value: N.
   B := matgen value: N.
   C := matmul value: A value: B.
   ans := (C at: N//2+1) at: N//2+1
] timeToRun milliSeconds. ans}

"=> {0:00:00:15.293 . -47.6671666664} "


計測結果は 15.3秒と若干遅くなりますが、ちょっとスリムになりました。

じつはコメントアウトしてあるように、Squeak独自の APL風な機能を使ってもう一段シンプルにしたかったのですが、配列同士の演算でいちいち新しい配列を生成するのに思ったよりコストが高く付くようでかなり遅くなってしまいました。ちょっと残念。

| ans |
{[ | matmul matgen N A B C |
   matmul := [:a :b |
      | n b2 |
      n := a first size.
      b2 := (1 to: n) collect: [:i | b collect: [:each | each at: i]].
      a collect: [:ai | b2 collect: [:b2j | (ai * b2j) sum]]
   ].

   matgen := [:n |
      | tmp |
      tmp := 1.0 / n / n.
      (1 to: n) collect: [:i | (1 to: n) collect: [:j | tmp * (i - j) * (i + j - 2)]]
   ].

   N := 500.
   N := N truncateTo: 2.
   A := matgen value: N.
   B := matgen value: N.
   C := matmul value: A value: B.
   ans := (C at: N//2+1) at: N//2+1
] timeToRun milliSeconds. ans}

"=> {0:00:00:44.443 . -47.6671666664} "


ちなみに、組み込みの行列モドキ(Matrix)を使って書くとこうなります。

| ans |
{[ | N tmp A B |
   N := 500.
   tmp := 1.0 / N / N.
   A := Matrix new: N tabulate: [:i :j | tmp * (i - j) * (i + j - 2)].
   B := Matrix new: N tabulate: [:i :j | tmp * (i - j) * (i + j - 2)].
   ans := A +* B at: N//2+1 at: N//2+1
] timeToRun milliSeconds. ans}

"=> {0:00:00:43.133 . -47.6671666664} "


コードとしてはぐっとシンプルになって良い感じですが、やはりまあコストはそれなりにかかってしまうようでベンチマーク向きではないですね。^^;

ちなみに Ruby でも require "matrix" して Matrix とその演算を使用した matmul_v2.rb での同条件での計測は 180.2秒と、配列を用いループで回す v1 より遅くなるようです。