14.9 より透明性のあるキャッシュの仕組み

11.4節で紹介した knitr のキャッシュの仕組みが複雑すぎると思ったら (実際そうです!), xfun::cache_rds() 関数に基づいた, より簡単なキャッシュの仕組みを検討してください. これが例です.

xfun::cache_rds({
  # ここに時間のかかるコードを書く
})

knitr のキャッシュは, キャッシュの無効化のタイミングがどう決定されるかという点が難解なのです. xfun::cache_rds() においては, これはずっと明確です. 最初に R コードをこの関数に与えたときは, コードが評価され結果が .rds ファイルに保存されます. 次に cache_rds() を再実行すると, .rds ファイルを読み込み, コードを再び評価することなく直ちに結果を返します. キャッシュを無効化する最も明確な方法は, .rds ファイルを削除することです. 手動で削除したくないなら, xfun::cache_rds()rerun = TRUE 引数を付けて呼び出します.

knitr のソース文書上のコードチャンクで xfun::cache_rds() が呼び出された時, .rds ファイルのパスはチャンクオプション cache.path とチャンクラベルによって決定します. 例えば input.Rmd という Rmd 文書に foo というチャンクラベルのあるコードチャンクがあるとします.

```{r, foo}
res <- xfun::cache_rds({
  Sys.sleep(3)
  1:10
})
```

.rds ファイルのパスは input_cache/FORMAT/foo_HASH.rds という形式になります. ここで FORMAT は Pandoc の出力フォーマット名 (例えば html あるいは latex) であり, HASH は a-z および 0-9 からなる32桁の16進 MD5 ハッシュ値です. 例えば input_cache/html/foo_7a3f22c4309d400eff95de0e8bddac71.rds のようになります.

?xfun::cache_rds のヘルプで言及されているように, キャッシュを無効化したいであろう2つのよくあるケースがあります. (1) 評価式が変更された時, (2) 評価式の外部の変数が使用され, その変数の値が変更された時です. 次に, この2つのキャッシュ無効化の方法がどう動作するのかと, 異なるコードのバージョンに対応する複数のキャッシュのコピーをどう保持するかを説明します.

14.9.1 コードの変更によってキャッシュを無効化する

例えば cache_rds({x + 1}) から cache_rds({x + 2}) へと, cache_rds() 内のコードを変更したとき, キャッシュは自動で無効化され, コードは再評価されます. しかし, 空白やコメントの変更は問われないことに注意してください. あるいは一般論として, パースされた表現に影響のない範囲の変更ではキャッシュは無効化されません. 例えば cache_rds() にパースされた以下2つのコードは本質的に同等です.

res <- xfun::cache_rds({
  Sys.sleep(3  );
  x<-1:10;  # セミコロンは問題ではない
  x+1;
})

res <- xfun::cache_rds({
  Sys.sleep(3)
  x <- 1:10  # これはコメント
  x +
    1  # 空白の変更は完全に自由
})

つまり, 最初のコードを cache_rds() で実行したなら, 2度目のコードはキャッシュの利便性を得られます. この仕様のおかげでキャッシュを無効化することなくコードの見た目を整える変更ができます.

2つのバージョンのコードが同等であるか自信がないなら, 以下の parse_code() を試してください.

parse_code <- function(expr) {
  deparse(substitute(expr))
}
# 空白とセミコロンは影響しない
parse_code({x+1})
## [1] "{"         "    x + 1" "}"
parse_code({ x   +    1; })
## [1] "{"         "    x + 1" "}"
# 左アロー演算子と右アロー演算子は同等
identical(parse_code({x <- 1}), parse_code({1 -> x}))
## [1] TRUE

14.9.2 グローバル変数の変更によってキャッシュを無効化する

変数にはグローバルとローカル変数の2種類があります. グローバル変数は評価式の外部で作られ, ローカル変数は評価式の内部で作られます. キャッシュされた結果は, 評価式内のグローバル変数の値が変われば, もはや再度実行して得られるはずの結果を反映していません. 例えば以下の評価式で, y が変化したなら, あなたが一番やりたいのはきっと, キャッシュを無効化して評価をやり直すことでしょう. さもなければ古い y の値を維持したままになってしまいます.

y <- 2

res <- xfun::cache_rds({
  x <- 1:10
  x + y
})

y が変化した時にキャッシュを無効化するには, キャッシュを無効化すべきかを決定する際に y も考慮する必要があることを, hash 引数を通して cache_rds() に教えてあげることもできます.

res <- xfun::cache_rds({
  x <- 1:10
  x + y
}, hash = list(y))

hash 引数の値が変化した時, 前述のキャッシュファイル名に含まれる32桁のハッシュ値も対応して変化するため, キャッシュは無効化されます. これで他の R オブジェクトとキャッシュの依存関係を指定する手段を得ました. 例えば R のバージョンに依存してキャッシュを取りたいなら, このようにして依存関係を指定することもできます.

res <- xfun::cache_rds({
  x <- 1:10
  x + y
}, hash = list(y, getRversion()))

