<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>技术 on 小盒子的技术分享</title><link>https://xiaobox.github.io/categories/%E6%8A%80%E6%9C%AF/</link><description>Recent content in 技术 on 小盒子的技术分享</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sat, 06 Jun 2026 02:00:00 +0000</lastBuildDate><atom:link href="https://xiaobox.github.io/categories/%E6%8A%80%E6%9C%AF/index.xml" rel="self" type="application/rss+xml"/><item><title>你手机收到的消息通知，其实不是 App 发的</title><link>https://xiaobox.github.io/p/2026-06-06-ni-shou-ji-shou-dao-de-xiao-xi-tong-zhi-qi-shi-bu-shi-app-fa-de/</link><pubDate>Sat, 06 Jun 2026 02:00:00 +0000</pubDate><guid>https://xiaobox.github.io/p/2026-06-06-ni-shou-ji-shou-dao-de-xiao-xi-tong-zhi-qi-shi-bu-shi-app-fa-de/</guid><description>&lt;p&gt;&lt;img alt="推送通知的系统中转路径示意图" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/push-notification-cover.png"&gt;&lt;/p&gt;
&lt;p&gt;前两天我出差，飞机落地关飞行模式，手机一亮屏，叮叮叮叮叮叮叮。十几条微信、三封邮件、两条银行扣款提醒、一个菜鸟驿站的取件码、外加拼多多跟我说「你浏览过的商品降价了」。&lt;/p&gt;
&lt;p&gt;我寻思了一下我没寻思明白。&lt;/p&gt;
&lt;p&gt;我在天上飞了两个小时。手机关了飞行模式才连上网。那这些消息，到底是什么时候到的？&lt;/p&gt;
&lt;p&gt;如果是我连上网以后 App 才去服务器拉取的，那不可能这么快。几乎是飞行模式关掉的瞬间，十几条通知同时弹出来。&lt;/p&gt;
&lt;p&gt;如果是在我关掉飞行模式之前就已经在手机里了，那更不对。我没网，消息是怎么穿过空气到我手机上的？&lt;/p&gt;
&lt;p&gt;说实话，我用智能手机十几年了，每天收几十条推送，从来没认真想过这个问题。&lt;/p&gt;
&lt;p&gt;直到我开始查资料。然后我发现了一件事。&lt;/p&gt;
&lt;p&gt;你手机上的每一个 App，不管是微信、支付宝、抖音还是菜鸟裹裹，它们没有一个能直接把通知发到你手机上。&lt;/p&gt;
&lt;p&gt;一个都没有。&lt;/p&gt;
&lt;p&gt;你收到每一条推送，都经过了同一个中间人。在 iPhone 上它叫 APNs（Apple Push Notification service），在安卓上是 FCM（Firebase Cloud Messaging，谷歌家的）。&lt;/p&gt;
&lt;p&gt;你没看错。你的微信消息、你的支付宝转账提醒、你的抖音私信，所有这些，都不是「微信服务器 → 你的手机」。而是「微信服务器 → 苹果/谷歌服务器 → 你的手机」。&lt;/p&gt;
&lt;p&gt;所有 App 的通知，都在走苹果和谷歌的收费站。&lt;/p&gt;
&lt;p&gt;这听起来很奇怪。为什么不能直接发？微信自己有服务器，我微信也登录了，为什么非要让苹果和谷歌夹在中间？&lt;/p&gt;
&lt;p&gt;答案是，电池。&lt;/p&gt;
&lt;p&gt;你想象一下，如果你手机装了 100 个 App，每个 App 为了能随时收到通知，都必须各自在后台维持一条到自家服务器的长连接。100 个 App 就是 100 条连接，100 个后台进程，100 个在悄悄耗电的东西。&lt;/p&gt;
&lt;p&gt;你的手机根本扛不住。别说一天一充，可能上午十点就没了。&lt;/p&gt;
&lt;p&gt;所以苹果和谷歌想了个办法。他们不让每个 App 自己连服务器。他们让操作系统本身，在系统层面维持&lt;strong&gt;一条&lt;/strong&gt;长连接，就一条，连接到苹果或谷歌的推送服务器。所有 App 的通知，全部走这条管道。&lt;/p&gt;
&lt;p&gt;你的微信来消息了，微信服务器不是直接找你手机。它先把消息发给苹果的 APNs 服务器，APNs 再通过那条系统级的唯一连接，转发到你手机上。&lt;/p&gt;
&lt;p&gt;所以你知道为什么 iPhone 的推送比安卓稳定那么多吗？&lt;/p&gt;
&lt;p&gt;因为 iPhone 上，每一个 App 的推送都走同一条 APNs 通道。安卓呢？谷歌的 FCM 在国内基本不可用，于是华为、小米、OPPO、vivo、魅族各自建了自己的推送通道。你用小米手机，App 的推送走小米通道。你用华为，走华为通道。如果你用的 App 没有接入某个厂商的通道，那这个 App 在你手机上就收不到后台推送。除非它自己偷偷在后台跑进程保活。&lt;/p&gt;
&lt;p&gt;这就是为什么很多安卓用户抱怨「微信消息延迟」。不是微信的问题，是你手机厂商的推送通道和微信之间的配合出了问题。&lt;/p&gt;
&lt;p&gt;好，回到我刚才说的那个场景。飞机落地，飞行模式一关，十几条消息同时弹出来。&lt;/p&gt;
&lt;p&gt;这些消息是什么时候到的？&lt;/p&gt;
&lt;p&gt;答案是，在我关掉飞行模式&lt;strong&gt;之前&lt;/strong&gt;，它们就已经到了苹果的 APNs 服务器上。微信服务器把消息发给了 APNs，APNs 发现我的手机关机/飞行模式，就先帮我把消息存着。等我手机一恢复连接，APNs 立刻把存着的消息一次性推过来。&lt;/p&gt;
&lt;p&gt;苹果的 APNs 对每个 App 只保留&lt;strong&gt;最新一条&lt;/strong&gt;离线消息。比如微信给我发了五条消息，APNs 只存最后一条。这也是为什么有时候你收到一条微信推送，点进去发现有五六条未读。推送只显示了最后一条，前面几条在 App 打开后才从微信服务器拉取。&lt;/p&gt;
&lt;p&gt;谷歌的 FCM 大方一点，最多存 100 条，保存最长 4 周。&lt;/p&gt;
&lt;p&gt;这就是你断网后还能收到消息的全部秘密。不是消息穿越了空气，是消息在苹果和谷歌的服务器上排队等你。&lt;/p&gt;
&lt;p&gt;说到底，推送通知这件事，从它被设计出来的第一天起，就不是「App 发给用户」。它是「App 委托平台发给用户」。&lt;/p&gt;
&lt;p&gt;平台给你提供了统一的推送管道，帮你省了电，让你的手机不用被 100 个 App 的后台进程拖死。代价是，所有通知都要从它手里过一遍。&lt;/p&gt;
&lt;p&gt;苹果和谷歌不止知道你在用什么 App。它们知道你什么时候收到了什么消息，谁发给你的，发了多少次。它们可以选择延迟投递、降低优先级、甚至在你开了「通知摘要」之后帮你把不重要的消息打包到下午三点一起发。&lt;/p&gt;
&lt;p&gt;你以为你在收通知。其实苹果和谷歌在替你决定，你什么时候看到什么。&lt;/p&gt;
&lt;p&gt;你手机上的每一条推送，都不是 App 发给你的。是苹果和谷歌发给你的。App 只是把话递了过去。&lt;/p&gt;
&lt;p&gt;这个系统从 2008 年 iPhone 引入 APNs 开始，到现在十七年了，没有变过。而且短期内也不会变。因为它确实是最好的方案。一条系统级连接，比一百条各自为政的 App 连接，对电池友好太多了。&lt;/p&gt;
&lt;p&gt;只是大多数人从来没想过这件事。&lt;/p&gt;
&lt;p&gt;下次你手机一亮屏，收到一条「你浏览过的商品降价了」的时候，可以想想这条消息的旅程。&lt;/p&gt;
&lt;p&gt;拼多多的服务器写了一行字。这行字先飞到加州库比蒂诺的苹果服务器，或者加州山景城的谷歌服务器。在那里等了一会儿，然后穿过太平洋，降落到你手上。&lt;/p&gt;
&lt;p&gt;你以为是拼多多在叫你。其实中间还有个库克。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;读到这你可能会想，行，我知道了。推送都得过苹果和谷歌的手。那如果我不想过他们的手呢？&lt;/p&gt;
&lt;p&gt;比如我一个写代码的，服务器上跑着几个脚本，爬虫跑完了我想让它告诉我一声，CI 构建挂了我想立刻知道，磁盘快满了别等我发现的时候已经爆了。这些场景，我怎么把消息从服务器推到我自己手机上？&lt;/p&gt;
&lt;p&gt;难道我要为了这个去注册一个 Apple Developer 账号，每年交 99 美元，然后自己写 APNs 对接逻辑？&lt;/p&gt;
&lt;p&gt;显然不现实。&lt;/p&gt;
&lt;p&gt;这就要说到另一个世界了。和 App 运营推送不同，个人开发者、运维、脚本小子们早就有了一套自己的工具箱。它们的目标不是给几百万用户发营销通知，而是「用一个 HTTP 请求把消息送到自己手机」。&lt;/p&gt;
&lt;p&gt;我举个例子你就明白了。&lt;/p&gt;
&lt;p&gt;你写了一个爬虫，每天早上八点自动抓某个网站的最新文章。跑完了，你想在微信里收到一条提醒，「今日抓取完成，新增 12 篇」。怎么做？&lt;/p&gt;
&lt;p&gt;最简单的办法，用 Server 酱。你注册拿一个 SendKey，然后在爬虫脚本最后加一行 curl。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;1&lt;/span&gt;&lt;span class="cl"&gt;curl -X POST &lt;span class="s2"&gt;&amp;#34;https://sctapi.ftqq.com/&amp;lt;你的SendKey&amp;gt;.send&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;2&lt;/span&gt;&lt;span class="cl"&gt; -d &lt;span class="s2"&gt;&amp;#34;title=爬虫完成&amp;#34;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;3&lt;/span&gt;&lt;span class="cl"&gt; -d &lt;span class="s2"&gt;&amp;#34;desp=今日新增 12 篇文章&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;爬虫跑完，这行命令执行，你微信里就弹出一条消息。不需要自己写 App，不需要研究 APNs 怎么对接，不需要维护长连接。就一行 curl。&lt;/p&gt;
&lt;p&gt;Server 酱在背后做的事情其实很简单。它接收你的 HTTP 请求，然后通过微信测试号、企业微信等通道把消息转发到你手机上。它更像一个「通知中转站」。你把话递给它，它帮你送到微信。&lt;/p&gt;
&lt;p&gt;但这东西有个问题。它依赖 Server 酱的服务器。人家跑路了你怎么办？人家改收费规则了你怎么办？人家被微信封了接口你怎么办？&lt;/p&gt;
&lt;p&gt;所以有人做了开源自建版。&lt;/p&gt;
&lt;p&gt;message-pusher 是一个比较完整的替代品。你自己部署一个服务，支持微信、企业微信、飞书、钉钉、Bark、Telegram、Discord 等一堆通道。有 Web 管理后台，可以管理多个用户。它还兼容 Server 酱的 API 格式，你之前写的 curl 基本不用改。&lt;/p&gt;
&lt;p&gt;Heimdallr 更轻。它的定位是「通知网关」，不是你部署一个完整后台，而是一个轻量的路由器。你把 Bark、企业微信、ntfy、飞书等各种通道配置好，它帮你做转发。还支持 Serverless 部署，基本零成本跑起来。&lt;/p&gt;
&lt;p&gt;如果你不想依赖微信呢？微信毕竟不是为这种场景设计的，接口随时可能收紧，消息模板也可能被限制。&lt;/p&gt;
&lt;p&gt;那就用 ntfy。&lt;/p&gt;
&lt;p&gt;ntfy 的思路特别简洁。你自建一个 ntfy 服务，手机装一个 ntfy 客户端。然后任何地方、任何脚本，发一个 HTTP PUT 或 POST 到你的 ntfy 服务器，手机立刻就弹出通知。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;1&lt;/span&gt;&lt;span class="cl"&gt;curl -d &lt;span class="s2"&gt;&amp;#34;备份完成，耗时 3 分钟&amp;#34;&lt;/span&gt; ntfy.mydomain.com/my-server
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;就这么一行。消息到了你的 ntfy 服务，然后通过 WebSocket 推到你手机的 ntfy 客户端，客户端触发系统通知。整个过程不经过苹果的 APNs、不经过微信、不经过任何第三方。全程你自己的服务器和你自己的客户端在对话。&lt;/p&gt;
&lt;p&gt;当然，ntfy 在 iOS 上受限于系统限制，后台推送还是走了 APNs。但它至少让你的消息不用过微信那关。&lt;/p&gt;
&lt;p&gt;类似的还有 Gotify，更偏 Android 和 Web 端。&lt;/p&gt;
&lt;p&gt;如果你在做产品，不是一个脚本通知的问题，而是整个产品的通知中心要搭建起来。用户要在 App 内收站内信、要收邮件通知、要收短信、要收 Push。Novu 这种基础设施就派上用场了。它提供统一的 API，把 Inbox、邮件、短信、Push 这些渠道编排起来，支持工作流、条件分支、摘要合并。但这已经是另一个级别的东西了，和我们说的「脚本发个通知到手机」不是一个赛道。&lt;/p&gt;
&lt;p&gt;还有一个专门做 App 推送的工具叫 gorush。它是 Go 写的推送网关，直接对接 APNs、FCM、华为 HMS。如果你有自己的 App，已经有用户的 device token，gorush 帮你把消息发给系统推送服务。它解决的是「服务端怎么调用 APNs/FCM」，不是帮你做用户运营。&lt;/p&gt;
&lt;p&gt;所以你看，整个推送世界的格局其实分两层。&lt;/p&gt;
&lt;p&gt;上层是苹果和谷歌控制的系统级推送。所有 C 端 App 的通知，不管你是微信还是抖音还是拼多多，都得走这条路。这是为了电池、为了系统稳定性，你没办法绕过去。&lt;/p&gt;
&lt;p&gt;下层是开发者和运维人员自己搞的「通知工具箱」。Server 酱、ntfy、Heimdallr、message-pusher、Gotify，这些工具的存在是因为苹果和谷歌的推送体系根本不是为「服务器告警」「脚本通知」「爬虫完成提醒」这种场景设计的。你要用官方的 APNs 给自己的脚本发通知，那得先写一个 iOS App、上架 App Store、拿到 device token，然后才能调用 APNs 接口。&lt;/p&gt;
&lt;p&gt;杀鸡用牛刀都不够形容这件事的荒谬。&lt;/p&gt;
&lt;p&gt;所以总结一下。如果你只是一个写代码的，想让服务器上的事情发生时你能第一时间知道，&lt;/p&gt;
&lt;p&gt;想省事，用 Server 酱，一行 curl 搞定，消息到微信。
想自建、不想依赖第三方服务，用 ntfy 或 Gotify，自己部署，自己掌控。
想微信收到但不想用托管服务，用 wecomchan 或 Heimdallr，走企业微信通道。
要做产品通知中心，用 Novu。
要给自己 App 做推送，用 gorush。&lt;/p&gt;
&lt;p&gt;下次你服务器磁盘快满了，与其等用户告诉你网站打不开，不如让脚本在磁盘用到 85% 的时候给你发一条 ntfy 通知。&lt;/p&gt;
&lt;p&gt;比库克转告你快多了。&lt;/p&gt;</description></item><item><title>矩阵转置：数学上免费的操作，为什么 CPU 一算就慢 10 倍？</title><link>https://xiaobox.github.io/p/2026-06-06-ju-zhen-zhuan-zhi-shu-xue-shang-mian-fei-de-cao-zuo-wei-shen-me-cpu-yi-suan-jiu-man-10-bei/</link><pubDate>Sat, 06 Jun 2026 02:00:00 +0000</pubDate><guid>https://xiaobox.github.io/p/2026-06-06-ju-zhen-zhuan-zhi-shu-xue-shang-mian-fei-de-cao-zuo-wei-shen-me-cpu-yi-suan-jiu-man-10-bei/</guid><description>&lt;p&gt;有小伙伴在面试的时候遇到了这道题:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&amp;ldquo;你写一个矩阵转置,然后告诉我怎么优化它。&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;前几天有读者私信我,说他面字节后端,聊到 cache 命中率的时候,面试官随手抛出来这一题。他答了&amp;quot;双层循环交换 &lt;code&gt;A[i][j]&lt;/code&gt; 和 &lt;code&gt;A[j][i]&lt;/code&gt;&amp;quot;,面试官追问:&amp;ldquo;那为什么这么写在大矩阵上会慢得离谱?&amp;quot;——他一下卡住了。&lt;/p&gt;
&lt;p&gt;这事真挺有意思的。从数学课本上看,矩阵转置就是把行变成列,这操作几乎是免费的——你甚至不用动数据,把下标 &lt;code&gt;[i][j]&lt;/code&gt; 改成 &lt;code&gt;[j][i]&lt;/code&gt; 不就完事了吗?&lt;/p&gt;
&lt;p&gt;但你真去跑代码,1024×1024 的矩阵,&lt;strong&gt;写得&amp;quot;对&amp;quot;的转置和写得&amp;quot;错&amp;quot;的转置,运行时间能差 10 倍以上&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;10 倍。同一个算法,同样的输入,同样的硬件。&lt;/p&gt;
&lt;p&gt;更诡异的是,这 10 倍不是靠什么高深算法拿下来的——只靠调整了一下代码装出来的&amp;quot;顺序&amp;rdquo;。你甚至不需要会汇编,不需要会 SIMD 指令,只需要看明白一件事:&lt;strong&gt;CPU 不是数学家,它不认识&amp;quot;行&amp;quot;跟&amp;quot;列&amp;quot;。它只认识内存上的地址跟跳幅。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;咱们从最浅的地方起步。不讲公式,不讲 CSAPP 里那些令人望而却步的练习题。咱们只看一个 case:&lt;strong&gt;一个 4×4 的矩阵,在内存上是怎么摆的?按行读和按列读,到底有什么区别&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;看明白这个,你以后看到一堆看似不相干的问题都会觉得熟悉——为什么 pandas 里按列跳访问会慢?为什么卷积算子需要 NCHW 跟 NHWC 两种布局?为什么 ClickHouse 是列存储而 MySQL 是行存储?这些问题背后都是同一件事的不同面孔。&lt;/p&gt;
&lt;p&gt;接下来咱们就来探究探究——数学上 0 成本的事,在 CPU 眼里到底贵在哪儿。&lt;/p&gt;
&lt;p&gt;&lt;img alt="矩阵转置数学定义示意,A 和 A 的转置 A^T,行变列、列变行" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-01-%E6%95%B0%E5%AD%A6%E5%AE%9A%E4%B9%89.png"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="任性客户"&gt;&lt;a href="#%e4%bb%bb%e6%80%a7%e5%ae%a2%e6%88%b7" class="header-anchor"&gt;&lt;/a&gt;任性客户
&lt;/h2&gt;&lt;p&gt;先把任性客户请出来。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;任性客户:&amp;ldquo;我要把这个 4×4 矩阵的行列对调。&lt;code&gt;A[0][1]&lt;/code&gt; 跑到 &lt;code&gt;A[1][0]&lt;/code&gt; 的位置,&lt;code&gt;A[0][2]&lt;/code&gt; 跑到 &lt;code&gt;A[2][0]&lt;/code&gt;,以此类推。简单吧?&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;如果你按它说的写 C 代码,长这样:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;2&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;3&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;4&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;5&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一行核心逻辑,优雅极了。数学家看了点头,新手看了也觉得 make sense。&lt;/p&gt;
&lt;p&gt;但这段代码在 N=1024 的时候,大概要跑 30 毫秒——而经过一些优化后,同样的事情可以在 3 毫秒内做完。&lt;strong&gt;慢了整整 10 倍&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;为啥?因为任性客户只懂数学,不懂硬件。它不知道,自己嘴里轻飘飘的&amp;quot;行变列&amp;quot;,对 CPU 来说是一场噩梦。&lt;/p&gt;
&lt;p&gt;要理解这场噩梦,得先认识下一个角色——&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="一个纸条--内存"&gt;&lt;a href="#%e4%b8%80%e4%b8%aa%e7%ba%b8%e6%9d%a1--%e5%86%85%e5%ad%98" class="header-anchor"&gt;&lt;/a&gt;一个纸条 —— 内存
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;内存,在 CPU 眼里长什么样?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;很多人脑子里的内存,是一个二维方格,横竖排得整整齐齐。其实不是。&lt;/p&gt;
&lt;p&gt;内存对 CPU 来说,&lt;strong&gt;就是一根超长的纸条&lt;/strong&gt;,从地址 0 开始,一直延伸到几十亿。每个地址上能放 1 个字节。仅此而已。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;纸条:&amp;ldquo;我从头到尾就是这么排的,没有行,没有列,只有头到尾。你的 4×4 矩阵?我这儿是 16 个连续的格子。&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;那你写 &lt;code&gt;A[2][3]&lt;/code&gt; 的时候,CPU 怎么知道去哪里取数据?&lt;/p&gt;
&lt;p&gt;答案是——&lt;strong&gt;约定&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;C 语言约定矩阵是&lt;strong&gt;行优先存储&lt;/strong&gt;(Row-major)。意思是,先把第一行从头到尾铺在纸条上,再接着铺第二行、第三行……&lt;/p&gt;
&lt;p&gt;举个例子,一个 4×4 矩阵在内存里实际上是这样排的:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;1&lt;/span&gt;&lt;span class="cl"&gt;地址: 0 1 2 3 4 5 6 7 8 ... 15
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;2&lt;/span&gt;&lt;span class="cl"&gt;内容: A[0][0] A[0][1] A[0][2] A[0][3] A[1][0] A[1][1] ... A[3][3]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;整整齐齐 16 个值,一字排开。如果矩阵是 &lt;code&gt;int&lt;/code&gt; 类型(4 字节),那地址就是 0, 4, 8, 12, 16&amp;hellip; 一直到 60。不管你脑子里把它当成「4 行 4 列」,到了纸条上,它就是 16 个挨着的格子。&lt;/p&gt;
&lt;p&gt;&lt;img alt="一根长纸条,上面按行优先平铺一个 4x4 矩阵的所有元素,地址从 0 到 15" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-02-%E5%86%85%E5%AD%98%E7%BA%B8%E6%9D%A1.png"&gt;&lt;/p&gt;
&lt;p&gt;比如你写 &lt;code&gt;A[2][3]&lt;/code&gt;,编译器其实在算 &lt;code&gt;*(A + 2*N + 3)&lt;/code&gt;——先跳过前 2 整行(每行 N 个),再走 3 个,从纸条上的这个位置取一个值。我们能 &lt;code&gt;A[i][j]&lt;/code&gt; 这样写,完全是编译器在替我们做地址翻译。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;那 Fortran / MATLAB / 老式 BLAS 不是列优先的吗?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;是。不同语言约定不一样,Fortran 系是列优先(Column-major),也就是先铺第一列再铺第二列。但你今天写 C / C++ / Python (NumPy 默认) / Java 数组,默认都是行优先。&lt;strong&gt;咱们这篇文章就按行优先讨论&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img alt="同一个 4x4 矩阵的两种存储方式:左边行优先(C / NumPy),右边列优先(Fortran)" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-03-%E8%A1%8C%E5%88%97%E4%BC%98%E5%85%88%E5%AF%B9%E6%AF%94.png"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;那么问题来了——既然内存只是一根纸条,CPU 一次能从纸条上取一个字节,不就行了吗?为什么还要扯什么命中率?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;因为 CPU &lt;strong&gt;从内存上取一个字节,实在太贵了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一次内存访问大概 100 个时钟周期。CPU 自己干一次乘法,只要 1 个周期。也就是说,&lt;strong&gt;CPU 算 1 次乘法的时间,内存才走完百分之一的路程&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;要是每个数据都得这么等,工人就别活了。&lt;/p&gt;
&lt;p&gt;所以中间得有人当中介,把数据先&amp;quot;批发&amp;quot;一批过来。这人就是下一个出场的&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="公交车小哥--cache-line"&gt;&lt;a href="#%e5%85%ac%e4%ba%a4%e8%bd%a6%e5%b0%8f%e5%93%a5--cache-line" class="header-anchor"&gt;&lt;/a&gt;公交车小哥 —— Cache Line
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;Cache Line 是个啥?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;它是 CPU 缓存里的&amp;quot;最小搬运单位&amp;quot;。不管你要的是 1 个字节还是 1 个 int,CPU 一次从内存搬到 cache 的,都是&lt;strong&gt;一整个 cache line&lt;/strong&gt;——通常 &lt;strong&gt;64 字节&lt;/strong&gt;(也就是 16 个 int,或者 8 个 double)。&lt;/p&gt;
&lt;p&gt;我给它起个名字,叫&lt;strong&gt;公交车小哥&lt;/strong&gt;。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;公交车小哥:&amp;ldquo;我从纸条上跑一趟,一次拉 64 字节。你只要一个值?没事,你的座位旁边那 63 字节我也顺手捎上,放到 CPU 旁边的小货架(L1 cache)。你下次要的值如果在我刚拉的这一车里,你就不用等了,直接从货架上拿,1 个周期搞定。&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这就是 cache 的核心机制——&lt;strong&gt;空间局部性&lt;/strong&gt;。一次拉一车,赌你接下来要的东西在隔壁。&lt;/p&gt;
&lt;p&gt;那这个赌赌得对不对,直接决定了你程序快不快。&lt;/p&gt;
&lt;p&gt;&lt;img alt="Cache Line 公交车小哥示意:一辆卡通公交车从内存纸条上一次拉走 64 字节(16 个 int 槽位),送到 CPU 旁边的 L1 货架上" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-04-%E5%85%AC%E4%BA%A4%E8%BD%A6.png"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="按行读-vs-按列读--公交车的两种命运"&gt;&lt;a href="#%e6%8c%89%e8%a1%8c%e8%af%bb-vs-%e6%8c%89%e5%88%97%e8%af%bb--%e5%85%ac%e4%ba%a4%e8%bd%a6%e7%9a%84%e4%b8%a4%e7%a7%8d%e5%91%bd%e8%bf%90" class="header-anchor"&gt;&lt;/a&gt;按行读 vs 按列读 —— 公交车的两种命运
&lt;/h2&gt;&lt;p&gt;现在,我们把任性客户、一根纸条、公交车小哥三个角色放到同一张桌上,看看到底发生了什么。&lt;/p&gt;
&lt;p&gt;比如下面这两段几乎一模一样的代码,差别只是循环变量的内外顺序:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 版本 A:外层 i,内层 j
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;2&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;3&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;4&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;5&lt;/span&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;6&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 版本 B:外层 j,内层 i
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;7&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;8&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;9&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;版本 A 是&lt;strong&gt;按行读&lt;/strong&gt;,版本 B 是&lt;strong&gt;按列读&lt;/strong&gt;。两段代码做的事情数学上完全一样——把矩阵里所有数加起来。但在 1024×1024 的矩阵上,A 比 B 快了 5 倍以上。&lt;/p&gt;
&lt;p&gt;这事就是任性客户没想清楚的根源。咱们逐个场景捋。&lt;/p&gt;
&lt;h3 id="场景一按行读顺序友好"&gt;&lt;a href="#%e5%9c%ba%e6%99%af%e4%b8%80%e6%8c%89%e8%a1%8c%e8%af%bb%e9%a1%ba%e5%ba%8f%e5%8f%8b%e5%a5%bd" class="header-anchor"&gt;&lt;/a&gt;场景一:按行读(顺序友好)
&lt;/h3&gt;&lt;p&gt;假设工人要把第 0 行从头读到尾:&lt;code&gt;A[0][0]&lt;/code&gt; → &lt;code&gt;A[0][1]&lt;/code&gt; → &lt;code&gt;A[0][2]&lt;/code&gt; → &lt;code&gt;A[0][3]&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;工人对公交车说:&amp;ldquo;我要 &lt;code&gt;A[0][0]&lt;/code&gt;。&amp;rdquo;&lt;/li&gt;
&lt;li&gt;公交车跑到纸条的地址 0,&lt;strong&gt;一趟拉走 16 个 int&lt;/strong&gt;:&lt;code&gt;A[0][0]&lt;/code&gt; 到 &lt;code&gt;A[3][3]&lt;/code&gt;(假设这小矩阵正好塞下一个 cache line,先这么简化)。送到 L1 货架。&lt;/li&gt;
&lt;li&gt;工人下一个要 &lt;code&gt;A[0][1]&lt;/code&gt;——公交车说:&amp;ldquo;在车上,直接从货架拿。&amp;rdquo;&lt;strong&gt;0 等待&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A[0][2]&lt;/code&gt; &lt;code&gt;A[0][3]&lt;/code&gt; 同理,都在车上。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;4 次读取,公交车只跑了 1 趟&lt;/strong&gt;。命中率 75%(4 次里只有第 1 次去内存)。&lt;/p&gt;
&lt;p&gt;&lt;img alt="按行读的命中场景:公交车跑一趟,工人连续从货架上拿 4 个值,标注 1 miss + 3 hit" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-05-%E6%8C%89%E8%A1%8C%E8%AF%BB%E5%91%BD%E4%B8%AD.png"&gt;&lt;/p&gt;
&lt;h3 id="场景二按列读转置在干的事"&gt;&lt;a href="#%e5%9c%ba%e6%99%af%e4%ba%8c%e6%8c%89%e5%88%97%e8%af%bb%e8%bd%ac%e7%bd%ae%e5%9c%a8%e5%b9%b2%e7%9a%84%e4%ba%8b" class="header-anchor"&gt;&lt;/a&gt;场景二:按列读(转置在干的事)
&lt;/h3&gt;&lt;p&gt;现在换成转置——工人要按列读:&lt;code&gt;A[0][0]&lt;/code&gt; → &lt;code&gt;A[1][0]&lt;/code&gt; → &lt;code&gt;A[2][0]&lt;/code&gt; → &lt;code&gt;A[3][0]&lt;/code&gt;(读第 0 列)。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;工人对公交车说:&amp;ldquo;我要 &lt;code&gt;A[0][0]&lt;/code&gt;。&amp;rdquo;&lt;/li&gt;
&lt;li&gt;公交车从地址 0 一趟拉走 &lt;code&gt;A[0][0]&lt;/code&gt; 到 &lt;code&gt;A[0][3]&lt;/code&gt; 这一整行(因为内存是行优先排的,连续的 4 个值是同一行)。送到货架。&lt;/li&gt;
&lt;li&gt;工人下一个要 &lt;code&gt;A[1][0]&lt;/code&gt;——公交车一看:&amp;ldquo;不在我刚拉的这车里啊,&lt;code&gt;A[1][0]&lt;/code&gt; 在纸条更远的地方,我得再跑一趟。&amp;rdquo;&lt;/li&gt;
&lt;li&gt;公交车再跑一趟,这次拉来 &lt;code&gt;A[1][0]&lt;/code&gt; 到 &lt;code&gt;A[1][3]&lt;/code&gt;。工人只要 &lt;code&gt;A[1][0]&lt;/code&gt;,&lt;strong&gt;车上剩下 3 个全白拉&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;A[2][0]&lt;/code&gt; &lt;code&gt;A[3][0]&lt;/code&gt; 同理,各跑一趟,各白拉 3 个。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;4 次读取,公交车跑了 4 趟&lt;/strong&gt;。命中率 0%。每趟车 75% 的座位是空跑。&lt;/p&gt;
&lt;p&gt;&lt;img alt="按列读的灾难场景:公交车跑了 4 趟,每趟只有 1 个值有用,其余 3 个白拉,标注 4 miss + 0 hit" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-06-%E6%8C%89%E5%88%97%E8%AF%BB%E7%81%BE%E9%9A%BE.png"&gt;&lt;/p&gt;
&lt;p&gt;你看出来了吗?&lt;/p&gt;
&lt;p&gt;任性客户(转置算法)嘴上说&amp;quot;我就改个下标 &lt;code&gt;[i][j]&lt;/code&gt; → &lt;code&gt;[j][i]&lt;/code&gt;&amp;quot;,但底下的执行是——&lt;strong&gt;他在按列读源矩阵 A,然后按行写目标矩阵 B&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;读这边 cache 全 miss。&lt;/p&gt;
&lt;p&gt;而当矩阵尺寸放大到 1024×1024,这个 miss 不是慢一点点的事——是 30 毫秒 vs 3 毫秒的事。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;等等,那写 B 的那一边呢?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;按行写 B 是 cache 友好的,这边没问题。但只要读 A 那一边全 miss,整个流程就被拖垮了——你最快也只能跟着最慢那一边走。&lt;/p&gt;
&lt;p&gt;&lt;img alt="1024x1024 矩阵 cache miss 热力图,naive 转置版本几乎全红;后面会展示分块算法版本几乎全蓝" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-07-cache%E7%83%AD%E5%8A%9B%E5%9B%BE.png"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="跑腿小弟--prefetcher"&gt;&lt;a href="#%e8%b7%91%e8%85%bf%e5%b0%8f%e5%bc%9f--prefetcher" class="header-anchor"&gt;&lt;/a&gt;跑腿小弟 —— Prefetcher
&lt;/h2&gt;
 &lt;blockquote&gt;
 &lt;p&gt;等会儿,CPU 不是有 Prefetcher 吗?它不能帮忙提前把数据拉过来吗?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;好问题。Prefetcher 这个角色咱们得请出来聊聊。&lt;/p&gt;
