キャッシュの寿命はなぜ確率で扱うのか
2026年9月27日
著者: 竹洞 陽一郎
主なポイント
- キャッシュの領域は有限なので追い出しが起き、いつ追い出されるかはユーザーごとの閲覧量によって違い、事前には分からないため、キャッシュの寿命は生存関数 S(t) = P(T > t)(t の時点でまだ残っている確率)で扱います。
- 生存関数の形は追い出し方式で決まり、Chromiumは「経過時間×サイズ」、Firefoxは利用頻度と最終利用時刻(frecency)、WebKitはサイズとアクセス回数の組み合わせで追い出すため、S(t) はファイルの種類やサイズで層別して実測する必要があります。
- 純粋なLRUを仮定すると、1人の端末での寿命はキャッシュ容量 C ÷ 1日の新規取得量 r でほぼ決まり、C = 320MiBでは典型的なユーザーで2〜7日程度と概算されますが、これは仮定に基づく目安であり、設計値ではありません。
- Cheの近似によれば、LRUキャッシュには特性時間 TC があり、リクエスト頻度が 1/TC より低いオブジェクトはキャッシュを素通りするため、訪問頻度の低いユーザーには、訪問をまたいだブラウザキャッシュの再利用が効きません。
- 「大きいもの、使われないものから消し、有効期限は見ない」という点は、Chromium、Firefox、KHTML以来のWebKitで一致しており、ファイルの統合と削減がキャッシュの生存に効くという主張の根拠になっています。
「いつ消えるか」は、誰にも分からない
ブラウザキャッシュは何日残るのかでは、ブラウザのキャッシュが期待するほど長く残らないことを、仕様、実測データ、実装から示しました。
max-ageは何秒にすべきかでは、その事実を踏まえたmax-ageの決め方を説明しました。
この記事は、その2本の背後にある理論を扱います。
あるファイルがブラウザのキャッシュに、いつまで残っているかは、事前には分かりません。
追い出しは、そのユーザーが他のサイトをどれだけ見たかで決まるので、同じファイルでも、あるユーザーの端末では3日残り、別のユーザーの端末では6時間で消えます。
つまり、「t 時間後に残っているかどうか」は、ユーザーごとに違う、確率的な事柄です。
この記事では、この「確率的な寿命」を扱う道具として、統計学の生存時間解析を導入し、追い出し方式との関係、容量からの概算、そしてこの考え方がCDNやJVMにも及ぶことを説明します。
数学の記号は最小限にし、出てくる記号はその場で説明します。
キャッシュの領域は有限である
なぜ追い出しが起きるのか。
キャッシュの領域は有限だからです。
ブラウザのキャッシュは、端末の限られた容量を全サイトで共有しています。
CDNのエッジサーバーも同じで、メモリとディスクの二段階のキャッシュを持ち、どちらも有限です。
よくリクエストされるオブジェクトはメモリに置かれ、メモリからあぶれたものはディスクへ移り、ディスクでもリクエストされないままだと消えます(段階の構成や移動の基準は、CDNの実装によって異なります)。
ブラウザも、メモリキャッシュとディスクキャッシュの二段構えである点は同じです。
領域が有限で、そこに次々と新しいものが入ってくる以上、何かを追い出さなければなりません。
追い出しがあるということは、そこに置かれたものには必ず「寿命」があるということです。
そして、いつ追い出されるかが事前に分からないからこそ、寿命を確率として扱う必要が生まれます。
「有限だから追い出しが起き、追い出しがあるから寿命があり、寿命が確率的だから統計で扱う」という順序です。
何が「生き残る」かの条件は、領域によって違います。
| 領域 | 有限の資源 | 生き残る条件 | 追い出しの基準 |
|---|---|---|---|
| ブラウザキャッシュ | 端末のメモリとディスクのキャッシュ領域(全サイトで共有) | そのユーザーが使い続けること | Chromiumは最終利用からの経過時間×サイズ、Firefoxは利用頻度と最終利用時刻。加えて、ユーザーによる消去やパーティショニング |
| CDNのエッジキャッシュ | エッジサーバーのメモリとディスク(二段階) | 誰かがリクエストし続けること | 容量超過時の追い出しに加えて、CDNごとの非アクティブ期限(Gcoreは最終リクエストから36時間。Gcore Docs「CDN cache lifetime configuration」、2026年9月25日確認) |
| JVMのYoung世代(後述) | ヒープ | GCの時点で参照されていること(到達可能性) | 参照がなければ即回収。参照があるものは、生き延びた回数で昇格 |
ブラウザキャッシュの「使い続ける」は、そのユーザー1人がそのファイルを読み込み続けることです。
CDNは多数のユーザーのリクエストが1つのエッジサーバーに集まるので、1人がアクセスしなくても、他の誰かが来れば残ります。
ブラウザキャッシュは1人分しかないので、その人がアクセスしければ確実に消えていきます。
ブラウザキャッシュは何日残るのかで、ブラウザのキャッシュよりCDNのキャッシュのほうが当てになると書いたのは、この構造の違いによります。
生存時間解析と生存関数
このような「いつ消えるか分からないものが、時間 t の時点でまだ残っている確率」を扱う考え方は、統計学で生存時間解析と呼ばれています。
名前のとおり、もともとは「ある時点まで生き残る確率」を扱う手法です。
信頼性工学では、製造した部品の一群について、「稼働開始から t 時間後に故障せずに動いている部品の割合」を求めて、保証期間や交換周期を決めるのに使われています。
医学では、治療を受けた患者の一群について、「治療から t 年後に再発していない患者の割合」を求めて、治療法の効果を比較するのに使われています。
どちらも、共通する構造があります。
対象が多数あり、それぞれが「いつか終わる」のですが、いつ終わるかは1つずつ違い、事前には分かりません。
それでも、多数の対象を観察すれば、「t 時点で残っている割合」は、はっきりした形の曲線として現れます。
その曲線を生存関数と呼び、通常 S(t) と書きます。
生存関数を、もう少し正確に定義しておきます。
1つの対象が「終わる」までの時間を T とします(部品なら故障までの稼働時間、患者なら再発までの年数です)。
T は対象ごとに違う値をとるので、確率変数として扱います。
生存関数 S(t) は、「T が t より大きい確率」、つまり「t の時点でまだ終わっていない確率」です。
S(t) = P(T > t)
P( ) は確率(Probability)を表す記号で、P(T > t) は「T が t より大きくなる確率」と読みます。
確率は0から1までの値で、1は「必ずそうなる」、0は「決してそうならない」、0.5は「半々」です。
たとえば S(3) = P(T > 3) = 0.5 であれば、「終わるまでの時間が3より長い確率が0.5」、言い換えると「3の時点で残っているのは全体の半分」という意味です。
定義から、S(t) には次の性質があります。
- S(0) = 1:開始時点では、すべての対象が残っています
- S(t) は t が増えると減るか、変わらないかのどちらかで、増えることはありません(一度終わったものは戻らないため)
- t を大きくしていくと、S(t) は0に近づきます(いつかはすべて終わるため)
- 1 − S(t) は「t までに終わった割合」で、統計学でいう累積分布関数です
- S(t) = 0.5 になる t が、寿命の中央値です(半数がこの時点までに終わっています)
- S(t) の曲線の下側の面積が、寿命の平均値になります(連続時間では E[T] = ∫0∞ S(t) dt です)
実務では、S(t) を数式で与えるのではなく、多数の対象を観察した結果から曲線として推定します。
「100個の部品のうち、1,000時間後に80個が動いていれば、S(1,000時間) ≒ 0.8」という具合です。
図で示すと、次のようになります。
図1は、12個の対象それぞれの寿命を横棒で表したものです。
×印が「終わった時刻」で、部品なら故障、患者なら再発、キャッシュなら追い出しにあたります。
t = 3 の時点で縦線を引くと、12個のうち6個が残っているので、S(3) = 6/12 = 0.50 です。
図2は、この数え方を t のすべての値について行ったものです。
対象が12個しかないので階段状になりますが、対象を増やしていくと滑らかな曲線になります。
この曲線が生存関数 S(t) で、S(0) = 1 から始まって単調に下がり、0.5 を横切る t が寿命の中央値、曲線の下側の面積が寿命の平均値になります。
ただし、図1のように「終わった時刻」を直接観測できるのは、部品の故障や患者の再発のように、終了の瞬間を記録できる場合です。
キャッシュの場合、観測できるのは「ユーザーが再訪したときにヒットしたかミスしたか」だけで、追い出された時刻そのものは分かりません。
再訪がない期間は、キャッシュが「生き残っている」のか「観測できないだけ」なのかを区別できません(統計学ではこれを打ち切りと呼びます)。
また、キャッシュが消える原因は追い出しだけではなく、ユーザーによる消去、パーティショニング、ストレージの逼迫、プライベートブラウジングなどが競合します。
これらを踏まえて S(t) を推定する方法は、ブラウザキャッシュのヒット率を生存時間解析で実測するで扱います。
この記事に出てくる S(t) の数値は、すべて関数の形を説明するための仮の値で、実測値でも、層別する前の全ユーザーの平均値でもありません。
LRUキャッシュとは ― 追い出し方式の違いが生存関数の形を決める
生存関数の形を決めるのは、「何を追い出すか」を決める方式です。
その代表がLRU(Least Recently Used、最も長く使われていないもの)で、このシリーズで扱う実装はすべて、LRUを基本にした変種です。
LRUは、キャッシュがいっぱいになったときに、「最後に使われてから最も時間が経っているもの」を追い出す方式です。
仕組みは単純です。
各エントリについて「最後に使われた時刻」だけを記録しておき、新しいものを入れる場所が足りなくなったら、その時刻が最も古いエントリを捨てます。
エントリが読み込まれる(ヒットする)たびに、そのエントリの時刻が今に更新されるので、使われ続けているものは追い出されにくく、使われなくなったものは自然に古くなって追い出されます。
一列に並んだ本棚で、本を使ったら必ず列の先頭に戻し、棚がいっぱいになったら列の末尾の本を捨てる、と考えると分かりやすいです。
LRUが広く使われる理由は3つあります。
記録するのが最終利用時刻だけなので実装が簡単であること、「最近使われたものは近いうちにまた使われる」というWebアクセスやプログラムの一般的な性質(時間的局所性)に合っていること、そして、何が人気かを事前に推定する必要がなく、使われた事実だけで判断できることです。
| 方式 | 追い出す基準 | 特徴 |
|---|---|---|
| LRU(Least Recently Used) | 最後に使われてから最も時間が経ったもの | 最近の利用を重視する。一時的に大量のものが流れ込むと、よく使うものまで押し出される |
| LFU(Least Frequently Used) | 使われた回数が最も少ないもの | 人気を重視する。昔たくさん使われたものが居座りやすい |
| FIFO(First In, First Out) | 最も古く入ったもの | 使われたかどうかを見ない。最も単純 |
実際のブラウザやCDNは、純粋なLRUではなく、LRUに別の要素を組み合わせています。
max-ageは何秒にすべきかで示したChromiumとFirefoxのソースコード、および後述するWebKitとKHTMLのソースコードの内容を、この観点で並べると次のようになります。
| 実装 | 追い出しの基準 | LRUとの関係 |
|---|---|---|
| Chromium(ディスクキャッシュ) | 最終利用からの経過時間 × エントリのサイズ | LRUの基準にサイズを掛けたもの。大きいファイルほど早く追い出される |
| Firefox(cache2) | frecency(利用頻度と最終利用時刻の組み合わせ) | LRUとLFUの中間。頻繁に使われたものは、しばらく使われなくても残りやすい |
| WebKit / KHTML(メモリキャッシュ) | サイズとアクセス回数の組み合わせで分けた複数のLRUリスト(KHTMLは log2(サイズ) ÷ アクセス回数、WebKitは log2(サイズ ÷ アクセス回数)) | LRUリストを、サイズと人気度で層別したもの(LRU-SP) |
| Gcore CDN | 最終リクエストから36時間で削除 | 「最終利用からの経過時間」で判定する点はLRUと同じ考え方。加えて容量超過時の追い出し |
方式が違えば、生存関数の形も違います。
純粋なLRUなら、S(t) は「他のものがどれだけ流れ込んだか」だけで決まります。
Chromiumのようにサイズを掛ける方式では、大きいファイルの S(t) は小さいファイルより速く落ちます。
Firefoxのように頻度を加味する方式では、よく使われたファイルの S(t) は、しばらく使われなくても高いまま保たれます。
生存関数を実測するときにファイルの種類やサイズで層別する必要があるのは、このためです。
ブラウザキャッシュの生存関数
ブラウザのキャッシュは、生存時間解析の構造にそのまま当てはまります。
対象は「各ユーザーの端末に保存された、あるファイルのキャッシュ」です。
それぞれのキャッシュは、他のサイトの閲覧によっていつか追い出されますが、いつ追い出されるかはユーザーごとに違います。
部品の「故障」や患者の「再発」に相当するのが、キャッシュの「追い出し」です。
したがって、多数のユーザーを観察すれば、「前回そのファイルを読み込んでから t 経過した時点で、キャッシュがまだ残っているユーザーの割合」が、生存関数 S(t) として得られます。
S(0) は1(読み込んだ直後は全員のキャッシュに残っている)で、時間とともに下がり、いずれ0に近づきます。
仮の数値で示すと、次のような形です。
| 経過時間 t | S(t) | 意味 |
|---|---|---|
| 0 | 1.00 | 読み込んだ直後は、全員の端末のブラウザキャッシュに残っている |
| 6時間 | 0.88 | 9割近くのユーザーの端末のブラウザキャッシュに残っている |
| 1日 | 0.61 | 6割 |
| 3日 | 0.22 | 2割 |
| 7日 | 0.03 | ほぼ消えている |
| 30日 | 0.00 | 全てのユーザーの端末のブラウザキャッシュで残っていない |
グラフにすると、次のようになります。
表の値は、この曲線上の点です。
ここで、この S(t) がどこから来るのかを整理しておきます。
純粋なLRUで、他のファイルの流入が一定の率で続くなら、1人の端末におけるあるファイルの寿命は、「流入の累積がキャッシュ容量 C に達するまでの時間」で、ほぼ決定的に決まります。
(後述する C ÷ r がそれです)
つまり、事後に1人の端末を見れば、寿命は確率的というより、その人の閲覧量で決まる値です。
それでも設計の時点で確率を使うのは、その人の閲覧量が事前には分からず、しかもユーザーごとに大きく違うからです。
S(t) は「寿命が t より長いユーザーの割合」なので、閲覧量の分布がそのまま S(t) の形になります。
冒頭で「寿命は確率的な事柄」と書いたのは、この意味です。
上の表とグラフでは、その形を指数分布 S(t) = e−t/τ で近似しています。
(τ は平均寿命)
指数分布は、「すでに t だけ生き残った」という事実がその先の生存確率に影響しない、無記憶性という性質を持つ、扱いやすい作業用のモデルです。
実際のWebのアクセスには、ページを開くとCSS、JavaScript、画像が一斉にリクエストされるバースト性、同じユーザーが同じサイトを短時間に何度も訪れる時間的局所性、時間帯による日周変動があり、閲覧量の分布も一様ではないので、実際の生存関数は指数分布から外れ、初期に急に落ちて後半は緩やかに減る、右に裾を引く形になります。
指数分布は「計算できる仮定」として使い、実際の形はブラウザキャッシュのヒット率を生存時間解析で実測するで説明する方法で確かめる、という位置付けです。
キャッシュ容量から生存関数を概算する
容量が小さい端末や、他のサイトを多く見るユーザーほど、S(t) は速く0に近づきます。
S(t) は、実際のユーザーのブラウザで計測するRUM(Real User Monitoring)で実測するのが原則で、その方法はブラウザキャッシュのヒット率を生存時間解析で実測するで説明します。
ただし、容量から概算することもできます。
純粋なLRUを仮定すると、あるエントリが追い出されるのは、そのエントリを最後に使ってから、他のリソースの新規取得量がキャッシュ容量 C を超えたときです。
ユーザーの1日あたりの新規取得量を r とすると、平均的には C ÷ r の日数で消えます。
なお、以下の表では、キャッシュ容量はブラウザの実装に合わせて MiB(220 バイト)で、取得量はページの重さの統計に合わせて MB(106 バイト)で書いています。
320MiB は約335MBなので、「320MiB ÷ 100MB」は 335 ÷ 100 ≒ 3.4日 と計算しています。
この「C ÷ r」は、1人の端末で、他のファイルの流入によって自社サイトのファイルが押し出されるまでの時間の目安です。
これと似ていますが、別のものとして、LRUキャッシュの解析で標準的に使われるCheの近似(Che, Tung, Wang, 2002)があります。
Cheの近似では、各オブジェクトへのリクエストが独立なポアソン過程に従い、オブジェクトごとの人気度は大きく偏っていると仮定します。
ポアソン過程とは、事象(ここではリクエスト)が互いに独立に、一定の平均率 λ で起きるという仮定で、このとき一定時間あたりの事象の回数はポアソン分布に、事象と事象の間隔は指数分布に従います。
平たく言えば、「リクエストは、決まった周期ではなく、ばらばらの間隔で来る。ただし、長い目で見れば平均して1時間に λ 回来る」という前提で、コールセンターへの電話や店への客の到着を扱う待ち行列の理論と同じ仮定です。
この偏りは、Webのアクセスで「ロングテール」とよく言われるものです。
少数の人気ページにアクセスが集中し、大多数のページはまれにしかアクセスされない、という分布で、統計学ではZipf分布と呼ばれます(Breslau らが1999年に、Webのアクセスがこの分布に従うことを実測で示しています)。
そのうえで、「キャッシュに入ってから追い出されるまでの時間」がオブジェクトによらずほぼ一定の値になることを示し、これを特性時間 TC と呼びます。
リクエスト率 λ のオブジェクトのヒット率は、およそ 1 − e−λTC になります。
(これはオブジェクトごとのヒット率の式で、先に出てきた集団の生存関数 S(t) そのものではありません。指数の形が似ているだけです)。
原論文はこれを「キャッシュはローパスフィルタのように働く」と表現しています。
ローパスフィルタとは、電気回路や音響で使われる言葉で、ある周波数(カットオフ周波数)より低い周波数の信号だけを通し、それより高い周波数の信号を遮る装置のことです。
ここでは「周波数」を「リクエストの頻度」と読み替えます。
ただし、キャッシュの場合は向きが逆で、頻度が高いオブジェクトはキャッシュに留まり(ヒットし)、頻度が低いオブジェクトはキャッシュに留まれずオリジンへ通り抜けます。
つまり、オリジンから見ると、キャッシュは「頻度の低いリクエストだけを通すフィルタ」であり、そのカットオフ周波数が 1/TC です。
リクエスト率が 1/TC より低いオブジェクトは、キャッシュをほとんど素通りする(ヒットしない)ということです。
ロングテールの「尾」にあたる大多数のページは、まさにこの素通りする側です。
C ÷ r と TC の違いを整理しておきます。
C ÷ r は、バイト数の流入量から「押し出されるまでの時間」を出したもので、ファイルの人気度は考えていません。
TC は、多数のオブジェクトへのリクエストの分布(独立なポアソン過程と人気度の偏り)から導かれる、キャッシュ全体の特性値で、人気のあるオブジェクトはこの時間内に再参照されて生き残り、人気のないものは素通りする、という構造を含んでいます。
ブラウザキャッシュでは、さらに次の3つの要因が加わり、いずれもCheの近似の前提から外れる方向に働きます。
- r は端末全体の流入量である
-
Cheの近似は1つのキャッシュに来るリクエスト全体を対象にしますが、ブラウザキャッシュでは、自社サイトのファイルは他のサイトのファイルの流入に押し出されます。
自社サイトのファイルの人気度が高くても、そのユーザーが他のサイトを多く見れば消えます。 - サイズが追い出しの基準に含まれる
- Cheの近似はオブジェクトのサイズを均一とみなしますが、Chromiumはサイズを基準に掛けるので、大きいファイルは同じ経過時間でも先に消え、実効的な寿命が短くなります。
- パーティショニングで実効的な容量が減る
- キャッシュのキーに参照元のサイトが含まれるため、同じファイルでも参照元ごとに別のエントリになり、1つのキャッシュとして使える容量が分割されます。
したがって、表の「日数」は、純粋なLRU・均一なサイズ・一定の流入という仮定の下での目安であり、設計値として持ち帰るものではありません。
この見方は、ブラウザキャッシュにそのまま当てはまります。
あるユーザーにとって、自社サイトのファイルの「リクエスト率」とは、そのユーザーが自社サイトを訪れる頻度です。
訪問頻度が 1/TC (この記事の例では、数日に1回)より低ければ、そのユーザーのブラウザキャッシュは、訪問と訪問の間には自社サイトのファイルをほとんど保持できません。
月に1回しか来ない訪問者に対して、訪問をまたいだキャッシュの再利用は事実上機能しない、ということを、この理論は定量的に示しています。
ただし、その1回の訪問の中でページを回遊するときの再利用(セッション内のヒット)は別で、こちらは訪問頻度に関係なく効きます。
max-ageの設計で問題になるのは、訪問をまたいだ再利用のほうです。
| キャッシュ容量 C | 1日の新規取得量 r | 平均的な消失までの時間 |
|---|---|---|
| 320MiB | 100MB | 約3.4日 |
| 320MiB | 300MB | 約1.1日 |
| 320MiB | 1,000MB | 約8時間 |
| 1GiB | 300MB | 約3.6日 |
C の320MiBは、Chromiumの基準値80MiBの4倍を例示用の固定値として使ったもので、実際の値は端末の空き容量に応じて変わり、機種差も大きいです。
r については、ブラウザが1日に新規取得してキャッシュに保存するバイト数を直接計測した公開統計を、私は確認できていません。
そこで、一次資料のある数値から推定します。
HTTP ArchiveのWeb Almanac 2025によると、2025年7月時点のモバイルのトップページの重さの中央値は2,559KB(約2.6MB)、下層ページは2,362KBです。
これはキャッシュのない状態でクロールした値なので、「そのページを初めて訪問したときの取得量」に相当します。
r は「1日のページビュー数 × 1ページの重さ × 新規取得の割合」で決まりますが、1日のページビュー数については信頼できる公開統計を確認できていないため、幅を持たせて示します。
| 1日のページビュー数(仮定) | 新規取得の割合(仮定) | r |
|---|---|---|
| 20ページ | 50% | 約26MB |
| 50ページ | 50% | 約65MB |
| 100ページ | 50% | 約130MB |
| 200ページ | 60% | 約310MB |
新規取得の割合は、同じサイト内の回遊ではキャッシュが効き、別サイトへの移動では効かないことを踏まえた仮定です。
この推定では、多くのユーザーは数十MBから100MB強の範囲に入り、先の表の「100MB」の行が典型的なユーザーに相当します。
「300MB」以上の行は、1日200ページ以上をブラウザで見るユーザーや、動画や画像の多いサイトを見るユーザーに相当します。
2つの表の関係をグラフにすると、次のようになります。
図1は、ページビュー数から r を推定した表をそのまま棒グラフにしたものです。
図2は、その r を横軸(対数目盛)にとり、キャッシュ容量 C ごとに消失までの日数 C ÷ r を曲線で描いたものです。
図1で推定した典型的なユーザーの範囲(r が数十MBから100MB強)を、図2では帯で示しています。
C = 320MiB の場合、この帯の中では消失までの日数が2〜7日程度になります。
容量が1GiBあっても、閲覧の多いユーザーでは3〜4日です。
「1年」という max-age の値が、この日数に対してどれだけかけ離れているかが分かります。
Facebookの実測(モバイルでは、ヒットしたリクエストの半数が、キャッシュの古さ12時間以内の状態で行われていた)は、キャッシュ残存時間の分布ではなく、ヒット時のキャッシュの古さの分布であり、キャッシュの消失と訪問頻度の両方を反映した値なので、この表に直接当てはめることはできません。
仮に12時間をすべて消失によるものとみなすと、C=320MiBで r は600MB/日以上に相当しますが、これは上限側の見積もりです。
「大きいもの、使われないものから消す」設計はKHTML以来のもの
「大きいもの、使われないものから消す」という設計は、最近のものではありません。
WebKitの元になったKHTML(KDEのHTMLエンジン)のメモリキャッシュ(src/misc/loader.cpp)は、既定容量が約2MBで、エントリを20本のLRUリストに振り分けていました。
リストの番号は「サイズの対数 ÷ アクセス回数」で決まり、容量を超えると番号の大きいリスト、つまり大きくてアクセス回数の少ないエントリから追い出されます。
ソースコードのコメントには、この方式の出典として、九州産業大学のCheng氏らがCOMPSAC 2000で発表したLRU-SP(Size-adjusted and Popularity-aware LRU)の論文が示されています。
有効期限(expireDate)は鮮度の判定にだけ使われ、追い出しの判断には使われていません。
この構造は、現在のWebKitのMemoryCache(Source/WebCore/loader/cache/MemoryCache.cpp)にそのまま引き継がれています。
auto MemoryCache::lruListFor(CachedResource& resource) -> LRUList&
{
unsigned accessCount = std::max(resource.accessCount(), 1U);
unsigned queueIndex = WTF::fastLog2(resource.size() / accessCount);
既定容量は8MiBに増え、式も変わっています。
KHTMLは log2(サイズ) ÷ アクセス回数、WebKitは log2(サイズ ÷ アクセス回数)で、同じではありません。
しかし、サイズが大きいほど、アクセス回数が少ないほど番号の大きいリストに入り、先に追い出される、という考え方は同じです。
(これはメモリキャッシュの実装であり、ブラウザキャッシュは何日残るのかで扱ったSafariのディスクキャッシュとは別の階層です。)
Chromiumのディスクキャッシュは「経過時間 × サイズ」、Firefoxは「利用頻度」、KHTML以来のWebKitのメモリキャッシュは「サイズとアクセス回数の組み合わせ」と、基準の計算式は違います。
しかし、大きいものと使われないものから消し、有効期限は見ない、という点では、3つの系統すべてが一致しています。
ファイルの統合と削減がキャッシュの生存に効くという主張は、この一致に基づいています。
同じ考え方はJVMのガベージコレクションにも使われている
この考え方は、ITシステムの性能分析でも、キャッシュに限らず広く使えます。
分かりやすい例が、JVM(Java仮想マシン)のガベージコレクションです。
JVMの世代別GCは、「割り当てられたオブジェクトの大半は、割り当て直後に到達不能になる」という経験則(弱い世代仮説)を前提に、新しいオブジェクトをYoung領域に置き、GCを生き残ったものだけをSurvivor領域を経てOld領域に昇格させます。
昇格の閾値(MaxTenuringThreshold)を調整するときに見るTenuring Distribution(GCを何回生き残ったオブジェクトが、どれだけ残っているかの分布)は、オブジェクトの寿命についての生存関数そのものです。
生存関数が急に落ちるところに閾値を置けば、Old領域に昇格させる無駄が減ります。
ブラウザのキャッシュ、CDNのエッジキャッシュ、JVMのヒープは、いずれも「有限の領域に、寿命の分からないものを置き、いっぱいになったら追い出す」という同じ構造を持っており、生存関数で分析できます。
違うのは、先の表に示した「生き残る条件」だけです。
余談ですが、JVMの領域の名前は、この「寿命」の比喩でできています。
新しいオブジェクトが最初に置かれる領域はEden(エデン)と呼ばれます。
旧約聖書の創世記で、生命が最初に置かれた場所がエデンの園です。
GCを生き延びたオブジェクトが移る領域はSurvivor(生存者)、何度も生き延びて昇格することはTenuring(終身在職権を得ること)、昇格先はTenured(終身在職権を持つ)世代です。
Tenuringという言葉は、世代別GCの原型となったDavid Ungar氏の1984年の論文「Generation Scavenging」(Berkeley Smalltalk向けの実装)で使われています。
この論文では、新しいオブジェクトの領域はまだNewSpaceと呼ばれ、生存者の領域はPastSurvivorSpaceとFutureSurvivorSpaceでした。
この論文にはEdenという名前は出てこず、いつ誰によって付けられたかは確認できていません。
なお、この論文の冒頭には、Steely Danの曲「Throw Back the Little Ones」の歌詞(小さいのは投げ返し、大きいのは鍋で焼け)がエピグラフとして引かれています。
「生き延びた大きいものだけを手間をかけて扱う」という、世代別GCの考え方をそのまま表しています。
まとめ
- キャッシュの領域は有限なので追い出しが起き、追い出しがあるから寿命があり、寿命はいつ来るか分からないので確率で扱います。
- 生存関数 S(t) = P(T > t) は「t の時点でまだ残っている確率」で、S(0) = 1 から単調に下がり、0.5 を横切る t が中央値、曲線の下側の面積が平均寿命です。
- 生存関数の形を決めるのは追い出し方式です。
Chromiumは「経過時間×サイズ」、Firefoxは「利用頻度」、WebKitは「サイズとアクセス回数の組み合わせ」で、いずれもLRUの変種です。 - 1人の端末では、寿命は他のファイルの流入が容量を満たすまでの時間(C ÷ r)でほぼ決まります。
集団の S(t) が曲線になるのは、閲覧量がユーザーごとに違うからです。
指数分布はその形を扱う作業用の近似で、実際の形は実測で確かめます。 - Cheの近似によれば、LRUキャッシュには特性時間 TC があり、リクエスト頻度が 1/TC より低いオブジェクトはキャッシュを素通りします。
訪問頻度の低いユーザーに、訪問をまたいだブラウザキャッシュの再利用が効かないのは、この理論の帰結です - 「大きいもの、使われないものから消す」という設計はKHTML以来のもので、JVMの世代別GCも同じ生存関数の考え方で設計されています
この記事が事業に意味すること
キャッシュの設計は、多くの現場で「1年にしておけばよい」「CDNを入れれば速くなる」という経験則で行われています。
この記事で示したとおり、キャッシュの効き方は、ユーザーの訪問頻度、端末の容量、追い出しの方式によって決まる確率的なものです。
経験則が当てはまるサイトもあれば、まったく当てはまらないサイト(訪問頻度の低い企業サイト、SNS流入の多いサイト)もあります。
弊社は、キャッシュ設計を経験則ではなく、この記事の理論と、ブラウザキャッシュのヒット率を生存時間解析で実測するで説明する実測の両方に基づいて行っています。
自社のサイトでキャッシュがどれだけ効いているのかを、推測ではなく数字で知りたい場合は、お問い合わせフォームからご相談ください。
どのような改善で、どれだけ速くなったかは、高速化成功事例をご覧ください。
参考資料
- RFC 9111: HTTP Caching
- Chromium: net/disk_cache/cache_util.cc
- Chromium: net/disk_cache/simple/simple_index.cc
- Firefox: netwerk/cache2/CacheIndex.cpp(GitHubミラー)
- KHTML: src/misc/loader.cpp(GitHubミラー、kf5ブランチ)
- WebKit: Source/WebCore/loader/cache/MemoryCache.cpp
- Page Weight(HTTP Archive Web Almanac 2025)
- Web performance: Cache efficiency exercise(Facebook Engineering、2015年)
- L. Breslau, P. Cao, L. Fan, G. Phillips, S. Shenker, "Web Caching and Zipf-like Distributions: Evidence and Implications," Proc. IEEE INFOCOM, 1999
- H. Che, Y. Tung, Z. Wang, "Hierarchical Web Caching Systems: Modeling, Design and Experimental Results," IEEE Journal on Selected Areas in Communications, vol. 20, no. 7, pp. 1305–1314, 2002
- D. Ungar, "Generation Scavenging: A Non-disruptive High Performance Storage Reclamation Algorithm," Proc. ACM SIGSOFT/SIGPLAN Software Engineering Symposium on Practical Software Development Environments, 1984, pp. 157–167
- ブラウザキャッシュは何日残るのか(PerfData)
- max-ageは何秒にすべきか(PerfData)
- ブラウザキャッシュのヒット率を生存時間解析で実測する(PerfData)