FIFO Cache(First In First Out Cache)は、アクセス頻度に関係なく、最も早く追加された要素を追い出すキャッシュアルゴリズムです。キューとして実装され、新しい要素は末尾に追加され、オーバーフローが発生すると先頭の要素が削除されます。Android Developers (2026)によると、FIFO Cacheはすべての操作でO(1)を提供しますが、不均等なデータアクセスパターンではhit-ratioでLRUに劣ります。
重要ポイント
FIFO Cache(First In First Out Cache)は、要素を管理するためにキューを使用する固定サイズのキャッシュです。最初に追加された要素はキューの先頭に配置され、オーバーフローが発生したときに最初に削除されます。新しい要素は常に末尾に追加され、削除順序が追加順序と一致することが保証されます。
アクセスのたびに要素を並べ替えるLRUとは異なり、FIFOはgetリクエストで既存の要素の位置を変更しません。これにより、アルゴリズムは完全に決定論的になります。追加順序がわかれば、次にどの要素が追い出されるかを正確に予測できます。この予測可能性は、データを到着順に処理する必要があるリアルタイムシステムにとって重要です。
FIFO Cacheの実装は、いくつかのデータ構造で構築できます。最大パフォーマンスのための循環バッファ、柔軟性のためのリンクリスト、または組み込みキューを持たない言語のための2つのスタック(Two-Stack Queue)です。循環バッファは最良のキャッシュローカリティと最小限のオーバーヘッドを提供しますが、maxSizeのためのメモリの事前割り当てが必要です。
enqueue(value)操作は、キューの末尾に要素を追加します。サイズがmaxSizeに達した場合、追加の前に先頭の要素が削除されます。dequeue()操作は先頭の要素を削除して返します — 最も古い要素の強制的な取り出しのためです。peek()操作は削除せずに先頭の要素を返します — キューを変更せずに最も古い要素を表示するためです。
FIFOアルゴリズムは通常のキューの動作を模倣します:最初に入ったものが最初にサービスされます。キャッシングの文脈では、これはキャッシュに最も長く存在する要素が、その人気度に関係なく、スペースが必要になったときに削除されることを意味します。FIFOの追い出しポリシーはアクセス頻度を無視します。これはアルゴリズムの強みであり弱みでもあります。
循環バッファで実装する場合、head(キューの先頭のインデックス)とtail(末尾のインデックス)の2つのポインタが使用されます。enqueueでは、要素がtailインデックスに書き込まれ、tailがインクリメントされます。tailがバッファサイズに達すると、配列の先頭に折り返されます。tailがheadに追いつくと、キューは満杯で、headがシフトされます(追い出し)。循環バッファは動的メモリ割り当てを必要とせず、断片化を回避します。
FIFO Cacheは、一般的なワークロードに対して40%から60%のhit-ratioを示し、LIFOよりは高いですがLRUよりは低い値です。ただし、データアクセスが均一でホットスポットがないシナリオでは、FIFOは大幅に低い実装複雑性でLRUに匹敵する結果を示すことができます。メモリは効率的に使用されます。要素の並べ替えに追加のポインタは必要ありません。
FIFOの主な欠点は、キャッシュ汚染に対する感受性です。二度と必要とされない大量のデータがキャッシュに追加されると、徐々にすべての有用な要素を追い出し、hit-ratioが急激に低下します。LRUはこの問題を部分的に解決します。頻繁に使用される要素は先頭に移動することで常にリフレッシュされ、1回限りのデータはより速く追い出されます。FIFOでは、1回限りのデータはキューの順序によって自然に追い出されるまでキャッシュに残ります。
FIFO、LRU、LIFOの選択は、データアクセスパターンと動作の予測可能性の要件に依存します。LRUはほとんどのシナリオに最適で、FIFOは均一なアクセスを持つストリーミングデータに、LIFOはスタック構造に適しています。
| パラメータ | FIFO | LRU | LIFO |
|---|---|---|---|
| 追い出し基準 | 最初に追加されたもの | 最も最近使用されていないもの | 最後に追加されたもの |
| 構造 | キュー | HashMap + 双方向リンクリスト | スタック |
| 予測可能性 | 高い | 中程度 | 高い |
| 汚染対策 | 低い | 中程度 | 低い |
| ストリーミングデータ | 優れている | 満足できる | 悪い |
| リソース(CPU/RAM) | 最小 | 中程度 | 最小 |
FIFOは、処理順序が到着順序と一致する必要があるシナリオに最適です:データバッファリング、ロギング、イベント処理。LRUは不均等なアクセス(ユーザーデータ)のキャッシングに適しています。LIFOはスタックと元に戻す操作にのみ適用可能です。ほとんどのモバイルアプリケーションでは、LRUがデフォルトの選択肢ですが、厳格なメモリ制約や予測可能性の要件がある場合はFIFOが好ましい場合があります。
FIFO Cacheは、追い出しの予測可能性やデータ処理の順序が重要なシナリオで使用されます。主な使用例を見てみましょう。
オーディオとビデオの再生時、データは連続ストリームで到着し、一時的にバッファに保存されます。FIFO Cacheは、最初に受信したフラグメントが最初にデコードに送られることを保証します — これにより遅延のないスムーズな再生が保証されます。バッファサイズは、ストリームのビットレートと許容可能な遅延に基づいて選択されます:オーディオでは通常2〜5秒、ビデオでは10〜30秒です。データの並べ替え(LRUのような)が意味をなさないため、FIFOはこのようなシナリオに最適です。
同時ネットワークリクエストの数を制限する場合、FIFO Cacheを使用して保留中のリクエストを保存できます。最初に追加されたリクエストが最初に実行され、アプリケーションの異なるコンポーネント間でネットワークリソースの公平な配分が保証されます。このアプローチは、OkHttp Dispatcherや同様のライブラリでコネクションプール管理に使用されています。
モバイルデバイス上のシンプルなHTTPレスポンスキャッシュは、多くの場合FIFOを使用します。リクエストへの応答は到着順に保存され、制限に達すると最も古いものが削除されます。LRUはユーザーシナリオでより良いhit-ratioを提供しますが、FIFOは実装がより簡単で、各応答の最終アクセス時間を保存する必要がありません。均一な負荷のAPIの場合、FIFOとLRUのhit-ratioの差は最小限です。
モバイルアプリケーションでは、タッチイベントはジェスチャー処理の前にFIFOキューにバッファリングされます。各イベントは発生した順序で処理される必要があります。そうしないと、ジェスチャーが正しく認識されません。サイズ制限付きのFIFO Cacheは、高速スワイプ中のバッファオーバーフローを防ぎ、アプリケーションが処理できない場合は最も古いイベントを破棄します。
モバイルデバイスに最も効率的なアプローチである循環バッファを使用したKotlinでのFIFO Cacheの実装を見てみましょう。
class FifoCache<V>(
private val maxSize: Int
) {
private val buffer = arrayOfNulls<V>(maxSize)
private var head = 0
private var tail = 0
private var size = 0
fun enqueue(value: V) {
if (size == maxSize) {
// 最も古い要素を追い出す
buffer[head] = null
head = (head + 1) % maxSize
size--
}
buffer[tail] = value
tail = (tail + 1) % maxSize
size++
}
fun dequeue(): V? {
if (size == 0) return null
val result = buffer[head]
buffer[head] = null
head = (head + 1) % maxSize
size--
return result
}
fun peek(): V? {
return buffer[head]
}
}
循環バッファは、maxSizeを法として循環的にインクリメントされるheadとtailのインデックスを使用します。size == maxSizeのとき、enqueueは最初にheadの要素(最も古いもの)を削除し、headをシフトしてから、tailに新しい要素を書き込みます。モジュラ演算は自動的にポインタを配列の先頭に折り返し、手動のデータコピーを排除します。
Swiftでは、2つのスタックに基づくFIFOキュー(Two-Stack Queue)が便利な代替手段です。すべてのenqueue操作は最初のスタック(push)に行き、dequeue時に要素は逆順で2番目のスタックに転送されます — これによりdequeueは平均的にO(1)になります。
struct FifoCache<Value> {
private let maxSize: Int
private var inStack = [Value]()
private var outStack = [Value]()
mutating func enqueue(value: Value) {
if inStack.count + outStack.count >= maxSize {
if outStack.isEmpty {
outStack = inStack.reversed()
inStack.removeAll()
}
outStack.removeLast()
}
inStack.append(value)
}
mutating func dequeue() -> Value? {
if outStack.isEmpty {
outStack = inStack.reversed()
inStack.removeAll()
}
return outStack.popLast()
}
}
2つのスタックは、enqueueとdequeueに対してならしO(1)の複雑性を提供します。追い出し時のoutStack.removeLast()は最も古い要素(最初に追加されたもの)を削除します。このアプローチはメモリの事前割り当てを必要としませんが、頻繁なスタックの反転時にガベージコレクタに追加の負荷をかける可能性があります。メモリが限られたモバイルアプリケーションでは、循環バッファの方がより好ましいままです。
よくある質問
キューはサイズ制限のない抽象データ構造です。FIFO Cacheは固定最大サイズと追い出しポリシーを持つキューです:オーバーフロー時に先頭の要素が自動的に削除されます。通常のキューはオーバーフロー時に追加をブロックするか動的に拡張しますが、FIFO Cacheは古いデータを追い出すことで常に新しいデータを受け入れます。
FIFOは、ホットスポットのない均一なデータアクセスのシナリオでLRUより優れています。例えば、ログファイルやストリーミングデータをキャッシュする場合、各値は1回だけ使用され、LRUは利点を提供しません。FIFOは厳格なメモリ制約の下でも好ましいです — 並べ替えに追加のポインタを必要とせず、要素あたり16+バイトを節約します。
Androidでは、循環バッファを実装するKotlin標準ライブラリのArrayDequeを使用できます。FIFO Cacheの場合、ArrayDequeをラップします:enqueue時にサイズを確認し、超過した場合はremoveFirst()を呼び出します。スレッドセーフ版には、ConcurrentLinkedDequeまたはSynchronizedArrayDequeを使用します。
大量の1回限りのデータがキャッシュに追加されると、すべての有用な要素を追い出します。例えば、maxSize=30のギャラリーに50の画像を読み込むと、最初の20の有用な画像が追い出されます。ユーザーはおそらくそれらに戻るでしょうが。LRUはこの問題を部分的に解決します:頻繁に使用される要素はリフレッシュされ、キャッシュに残ります。
はい、ハイブリッドアルゴリズムが存在します。2Q(Two-Queue)はキャッシュを2つの部分に分割します:ホット(LRU)とコールド(FIFO)。新しい要素は最初にFIFOキューに入り、繰り返しアクセスのみがそれらをLRU部分に移動させます。これにより、頻繁に使用される要素の高いhit-ratioを維持しながら、LRUを1回限りのデータによる汚染から保護します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。