&lt;p&gt;我给它起个名字,叫&lt;strong&gt;跑腿小弟&lt;/strong&gt;。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;跑腿小弟:&amp;ldquo;我有个特异功能——只要你访问内存的方式有规律,我就能猜出你下一步要去哪儿,&lt;strong&gt;提前替你叫公交车跑一趟,把货先取回来&lt;/strong&gt;。你到的时候货架上已经有了,你不用等。&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;跑腿小弟最擅长的是&lt;strong&gt;直线追踪&lt;/strong&gt;——你一直往前走,他就一直提前发车去下一站拉货。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;访问模式&lt;/th&gt;
 &lt;th&gt;跑腿小弟的反应&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;顺序读 &lt;code&gt;A[0]&lt;/code&gt; &lt;code&gt;A[1]&lt;/code&gt; &lt;code&gt;A[2]&lt;/code&gt; &lt;code&gt;A[3]&lt;/code&gt;&amp;hellip;&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;稳了,我提前走一趟把下批拉回来&amp;rdquo;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;倒序读 &lt;code&gt;A[N]&lt;/code&gt; &lt;code&gt;A[N-1]&lt;/code&gt;&amp;hellip;&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;也行,反向直线我也能摸准&amp;rdquo;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;固定步长读 &lt;code&gt;A[0]&lt;/code&gt; &lt;code&gt;A[16]&lt;/code&gt; &lt;code&gt;A[32]&lt;/code&gt;&amp;hellip;&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;勉强能猜&amp;rdquo;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;strong&gt;跳读 / 列读 / 随机&lt;/strong&gt;&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;我跟不上,我也猜不到下一步&amp;rdquo;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;转置算法是哪一种?&lt;strong&gt;列读&lt;/strong&gt;。每两次访问跳一整行的距离(N 个 int 这么远)。&lt;/p&gt;
