Netflixのホームページに表示されるパーソナライズされた行を生成するRankerサービスは、大規模で複雑なサービスです。CPUプロファイルを分析した結果、「動画セレンディピティスコアリング」機能が各ノードのCPUの約7.5%を消費するホットスポットであることが判明しました。単なる「バッチ処理」という考えから始まったこの最適化は、根本的な解決策を求める旅へと発展しました。本記事では、その過程で得られた実践的な知見を共有します。詳細な根拠資料はNetflix Tech Blogでご確認いただけます。

問題: O(M×N)の非効率な内積演算
セレンディピティスコアは、「この新しいコンテンツがユーザーの視聴履歴とどのくらい異なるか?」を計算するものです。M個の候補動画とN個の履歴アイテムの埋め込み表現を比較するため、M×N回のコサイン類似度計算が必要でした。
初期実装は直感的ですが非効率でした。各候補ごとに埋め込みを取得し、履歴をループで回してペアごとに内積を計算する方式は、メモリアクセスの繰り返しとキャッシュ局所性の低下を招いていました。
// 単純なネストされたループ方式 (疑似コード)
for (Video candidate : candidates) { // M回
Vector c = embedding(candidate);
double maxSim = -1.0;
for (Video h : history) { // N回
Vector v = embedding(h);
double sim = cosine(c, v); // 内積演算が発生
maxSim = Math.max(maxSim, sim);
}
double serendipity = 1.0 - maxSim;
emitFeature(candidate, serendipity);
}
// 合計 M x N 回の個別の内積演算

5段階の最適化: 基本原則の重要性
- バッチ処理と行列演算化: M×N個の小さな内積演算を、単一の行列乗算(C = A * B^T)に変換しました。これはCPUが最適化しやすい形状です。
- バッチ処理だけでは不十分: 逆に5%の性能低下が発生しました。原因は、短命な
double[][]割り当てによるGC負荷と、非連続メモリアクセスでした。 - フラットバッファとThreadLocal再利用:
double[]フラットバッファとスレッドごとの再利用プールを導入し、割り当てオーバーヘッドを大幅に削減、キャッシュ効率を改善しました。 - BLASの落とし穴: ネイティブBLASライブラリはJNI遷移オーバーヘッドとレイアウト変換コストをもたらし、純粋なJava環境では理論上の利益を相殺しました。
- JDK Vector APIの登場: これが決定的でした。 純粋なJavaコードでSIMD(単一命令複数データ)演算を表現できるようにします。スカラー演算をベクトル化されたFMA(融合積和)演算に置き換え、CPUのベクトルユニットを最大限に活用できるようになりました。
// JDK Vector APIを活用した内部ループ (簡略化)
DoubleVector acc = DoubleVector.zero(SPECIES_PREFERRED);
for (; k + SPECIES.length() <= D; k += SPECIES.length()) {
DoubleVector a = DoubleVector.fromArray(SPECIES, candidatesFlat, i*D + k);
DoubleVector b = DoubleVector.fromArray(SPECIES, historyFlat, j*D + k);
acc = a.fma(b, acc); // ベクトル化されたFMA演算!
}
double dot = acc.reduceLanes(VectorOperators.ADD);
| 最適化段階 | 核心的な変更 | 主な利点 | 注意点 |
|---|---|---|---|
| 1. バッチ処理 | ネストループ → 行列乗算 | アルゴリズム的効率性 | データレイアウト設計が必要 |
| 2/3. メモリ最適化 | double[][] → double[] + ThreadLocal | キャッシュ局所性向上、GC削減 | バッファ管理ロジックの追加 |
| 5. JDK Vector API | スカラー演算 → SIMDベクトル演算 | CPUハードウェア効率の最大化 | インキュベーティングモジュールへの依存 |

結論: 最も速いライブラリが答えではない
この作業を通じて、「最も速いライブラリ」を探すことよりも、計算形状、データレイアウト、オーバーヘッドの排除という基本原則に忠実であることの重要性を学びました。これらの基本が整った後、JDK Vector APIは、JNIオーバーヘッドなしで純粋なJavaでSIMD性能を引き出せる完璧なツールとなりました。
結果として、CPU使用率は約7%低下、平均レイテンシは約12%低下し、ホットスポットだった機能のCPU占有率は7.5%から約1%に大幅に低下しました。これは単なるコード最適化を超え、問題を根本的に再構築した成果です。実務で性能改善を考える際は、新しいライブラリに飛びつく前に、まずデータの流れと形状を点検することをお勧めします。