あるいはデータファイルが最後に修正されたタイミングに依存させたいなら, こうします.

res <- xfun::cache_rds({
  x <- read.csv("data.csv")
  x[[1]] + y
}, hash = list(y, file.mtime("data.csv")))

hash 引数にこのグローバル変数のリストを与えたくなければ, 代わりに hash = "auto" を試しましょう. これは cache_rds() に全てのグローバル変数を自動的に把握するよう指示し, 変数の値のリストを hash 引数の値として使わせます.

res <- xfun::cache_rds({
  x <- 1:10
  x + y + z  # y と z はグローバル変数
}, hash = "auto")

これは以下と同等です.

res <- xfun::cache_rds({
  x <- 1:10
  x + y + z  # y と z はグローバル変数
}, hash = list(y = y, z = z))

hash = "auto" とした時, グローバル変数は codetools::findGlobals() によって識別されます. これは完全に信頼できるものではありません. あなたのコードを一番良く知っているのはあなた自身ですので, hash 引数には明示的に値のリストを指定して, どの変数がキャッシュを無効化できるかを万全にすることをお薦めします.

14.9.3 キャッシュの複数のコピーを保持する

キャッシュは典型的には時間のかかるコードに対して使用されるので, きっとあなたは無効化することに対して躊躇するでしょう. キャッシュを無効化するのが早すぎたり, 積極的すぎたりしたことを後悔するかもしれません. もし古いバージョンのキャッシュが再び必要になったら, 再現のために長い計算時間を待たなければなりませんから.

cache_rds()clean 引数を FALSE に設定すれば, キャッシュの古いコピーを保持できます. R のグローバルオプション options(xfun.cache_rds.clean = FALSE) の設定で, この挙動を R セッション全体を通したデフォルトにもできます. デフォルトでは, clean = TRUEcache_rds() は毎回, 古いキャッシュを削除しようと試みます. clean = FALSE の設定は, まだコードを試行錯誤しているうちは有用になりえます. 例えば, 2つのバージョンの線形モデルのキャッシュを取ることができます.

model <- xfun::cache_rds({
  lm(dist ~ speed, data = cars)
}, clean = FALSE)

model <- xfun::cache_rds({
  lm(dist ~ speed + I(speed^2), data = cars)
}, clean = FALSE)

どちらのモデルを使うかを決めたら, clean = TRUE を再度設定するか, この引数を消すことでデフォルトの TRUE に戻すことができます.

14.9.4 knitr のキャッシュ機能との比較

knitr キャッシュ, つまりチャンクオプション cache = TRUE と, xfun::cache_rds() をそれぞれいつ使えばよいのか迷っているかもしれません. xfun::cache_rds() の最大の欠点は, 評価式の値のみをキャッシュしそれ以外の結果をキャッシュしないことです. その一方で knitr は評価式以外の値についてもキャッシュを取ります. 出力やグラフを表示するといった評価式以外の結果には有用なものもあります. 例えば以下のコードでは, cache_rds() が次にキャッシュを読み込んだ時には, テキスト出力とグラフが失われてしまい, 1:10 という値だけが戻ってきます.

xfun::cache_rds({
  print("Hello world!")
  plot(cars)
  1:10
})

これと比較してオプション cache = TRUE のあるコードチャンクでは, 全てがキャッシュされます.

```{r, cache=TRUE}
print("Hello world!")
plot(cars)
1:10
```

knitr のキャッシュ機能の大きな欠点であると同時にユーザーが最もよく不満の対象とするのは, キャッシュがとても多くの要因で決まるため, 知らないうちに無効化してしまうことがある点です. 例えば, チャンクオプションのいかなる変更もキャッシュを無効化する可能性がありますが,40 演算に影響しないであろうチャンクオプションもあります. 以下のコードチャンクでチャンクオプション fig.width = 6fig.width = 10 へと変更してもキャッシュを無効化すべきではありませんが, 実際は無効化してしまいます.

```{r, cache=TRUE, fig.width=6}
# there are no plots in this chunk
x <- rnorm(1000)
mean(x)
```

実際に knitr のキャッシュはかなり強力で柔軟であり, 多くの方法で挙動を調整できます. あなたはキャシュがどう動作するのかを学び理解するのに, 最終的に計算するタスクの所要時間よりもはるかに多くの時間を費やしてしまうかもしれません. ですので私はパッケージの作者として, これらのあまり知られていない機能は紹介するに値するのかと, 疑問に思うことがよくあります.

まだはっきりわからない人は, xfun::cache_rds() は演算をキャッシュする一般的な方法でありどこでも動作しますが, 一方の knitr のキャッシュは knitr 文書でのみ動作すると覚えてください.


  1. これはデフォルトの挙動であり, 変更することができます. より細かい粒度でキャッシュを生成し, 全てのチャンクオプションがキャッシュに影響しないようにするには, https://gedevan-aleksizde.github.io/knitr-doc-ja/cache.html をご覧ください.↩︎