<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>SIMD on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/simd/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Tue, 03 Mar 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/simd/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>Netflix 用 JDK Vector API 优化推荐系统：一次教科书级的性能工程实录</title>
      <link>https://ftmao.com/blog/posts/netflix-jdk-vector-api-recommendation/</link>
      <pubDate>Tue, 03 Mar 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-jdk-vector-api-recommendation/</guid>
      <description>&lt;h1 id=&#34;netflix-用-jdk-vector-api-优化推荐系统一次教科书级的性能工程实录&#34;&gt;Netflix 用 JDK Vector API 优化推荐系统：一次教科书级的性能工程实录&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/optimizing-recommendation-systems-with-jdks-vector-api-30d2830401ec&#34;&gt;Optimizing Recommendation Systems with JDK&amp;rsquo;s Vector API&lt;/a&gt;
目标读者：写 Java 后端、关心 JVM 性能工程的工程师；也适合任何在“要不要引入原生库/SIMD”上纠结过的人。
阅读目标：看清一次完整性能优化的正确顺序——先改计算形状，再改数据布局，最后才轮到换计算内核。以及理解为什么 BLAS 这种“标准答案”在真实生产路径里会失效。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一背景ranker-里的-75-热点&#34;&gt;一、背景：Ranker 里的 7.5% 热点&lt;/h2&gt;
&lt;p&gt;Ranker 是 Netflix 最大、最复杂的服务之一，首页那些个性化推荐行就是它算出来的。看 CPU profile 时有一个特征始终扎眼：&lt;strong&gt;视频惊喜度打分（video serendipity scoring）&lt;/strong&gt;——回答一个简单问题：“这部新片和你最近看的东西有多不一样？”这个特征吃掉每个节点约 &lt;strong&gt;7.5% 的总 CPU&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;算法本身不复杂：候选视频和用户历史里的每个视频都表示成 embedding 向量；对每个候选，算出它与所有历史向量的余弦相似度，取最大值，用 1 减去它得到“新颖度”分数，作为下游推荐逻辑的输入特征。原始实现非常直白：对每个候选取 embedding，遍历历史逐对算余弦，维护最大值。容易理解，但在 Ranker 的规模下意味着大量串行工作、重复查找、分散的内存访问和糟糕的缓存局部性。火焰图确认了这一点：服务里最热的热点之一就是惊喜度编码器里的 Java 点积。算法层面，这是一个 &lt;strong&gt;M 个候选 × N 个历史&lt;/strong&gt;的嵌套循环，产生 O(M×N) 次独立点积。&lt;/p&gt;
&lt;p&gt;一个容易被忽略但决定了整个优化方向的数据：他们埋点统计了流量形状，发现&lt;strong&gt;约 98% 的请求是单视频请求，剩下 2% 是大批量请求&lt;/strong&gt;——但那 2% 的批次实在太大，最终处理的视频总量在单视频和批处理之间大约是 &lt;strong&gt;50:50&lt;/strong&gt;。所以批处理值得做，即使它对中位数请求毫无帮助。&lt;/p&gt;
&lt;h2 id=&#34;二第一步把嵌套循环看成矩阵乘&#34;&gt;二、第一步：把嵌套循环看成矩阵乘&lt;/h2&gt;
&lt;p&gt;第一个想法是换“计算形状”：不再想成“很多次小点积”，而是当成一次矩阵运算。设 embedding 维度为 D：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把所有候选 embedding 装进矩阵 A（形状 M×D）；&lt;/li&gt;
&lt;li&gt;把所有历史 embedding 装进矩阵 B（形状 N×D）；&lt;/li&gt;
&lt;li&gt;所有行归一化成单位向量；&lt;/li&gt;
&lt;li&gt;计算 &lt;code&gt;C = A × Bᵀ&lt;/code&gt;，C 就是 M×N 的余弦相似度矩阵。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样 &lt;strong&gt;M×N 次独立点积变成一次矩阵乘&lt;/strong&gt;——而这正是 CPU 和优化过的计算内核最擅长的东西。工程上通过同时支持 &lt;code&gt;encode()&lt;/code&gt;（单视频）和 &lt;code&gt;batchEncode()&lt;/code&gt;（批量）接入现有框架，保持向后兼容。到这里看起来像“做完了”。但并没有。&lt;/p&gt;
&lt;h2 id=&#34;三第二步批处理反而慢了-5&#34;&gt;三、第二步：批处理反而慢了 5%&lt;/h2&gt;
&lt;p&gt;上了批处理版本跑金丝雀，结果出人意料：&lt;strong&gt;性能回退约 5%&lt;/strong&gt;。算法没问题——把点积归并成矩阵乘在数学上是等价的。问题出在第一版实现自己引入的开销：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;每个批次都新建 &lt;code&gt;double[][]&lt;/code&gt; 矩阵装候选、历史和结果。这些又大又短命的对象制造了 GC 压力；而且 &lt;code&gt;double[][]&lt;/code&gt; 在内存里本身就不连续，多一层指针跳转，缓存行为更差。&lt;/li&gt;
&lt;li&gt;第一版 Java 矩阵乘是朴素的标量实现，用不上 SIMD。换句话说，&lt;strong&gt;付了批处理的成本，却没拿到批处理该有的计算效率&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;教训来得很快：算法层面的改进，如果实现细节——内存布局、分配策略、计算内核——跟你作对，就一文不值。这一步定下了后面的路线：先把数据布局改成缓存友好的，消灭每批次分配，然后再回头解决矩阵乘内核。&lt;/p&gt;
&lt;h2 id=&#34;四第三步扁平-buffer-加-threadlocal-复用&#34;&gt;四、第三步：扁平 buffer 加 ThreadLocal 复用&lt;/h2&gt;
&lt;p&gt;重做数据布局。&lt;code&gt;double[m][n]&lt;/code&gt; 换成&lt;strong&gt;行主序的扁平 &lt;code&gt;double[]&lt;/code&gt;&lt;/strong&gt;：内存连续，访问模式可预测。然后引入 &lt;code&gt;ThreadLocal&amp;lt;BufferHolder&amp;gt;&lt;/code&gt;，持有候选、历史等可复用 buffer——&lt;strong&gt;buffer 只增长不收缩&lt;/strong&gt;，避免每请求分配，同时每个线程持有自己的一份，无锁无争用。核心逻辑就几行：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;class BufferHolder {
  double[] candidatesFlat = new double[0];
  double[] getCandidatesFlat(int required) {
    if (candidatesFlat.length &amp;lt; required) {
      candidatesFlat = new double[required];
    }
    return candidatesFlat;
  }
  // historyFlat 同理
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;光是这一步，批处理路径就变得可预测多了：分配少了，GC 压力小了，缓存局部性好了。剩下的问题回到最初的那个：矩阵乘到底用什么算？&lt;/p&gt;
&lt;h2 id=&#34;五第四步blas测试里很好生产里不行&#34;&gt;五、第四步：BLAS——测试里很好，生产里不行&lt;/h2&gt;
&lt;p&gt;显而易见的下一步是 BLAS（Basic Linear Algebra Subprograms）。隔离的微基准看起来很有希望，但一旦接进真实的批量打分路径，收益就没兑现。几个因素在作对：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;默认的 &lt;code&gt;netlib-java&lt;/code&gt; 路径走的是 F2J（Fortran 转 Java）的 BLAS，不是真正的原生实现；&lt;/li&gt;
&lt;li&gt;就算用原生 BLAS，也要付 setup 和 JNI 边界切换的开销；&lt;/li&gt;
&lt;li&gt;Java 的行主序和很多 BLAS 例程期望的列主序对不上，会引入转换和临时 buffer；&lt;/li&gt;
&lt;li&gt;这些额外的分配和拷贝在完整流水线里很扎眼，尤其是旁边还有 TensorFlow 的 embedding 计算。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;BLAS 不是白试——它帮忙看清了时间到底花在哪。但它不是那种即插即用的胜利。他们需要的是：&lt;strong&gt;纯 Java、贴合扁平 buffer 架构、还能用上 SIMD&lt;/strong&gt; 的东西。&lt;/p&gt;
&lt;h2 id=&#34;六第五步jdk-vector-api&#34;&gt;六、第五步：JDK Vector API&lt;/h2&gt;
&lt;h3 id=&#34;先补一课jdk-vector-api-到底是什么&#34;&gt;先补一课：JDK Vector API 到底是什么&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;起点是 SIMD。&lt;/strong&gt; 现代 x86 CPU 都有向量寄存器，一条指令可以同时算多个数：AVX2 的 256 位寄存器一次装 4 个 double，AVX-512 的 512 位一次装 8 个。一条向量乘加指令顶 4-8 条标量指令——这就是 SIMD（Single Instruction, Multiple Data，单指令多数据），点积、矩阵乘这类规则的数值计算是它的主场。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Java 的痛点是：这些指令一直在硬件里，Java 程序员却够不着。&lt;/strong&gt; JIT 有自动向量化，但只对形状简单、能被它证明安全的循环生效，代码稍微复杂一点就悄悄退回标量，而且没有任何方式强制它发生；想要确定性的 SIMD，只能走 JNI 调原生库——代价就是上一步 BLAS 踩过的那些坑。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Vector API 是 Project Panama 的一部分&lt;/strong&gt;，从 JDK 16 起以孵化模块 &lt;code&gt;jdk.incubator.vector&lt;/code&gt; 的形式提供，到原文写作时仍在孵化（所以运行时要显式 &lt;code&gt;--add-modules&lt;/code&gt;）。它把 SIMD 变成了普通 Java 代码：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;VectorSpecies&lt;/code&gt; 描述「一次算几个 lane」（lane 就是向量寄存器里的一个槽位），&lt;code&gt;SPECIES_PREFERRED&lt;/code&gt; 让运行时按当前 CPU 挑最优宽度；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DoubleVector.fromArray(...)&lt;/code&gt; 从数组加载一组 lane，&lt;code&gt;fma(b, acc)&lt;/code&gt; 做向量融合乘加，&lt;code&gt;reduceLanes(ADD)&lt;/code&gt; 把各 lane 归约成一个标量；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;同一份代码的可移植性由 JIT 兜底&lt;/strong&gt;：编译时映射到宿主机器最好的指令集，AVX-512 机器跑 8 lane，AVX2 机器跑 4 lane，都不支持就退回标量。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph S[&amp;#34;标量路径 一条指令算一个数&amp;#34;]
        A1[&amp;#34;a0 乘 b0&amp;#34;] --&amp;gt; R1[&amp;#34;acc 逐个累加&amp;#34;]
        A2[&amp;#34;a1 乘 b1&amp;#34;] --&amp;gt; R1
        A3[&amp;#34;a2 乘 b2&amp;#34;] --&amp;gt; R1
        A4[&amp;#34;a3 乘 b3&amp;#34;] --&amp;gt; R1
    end
    subgraph V[&amp;#34;SIMD 路径 AVX2 一条指令算四个数&amp;#34;]
        B1[&amp;#34;向量寄存器加载&amp;lt;br/&amp;gt;a0 a1 a2 a3&amp;#34;] --&amp;gt; F[&amp;#34;一条 fma 指令&amp;#34;]
        B2[&amp;#34;向量寄存器加载&amp;lt;br/&amp;gt;b0 b1 b2 b3&amp;#34;] --&amp;gt; F
        F --&amp;gt; R2[&amp;#34;acc 四个 lane 同时累加&amp;#34;]
    end
    style F fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;一句话总结：&lt;strong&gt;它把「手写 intrinsics 的活」变成了「写普通循环的活」&lt;/strong&gt;——你不用知道目标机器是 SSE 还是 AVX-512，JIT 替你选。对他们来说更关键的是：&lt;strong&gt;纯 Java，没有原生依赖，没有 JNI 切换，写起来就是普通 Java 代码&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;跟他们的负载是绝配：embedding 已经搬进了扁平连续的 &lt;code&gt;double[]&lt;/code&gt; buffer，热循环被海量点积占满。最后一步就是用基于 Vector API 的纯 Java SIMD 实现换掉 BLAS——藏在一个小工厂后面，类加载时由 &lt;code&gt;MatMulFactory&lt;/code&gt; 选最优实现：&lt;code&gt;jdk.incubator.vector&lt;/code&gt; 可用就用 Vector API，否则退回一个高度优化的标量实现——循环展开的点积（同事 Patrick Strawderman 实现，灵感来自 Lucene 里的模式）。&lt;/p&gt;
&lt;p&gt;Vector API 版本的内层循环：用 &lt;code&gt;fma()&lt;/code&gt;（fused multiply-add）把 &lt;code&gt;a * b&lt;/code&gt; 累加进向量累加器；&lt;code&gt;DoubleVector.SPECIES_PREFERRED&lt;/code&gt; 让运行时自己挑适合机器的 lane 宽度（AVX2 上通常 4 个 double，AVX-512 上 8 个）。简化版：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;for (int i = 0; i &amp;lt; M; i++) {
  for (int j = 0; j &amp;lt; N; j++) {
    DoubleVector acc = DoubleVector.zero(SPECIES);
    int k = 0;
    for (; k + SPECIES.length() &amp;lt;= 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);  // 融合乘加
    }
    double dot = acc.reduceLanes(VectorOperators.ADD);
    // 处理 k..D-1 的尾部
    similaritiesFlat[i*N + j] = dot;
  }
}&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;原来的一长串标量乘加，变成少量向量 &lt;code&gt;fma()&lt;/code&gt; 加一次归约——算法没变，CPU 向量单元的利用率完全不同。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph OLD[&amp;#34;原始路径 标量&amp;#34;]
        O1[&amp;#34;候选 embedding&amp;#34;] --&amp;gt; O3[&amp;#34;逐个算余弦相似度&amp;#34;]
        O2[&amp;#34;历史 embedding 逐个取&amp;#34;] --&amp;gt; O3
        O3 --&amp;gt; O4[&amp;#34;M 乘 N 次独立点积&amp;lt;br/&amp;gt;分散内存访问 缓存不友好&amp;#34;]
    end
    subgraph NEW[&amp;#34;优化后路径 SIMD&amp;#34;]
        N1[&amp;#34;候选装入扁平 buffer A&amp;lt;br/&amp;gt;M×D 行主序&amp;#34;] --&amp;gt; N3[&amp;#34;归一化为单位向量&amp;#34;]
        N2[&amp;#34;历史装入扁平 buffer B&amp;lt;br/&amp;gt;N×D 行主序&amp;#34;] --&amp;gt; N3
        N3 --&amp;gt; N4[&amp;#34;一次矩阵乘 C = A × Bᵀ&amp;lt;br/&amp;gt;Vector API fma 累加&amp;#34;]
        N4 --&amp;gt; N5[&amp;#34;每行取最大值&amp;lt;br/&amp;gt;novelty = 1 - maxSim&amp;#34;]
    end
    O4 -.-&amp;gt;|&amp;#34;重构计算形状&amp;#34;| N1
    style O4 fill:#ffcccc
    style N4 fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h2 id=&#34;七回退与安全孵化特性的生产姿势&#34;&gt;七、回退与安全：孵化特性的生产姿势&lt;/h2&gt;
&lt;p&gt;Vector API 还在孵化，运行时要加 &lt;code&gt;--add-modules=jdk.incubator.vector&lt;/code&gt; 才能用。他们不想让正确性和可用性绑在这个 flag 上，所以把回退行为显式设计出来：启动时探测 Vector API 支持，有就走 SIMD 批量矩阵乘，没有就走优化过的标量路径，单视频请求继续用逐条实现。运营故事很干净：服务可以选择启用 Vector API 拿满性能，不启用系统也照样安全可预测。&lt;/p&gt;
&lt;h2 id=&#34;八生产结果&#34;&gt;八、生产结果&lt;/h2&gt;
&lt;p&gt;完整方案（批处理 + 扁平 buffer + ThreadLocal 复用 + Vector API）在跑生产流量的金丝雀上测得：&lt;strong&gt;CPU 利用率下降约 7%，平均延迟下降约 12%&lt;/strong&gt;；为了归一化吞吐差异还跟踪了 CPU/RPS（每 RPS 消耗的 CPU），&lt;strong&gt;改善约 10%&lt;/strong&gt;——同样的流量可以少用约 10% 的 CPU，全量上线后数据保持住了。在函数级别，这个特征占的 CPU 从最初的 &lt;strong&gt;7.5% 降到约 1%&lt;/strong&gt;。汇编层面的变化也很直白：从循环展开的标量点积，变成 AVX-512 硬件上的向量化矩阵乘。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;工程实践联想&#34;&gt;工程实践联想&lt;/h2&gt;
&lt;p&gt;这篇文章值得读两遍：第一遍看技术，第二遍看顺序。沉淀下来的启示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;优化顺序是：计算形状 → 数据布局 → 分配策略 → 计算内核。&lt;/strong&gt; 他们最后换的才是内核（Vector API），但前面三步缺任何一步，换内核都拿不到收益。那个 5% 的回退也说明：跳过布局直接批处理，等于同时引入新开销和新瓶颈。做性能优化先问“形状对不对”，再问“数据在内存里躺得对不对”，最后才轮到“用什么算”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;微基准通过不等于生产有效。&lt;/strong&gt; BLAS 在隔离测试里很有希望，一进真实路径就被 JNI、布局转换、临时分配吃光收益。任何库的 benchmark 都只对它自己的测试负责；决策必须在真实流量路径里做，最好带着上下游（他们旁边还跑着 TensorFlow 的 embedding 计算）一起测。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;double[][]&lt;/code&gt; 到扁平 &lt;code&gt;double[]&lt;/code&gt; 是 Java 性能工程的经典动作。&lt;/strong&gt; 二维数组是“数组的数组”，行指针分散，缓存局部性差；扁平数组行主序连续、访问可预测，配合 ThreadLocal buffer 只增不缩，每请求分配降成零——这套组合在 Lucene、Netty 里到处都是，值得变成肌肉记忆。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量形状决定优化对象。&lt;/strong&gt; 98% 的请求是单视频，只看中位数的话批处理毫无价值；但那 2% 的大批量扛了一半总计算量。看 p50 优化和看总量优化是两个问题，先埋点搞清楚自己的流量长什么样。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;孵化/预览特性的正确姿势：工厂加探测加回退。&lt;/strong&gt; 不赌 &lt;code&gt;--add-modules&lt;/code&gt; 一定加得上，启动时探测、随时退回标量实现，“试用新 JDK 特性”就从一次豪赌变成一个配置项。这个模式可以复用到任何“新能力可用性不确定”的场景。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;顺带一个私货：这篇文章最好的地方是诚实——它把“我们以为做完了结果慢了 5%”和“BLAS 没成”都写了出来。性能工程的真实过程就是这样：每一步的正确性都要靠金丝雀和生产数据确认，不存在一步到位。那些只写成功结果的优化文章，参考价值反而低。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：Netflix Ranker 的视频惊喜度打分（候选与历史 embedding 的余弦相似度取 max）占单节点 CPU 约 7.5%，热点是 O(M×N) 的逐对点积；流量形状 98% 单视频、2% 大批量，但计算总量 50:50。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路径&lt;/strong&gt;：M×N 次点积重构成矩阵乘 &lt;code&gt;C = A × Bᵀ&lt;/code&gt;（行归一化后即余弦相似度矩阵）→ 金丝雀回退约 5%（&lt;code&gt;double[][]&lt;/code&gt; 分配与 GC、非连续布局、标量内核）→ 扁平 &lt;code&gt;double[]&lt;/code&gt; 行主序加 &lt;code&gt;ThreadLocal&amp;lt;BufferHolder&amp;gt;&lt;/code&gt; 复用（buffer 只增不缩）→ BLAS 因 F2J 非原生、JNI 开销、行列主序不匹配而放弃 → 终解 JDK Vector API（&lt;code&gt;fma()&lt;/code&gt; 累加、&lt;code&gt;SPECIES_PREFERRED&lt;/code&gt; 自适应 lane 宽度、JIT 映射 SSE/AVX2/AVX-512）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全姿势&lt;/strong&gt;：&lt;code&gt;MatMulFactory&lt;/code&gt; 启动时探测，不可用则回退到 Lucene 风格的循环展开标量实现，不依赖 &lt;code&gt;--add-modules&lt;/code&gt; 一定在场。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产结果&lt;/strong&gt;：CPU 降约 7%、平均延迟降约 12%、CPU/RPS 改善约 10%、该特征的函数级 CPU 占比从 7.5% 降到约 1%；汇编从标量循环展开点积变成 AVX-512 向量化矩阵乘。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心结论&lt;/strong&gt;：优化拼的不是“最快的库”，而是把计算形状、内存布局、分配策略这些基本功做对——这些就位之后，Vector API 才成为那个“顺其自然的选择”。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://netflixtechblog.com/optimizing-recommendation-systems-with-jdks-vector-api-30d2830401ec&#34;&gt;Optimizing Recommendation Systems with JDK&amp;rsquo;s Vector API — Netflix Technology Blog&lt;/a&gt;（原文，作者 Harshad Sane，2026-03-03）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://openjdk.org/jeps/338&#34;&gt;JEP 338: Vector API (Incubator) — OpenJDK&lt;/a&gt;（Vector API 的 JEP 入口，含 SPECIES、fma、lane 等概念定义；后续孵化迭代见 JEP 417/426/448/460）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/fommil/netlib-java&#34;&gt;netlib-java — GitHub&lt;/a&gt;（文中提到的 Java BLAS 库，默认 F2J 路径的出处）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://lucene.apache.org/&#34;&gt;Apache Lucene&lt;/a&gt;（回退路径标量点积实现的灵感来源，Lucene 在向量检索里大量使用循环展开和 Panama Vector API）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    
  </channel>
</rss>