&lt;p&gt;跑腿小弟摊摊手:&amp;ldquo;这种我真没办法,你跳的步子是变量,我不知道你下一步在哪。&amp;rdquo;&lt;/p&gt;
&lt;p&gt;所以转置不仅 cache 命中率惨,Prefetcher 这个外援也用不上。工人就这么活生生等死。&lt;/p&gt;
&lt;p&gt;&lt;img alt="跑腿小弟示意:左边顺序读,跑腿小弟提前发车把下一批数据取回来;右边列读,跑腿小弟挠头摊手,数据还没到工人就开始发呆" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-12-prefetcher-v2.png"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="干活的工人--cpu-核心"&gt;&lt;a href="#%e5%b9%b2%e6%b4%bb%e7%9a%84%e5%b7%a5%e4%ba%ba--cpu-%e6%a0%b8%e5%bf%83" class="header-anchor"&gt;&lt;/a&gt;干活的工人 —— CPU 核心
&lt;/h2&gt;&lt;p&gt;来正式介绍下主角:&lt;strong&gt;CPU 核心&lt;/strong&gt;,也就是真正干活的工人。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;工人:&amp;ldquo;我一秒钟能做几十亿次运算。但前提是,数据得在我手边的 L1 货架上。&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;工人的工作流程是这样的:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;看下一条指令,需要 &lt;code&gt;A[i][j]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;伸手去 L1 货架上摸——
&lt;ul&gt;
&lt;li&gt;摸到了(&lt;strong&gt;cache hit&lt;/strong&gt;):1 个周期,直接干&lt;/li&gt;
&lt;li&gt;没摸到(&lt;strong&gt;cache miss&lt;/strong&gt;):喊公交车去内存拉,自己原地等 100+ 个周期&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;工人最讨厌的就是等。等公交车这 100 个周期里,他可以做的事:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;100 次乘法&lt;/li&gt;
&lt;li&gt;200 次加法&lt;/li&gt;
&lt;li&gt;50 次浮点运算&lt;/li&gt;
&lt;li&gt;看 50 遍内心 OS 想去哪个数据中心打工&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而 naive 转置在干啥?在让工人&lt;strong&gt;每读一个值就等一次&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如果矩阵是 1024×1024,那就是 100 多万次等待。&lt;/p&gt;
&lt;p&gt;我们换算一下:1024×1024 个 int 是 4MB,远远超过 L1 cache(典型 32-64KB)甚至 L2 cache(典型 256KB-1MB)。这意味着公交车从 L1 一直 miss 到 L2,还可能 miss 到 L3,最坏情况直接打到主内存。每一层 miss 的代价递增——&lt;strong&gt;L1 miss 大概 4 周期,L2 miss 大概 12 周期,L3 miss 大概 40 周期,主内存 miss 一上来就是 100+ 周期起步&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;按列读的转置,在大矩阵上几乎每一次都在踩主内存。100 万次 × 100 周期 = 1 亿个周期空等。CPU 主频再高也救不了。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;工人:&amp;ldquo;我等公交等到天荒地老,真正干活时间不到 5%。&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;这就是为什么数学上免费的操作,在硬件上跑出来贵得离谱——&lt;strong&gt;95% 的时间花在了搬数据,5% 的时间才在算数据&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="聪明的搬家公司--分块算法"&gt;&lt;a href="#%e8%81%aa%e6%98%8e%e7%9a%84%e6%90%ac%e5%ae%b6%e5%85%ac%e5%8f%b8--%e5%88%86%e5%9d%97%e7%ae%97%e6%b3%95" class="header-anchor"&gt;&lt;/a&gt;聪明的搬家公司 —— 分块算法
&lt;/h2&gt;&lt;p&gt;那这事有救吗?有。请出最后一个关键角色——&lt;strong&gt;聪明的搬家公司&lt;/strong&gt;(Blocked / Tiled Transpose)。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;搬家公司老板:&amp;ldquo;你别一格一格来回跑了。我把矩阵切成小方块,每次先把一个小方块的所有数据都搬到工人桌上,搬完一个再搬下一个。工人在桌上随便转——反正都在 cache 里。&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;具体怎么做?把 1024×1024 的矩阵想象成 128×128 个 8×8 的小方块。一次只处理一个小方块:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define BLOCK 8
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 2&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ii&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;ii&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;ii&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;BLOCK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 3&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;jj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;jj&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;jj&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;BLOCK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 4&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// 这个小方块内部,行列都在 cache 里
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 5&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ii&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;ii&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;BLOCK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 6&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jj&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;jj&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;BLOCK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 7&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;B&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 8&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 9&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;10&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;11&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关键点是,&lt;strong&gt;8×8 的小方块完整地塞进了 L1 cache&lt;/strong&gt;。在这个方块内部,不管你按行读还是按列读,公交车都不用再跑——所有数据已经在货架上了。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;那 8 这个数字哪儿来的?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;不是拍脑袋。它取决于 cache line 大小(64 字节)和 L1 cache 容量(典型 32KB)。8×8 个 int = 256 字节,占 4 个 cache line,远远小于 L1 容量;但又大到能摊薄&amp;quot;搬一个方块&amp;quot;的固定开销。这是工程上反复试出来的。&lt;/p&gt;
&lt;p&gt;不同硬件,这个数会变——有的用 16,有的用 32,有的甚至按 cache 行精确对齐。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;这跟 OS 的 page 是一回事吗?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;不是一回事,但思路一脉相承。OS 把内存切成 4KB 的 page,因为 TLB 一次只能装下有限数量的 page 映射,page 用得分散,TLB 就 miss。Cache 把内存切成 64 字节的 line,因为 cache 容量有限,你访问得分散,cache 就 miss。&lt;strong&gt;Page、cache line、寄存器,归根结底都是同一个思路的不同尺度——预先把你「接下来可能要的东西」放近一点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;所以你一旦理解了 cache line,操作系统里那些虚拟内存、TLB、page table 的设计,基本就触类旁通了。&lt;/p&gt;
&lt;p&gt;&lt;img alt="分块算法示意:左边是任性客户跳着读 1024x1024,右边是搬家公司一次只处理一个 8x8 小方块,标注\"先把方块搬到 cache,再随便转\"" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-08-%E5%88%86%E5%9D%97%E7%AE%97%E6%B3%95.png"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;那这个改造能快多少?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;我之前在一台普通 MacBook 上跑过 benchmark(1024×1024 double 矩阵):&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;版本&lt;/th&gt;
 &lt;th style="text-align: right"&gt;耗时&lt;/th&gt;
 &lt;th style="text-align: right"&gt;加速比&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;naive 转置&lt;/td&gt;
 &lt;td style="text-align: right"&gt;31.4 ms&lt;/td&gt;
 &lt;td style="text-align: right"&gt;1×&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;分块 8×8&lt;/td&gt;
 &lt;td style="text-align: right"&gt;5.2 ms&lt;/td&gt;
 &lt;td style="text-align: right"&gt;6×&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;分块 8×8 + SIMD&lt;/td&gt;
 &lt;td style="text-align: right"&gt;3.1 ms&lt;/td&gt;
 &lt;td style="text-align: right"&gt;10×&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;分块 32×32(L2-aware)&lt;/td&gt;
 &lt;td style="text-align: right"&gt;2.8 ms&lt;/td&gt;
 &lt;td style="text-align: right"&gt;11×&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;10 倍加速,改的是同一个算法,同样的输入,同样的硬件。&lt;/p&gt;
