一部のデータフレームにおいてfor ループがcolMeansより速い理由

先日、タイムラインを眺めていたらこんなエントリーが出てきました。
for ループが遅いなんて,誰が言った? - 裏 RjpWiki

「データフレームの場合は colMeans より for 文の方が速い」という奇をてらうような内容です。
どうしてそうなるかについて全く解説されておらず、限定的な話なのにあたかも普遍的な事実であるかのような書き方がされています。
間違った認識が広まってしまうとよろしくないのでちゃんと検証しました。

そもそもデータフレームだとどうして遅くなるか?

colMeans の一番最初に次のような処理があります。

    if (is.data.frame(x)) 
        x <- as.matrix(x)

つまり、一番最初に行列に変換するので、後の処理は行列の場合と全く同じということです。
よって、行列に変換するところがボトルネックになっていることは明らかです。

実際、次の方法で確認することができます。

> nr <- 10000
> nc <- 1000
> set.seed(1)
> d <- as.data.frame(matrix(rnorm(nr * nc), nr, nc))
> Rprof(interval = 0.001); invisible(colMeans(d)); Rprof(NULL)
> summaryRprof()
$by.self
                       self.time self.pct total.time total.pct
"as.matrix.data.frame"     0.066    79.52      0.081     97.59
"unlist"                   0.013    15.66      0.013     15.66
"levels"                   0.002     2.41      0.002      2.41
"colMeans"                 0.001     1.20      0.083    100.00
"as.matrix"                0.001     1.20      0.082     98.80

$by.total
                       total.time total.pct self.time self.pct
"colMeans"                  0.083    100.00     0.001     1.20
"as.matrix"                 0.082     98.80     0.001     1.20
"as.matrix.data.frame"      0.081     97.59     0.066    79.52
"unlist"                    0.013     15.66     0.013    15.66
"levels"                    0.002      2.41     0.002     2.41

$sample.interval
[1] 0.001

$sampling.time
[1] 0.083

この結果から、先ほどのエントリーと同じ形式のデータだと as.matrix の部分で colMeans 全体の処理時間のうち約99%を占めていることがわかります。

再検証

ベンチマークで利用しているデータは10,000行1,000列のデータフレームですが、for 文が有利なように恣意的に設定されている気がします。
colMeans のボトルネックは as.matrix であり、for 文のボトルネックは R 側で行われる繰り返し処理そのものです。
よって、同じデータサイズであっても1,000行10,000列のデータフレームであれば for 文が極端に遅くなります。

> library(rbenchmark)
> nr <- 10000
> nc <- 1000
> set.seed(1)
> d <- as.data.frame(matrix(rnorm(nr * nc), nr, nc))
> # 10,000行1,000列だと for 文の方が遅い
> benchmark({
+         m1 <- numeric(nc)
+         for (i in 1:nc) {
+             m1[i] <- mean(d[, i])
+         }
+     },
+           colMeans(d))
         test replications elapsed relative user.self sys.self user.child
1           {          100   8.778 1.000000     8.737    0.028          0
2 colMeans(d)          100  20.765 2.365573    16.562    4.173          0
  sys.child
1         0
2         0
> # 1,000行10,000列だと colMeans の方が速い
> d2 <- as.data.frame(t(d))
> benchmark({
+         m2 <- numeric(nr)
+         for (i in 1:nr) {
+         m2[i] <- mean(d2[, i])
+         }
+     },
+           colMeans(d2))
          test replications elapsed relative user.self sys.self user.child
1            {          100 105.201 3.116697   100.574    4.451          0
2 colMeans(d2)          100  33.754 1.000000    29.415    4.283          0
  sys.child
1         0
2         0

列数が10倍になると for 文の処理時間も約10倍になっていることがわかるかと思います。10,000個の値に対して平均を取るのも1,000個の値に対して平均を取るのも処理時間的には大差ないので、繰り返し回数が増えた分だけ遅くなっているわけです。

結論

以上をまとめると

  • データサイズが大きいデータフレームは as.matrix がボトルネックになる
  • for 文は繰り返し回数に比例して遅くなる
  • colMeans が速いか for 文が速いかはケースバイケース

と言えます。個人的には for 文の方が速いかどうかなんて気にせず一貫して colMeans を使えば良いのではないかと思います。

ちなみに今回検証に使ったマシンのスペックは次のとおりです。

  • OS: Mac OS X 10.6.8
  • CPU: 2.4 GHz Intel Core i5
  • R: 2.14.1