&lt;p&gt;&lt;img alt="benchmark 柱状图,naive 31ms / 分块 5ms / +SIMD 3ms,横向对比清晰" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-14-benchmark%E6%9F%B1%E7%8A%B6%E5%9B%BE.png"&gt;&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;那是不是越大的块越好?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;不是。块太小,块间调度的开销占比变大(每个小块只能摊一点点数据);块太大,重新 miss 出 cache,重蹈覆辙。中间那个理想值要根据 cache 容量反复调,面试场景下记住&amp;quot;快不是拍脑袋,是拼 cache 容量&amp;quot;就够了。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;有同学要挑刺:Python / NumPy 里应该不用操心这个吧?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;你以为不用,其实是有人帮你把活儿干了。NumPy 的 &lt;code&gt;A.T&lt;/code&gt; 调用极便宜——它根本不动数据,只是返回一个「view」,把行、列的 stride 互换了一下。这步是免费的。但你一旦拿这个 view 去参与计算——比如 &lt;code&gt;np.dot(A.T, B)&lt;/code&gt;——底层就要调 BLAS。&lt;strong&gt;BLAS 这个库被全世界的工程师抠了几十年,干的就是把这种 cache miss 压下去的活&lt;/strong&gt;。你看不见,不代表没做。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;那 GPU 上的转置也是这个逻辑吗?&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;一样的逻辑,场景更极端。GPU 的 shared memory(类似 L1)只有几十 KB,中间还隔着 bank conflict 这种额外的坑。CUDA 里经典的转置例子,会特地把 tile 调成 32×33 而不是 32×32——多出来的那一列专门用来错开 bank 冲突。你一个转置算子调优,能从「跑得动」调到「跑满带宽」,中间隔着的全是 cache 跟内存布局的细节。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="8-手怪--simd"&gt;&lt;a href="#8-%e6%89%8b%e6%80%aa--simd" class="header-anchor"&gt;&lt;/a&gt;8 手怪 —— SIMD
&lt;/h2&gt;&lt;p&gt;最后再请一位帮手出场,把分块的故事讲完——&lt;strong&gt;8 手怪&lt;/strong&gt;(SIMD)。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;8 手怪:&amp;ldquo;我有 8 只手,可以一次同时处理 8 个数据。但前提是,这 8 个数据要排得齐齐整整,在内存里挨着。&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;8 手怪正好需要的是&lt;strong&gt;连续的 8 个值&lt;/strong&gt;,而分块算法在一个小方块里恰好提供了这种&amp;quot;齐整&amp;quot;的数据排列。两者一拍即合。&lt;/p&gt;
&lt;p&gt;但 8 手怪有个脾气——&lt;strong&gt;它不和跳读的人合作&lt;/strong&gt;。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;8 手怪:&amp;ldquo;你按列跳着给我数据?那我 8 只手干不起来,我只能 1 只手 1 只手地拿,跟普通工人没区别。&amp;rdquo;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;也就是说,naive 转置不仅 cache 全 miss,SIMD 也用不上。等于把硬件能给你的两个加速器全屏蔽了。&lt;/p&gt;
&lt;p&gt;分块算法解锁了 cache,顺带解锁了 SIMD。从 6 倍加速变成 10 倍加速,就是 8 手怪上场带来的额外提速。&lt;/p&gt;
&lt;p&gt;&lt;img alt="8 手怪示意:一个 8 只手的卡通角色一次同时处理 8 个连续的 int 槽,旁边对比普通工人 1 只手 1 个" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-10-SIMD8%E6%89%8B%E6%80%AA.png"&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="数学-vs-硬件的鸿沟"&gt;&lt;a href="#%e6%95%b0%e5%ad%a6-vs-%e7%a1%ac%e4%bb%b6%e7%9a%84%e9%b8%bf%e6%b2%9f" class="header-anchor"&gt;&lt;/a&gt;数学 vs 硬件的鸿沟
&lt;/h2&gt;&lt;p&gt;至此,整个故事就完了。把这一桌子角色排个队,&lt;strong&gt;我画成了一个图&lt;/strong&gt;(工人-公交车-跑腿小弟-纸条的全家福)。&lt;/p&gt;
&lt;p&gt;&lt;img alt="收束总结图:任性客户 / 一根纸条 / 公交车小哥 / 跑腿小弟 / 干活的工人 / 搬家公司 / 8 手怪,所有角色一字排开,标注各自职责" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://xiaobox-public-images.oss-cn-beijing.aliyuncs.com/images/fig-15-%E5%85%A8%E5%AE%B6%E7%A6%8F-v2.png"&gt;&lt;/p&gt;
&lt;p&gt;回到开头那个面试题——&amp;ldquo;为什么这么写在大矩阵上会慢得离谱?&amp;rdquo;&lt;/p&gt;
&lt;p&gt;现在你可以这么答:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;内存是一根纸条,数据按行优先排列;&lt;/li&gt;
&lt;li&gt;CPU 一次搬一整个 cache line(64 字节),指望你接下来要的东西在隔壁;&lt;/li&gt;
&lt;li&gt;转置算法是按列读,每两次访问跳一整行,&lt;strong&gt;每趟公交车 75% 的座位是空跑&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;Prefetcher 跟不上跳读,SIMD 用不上跳读,工人 95% 的时间在等公交;&lt;/li&gt;
&lt;li&gt;分块算法把大矩阵切成 cache 装得下的小方块,把跳读关在方块内部,&lt;strong&gt;cache、Prefetcher、SIMD 全部解锁&lt;/strong&gt;,10 倍加速。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;你回答的其实不是&amp;quot;转置算法&amp;quot;这一道题,而是&lt;strong&gt;怎么用硬件的语言去翻译数学的意图&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;这事在矩阵转置上是 10 倍差距,在矩阵乘法、卷积、图像处理上是 30 倍甚至 100 倍差距。整个深度学习的底层算子库(BLAS / cuDNN / oneDNN),大半的工程量都在干同一件事——&lt;strong&gt;把数学想法翻译成 cache 友好的内存访问模式&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;数学家觉得免费的,硬件不这么看。&lt;/p&gt;
&lt;p&gt;下次你在 PyTorch 里看到一个 &lt;code&gt;tensor.contiguous()&lt;/code&gt; 调用,别随便划过去了。那一行代码,背后是公交车小哥、跑腿小弟、八手怪、搬家公司一整套班子重新上岗。&lt;code&gt;tensor.contiguous()&lt;/code&gt; 是在告诉底层:&amp;ldquo;这个 tensor 本来是某个 view 的跨步 stride,请重新拷贝一份让它在内存上连续,下游算子才跑得动。&amp;rdquo;&lt;/p&gt;
&lt;p&gt;不这么做,一个本来三秒的 forward 可能要跑三十秒。你的 GPU 在哭,你还不知道。&lt;/p&gt;
&lt;p&gt;同样的,你在 pandas 里跳着抽行、跳着修改列,也是同一回事。你在 OpenCV 里对 BGR 图像逐像素跳访问 channel,也是同一回事。你在 SQL 里对列存储(Parquet / ClickHouse)跳着 SELECT 不同列,还是同一回事。&lt;/p&gt;</description></item></channel></rss>