Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 系统与高性能 / Systems & Performance

计算系统 / Computing Systems

1. 计算系统:计算机如何执行与加速程序

2. 从 C++ 源码到 CPU 执行

3. CPU 流水线、乱序执行与分支预测

4. Cache、一致性、伪共享与 NUMA

5. GPU、SM、Warp 与显存

6. 计算执行模型:程序怎样映射到机器

7. SIMD 与编译器向量化

8. C++ 多线程与 OpenMP

9. CUDA 平台与编程模型

10. CUDA Kernel、内存与性能

11. CPU-GPU 异构流水线

12. MPI 与分布式并行

13. 并行算法模式

14. 性能模型与工具

15. 递进学习项目:从单线程到集群

历史完整正文 / Original Deep Dives

1. 历史完整正文:统一前文章逐篇保留

原体系结构与硬件 / Original Architecture

1. 硬件编程与高性能计算:一张可走通的学习地图 / A Practical Learning Map for Hardware Programming and HPC

2. 计算机体系结构:CPU、内存与 GPU / Computer Architecture: CPUs, Memory, and GPUs

3. 计算机架构基础——为什么 GPU 比 CPU 更快 / Computer Architecture Fundamentals: Why GPUs Outperform CPUs

4. 并行计算理论——30 天训练能优化到多快? / Parallel Computing Theory and the Limits of Training Acceleration

5. GPU 架构深入——上万个核心如何分工协作 / GPU Architecture and Massive Parallel Execution

6. CUDA 编程模型——把矩阵乘法映射到 GPU / The CUDA Programming Model for Mapping Matrix Multiplication to GPUs

7. CUDA 内存管理——百亿参数如何装进显存 / CUDA Memory Management for Large Models

8. CUDA 性能优化——从 30 天缩短到 10 天 / CUDA Performance Optimization

9. CPU 并行编程——OpenMP 与 SIMD 向量化 / CPU Parallel Programming with OpenMP and SIMD

10. HPC 集群与 MPI——多节点分布式训练 / HPC Clusters and MPI for Distributed Training

11. 异构计算——CPU 与 GPU 如何协同工作 / Heterogeneous Computing with CPUs and GPUs

12. 深度学习训练优化实战——从 30 天到 3 天 / Deep Learning Training Optimization from Thirty Days to Three

13. 性能分析工具——找到真正的瓶颈 / Performance Analysis Tools for Finding Real Bottlenecks

14. NPU 全景——昇腾/寒武纪/TPU/苹果生态 / The NPU Landscape: Ascend, Cambricon, TPU, and Apple

15. 未来趋势——2030 年的计算机会是什么形态 / Future Computing Trends Toward 2030

16. 硬件与高性能计算:从“程序为什么慢”开始 / Hardware and HPC Starting from Why Programs Are Slow

原并行计算 / Original Parallel Computing

1. 并行计算:从 SIMD 到 MPI / Parallel Computing from SIMD to MPI

2. 并行计算全景:从晶体管、CPU、GPU 到计算集群 / Parallel Computing from Transistors, CPUs, and GPUs to Clusters

3. 并行计算基础:任务分解、加速比与可扩展性 / Parallel Computing Fundamentals: Decomposition, Speedup, and Scalability

4. 处理器体系结构:从指令流水线到多核芯片 / Processor Architecture from Instruction Pipelines to Multicore Chips

5. CPU 并行:多线程、SIMD、Cache 一致性与 NUMA / CPU Parallelism with Threads, SIMD, Cache Coherence, and NUMA

6. 内存层次:Cache、带宽、局部性与一致性 / Memory Hierarchies, Bandwidth, Locality, and Coherence

7. GPU 体系结构:SIMT、Warp、SM 与吞吐优先设计 / GPU Architecture with SIMT, Warps, and Streaming Multiprocessors

8. CUDA 编程模型:Thread、Block、Grid 与内存协作 / CUDA Threads, Blocks, Grids, and Cooperative Memory Access

9. 并行算法模式:Map、Reduce、Scan、Stencil 与任务图 / Parallel Patterns: Map, Reduce, Scan, Stencil, and Task Graphs

10. 异构计算:CPU、GPU、NPU 如何协同工作 / Heterogeneous Computing with CPUs, GPUs, and NPUs

11. 分布式并行:MPI、集合通信、RDMA 与多机多卡 / Distributed Parallelism with MPI, Collective Communication, and RDMA

12. 性能工程:测量、Roofline、瓶颈定位与优化闭环 / Performance Engineering with Measurement, Roofline, and Bottleneck Analysis

13. 并行计算实战:AI、CAE、图像与科学计算 / Parallel Computing for AI, CAE, Imaging, and Scientific Computing

14. 并行计算实践路线:从单核优化到多机多卡 / A Parallel Computing Project Path from Single-Core to Multi-Node GPUs

本页目录

Cache、一致性、伪共享与 NUMA ​

CPU 快,内存为什么仍是瓶颈 ​

计算单元每周期能处理多项工作,而访问 DRAM 需要远得多的时间。Cache 保存近期或邻近数据,用容量换取低延迟:

text
registers -> L1 -> L2 -> shared/last-level cache -> DRAM -> storage
 fastest      small                              large, slow
1
2

具体层数、共享方式和延迟由处理器决定;不要背一组纳秒数字当作标准。

Cache line 与局部性 ​

Cache 通常按固定大小的 line 搬运数据,而不是只取一个 int:

text
array: [0][1][2][3][4][5][6][7] ...
        +------ one or more cache lines ------+
1
2
  • 时间局部性:刚访问的数据可能很快再访问;
  • 空间局部性:邻近数据可能很快被访问;
  • 连续遍历通常比随机指针追逐更容易利用 Cache 和预取;
  • AoS 与 SoA 的选择取决于每次计算实际需要哪些字段。

多核为什么需要一致性 ​

多个核心可能各自缓存同一物理地址。Cache coherence 保证对单个位置的写入以体系结构规定的方式传播和排序。MESI 是经典的四状态解释模型:Modified、Exclusive、Shared、Invalid。

MESI 不是所有 CPU 的唯一协议,也不能替代 C++ 内存模型。硬件一致性不意味着没有 data race;无同步的冲突访问在 C++ 中仍可能是未定义行为。

伪共享 ​

cpp
struct Counters {
    std::atomic<long long> left{0};
    std::atomic<long long> right{0};
};
1
2
3
4

两个线程修改不同变量,看似不共享逻辑数据;若变量落在同一 Cache line,所有权仍可能在核心间来回迁移:

text
Core 0 writes left  <---- cache line ping-pong ----> Core 1 writes right
1

修复必须以测量和目标平台为依据,可考虑分线程局部计数、批量归并,或使用 std::hardware_destructive_interference_size 作为实现提供的提示。盲目填充会浪费空间并改变局部性。

NUMA:内存也有距离 ​

多插槽或多 NUMA node 系统中,核心访问本地内存通常比远端内存更便宜:

text
NUMA node 0                         NUMA node 1
CPU cores -- local memory   link   CPU cores -- local memory
       \____________________remote_____________________/
1
2
3

first-touch 策略、线程迁移、内存绑定和跨 node 共享会影响带宽与延迟。NUMA 优化前应先确认机器拓扑和工作集确实足够大。

一个访问模式实验 ​

cpp
long long sum(const std::vector<int>& values) {
    long long result = 0;
    for (int value : values) {
        result += value;
    }
    return result;
}
1
2
3
4
5
6
7

顺序数组适合硬件预取和向量化。若改成随机索引,应分别测量工作集能否容纳于各级 Cache;不要把一次结果直接归因于“大 O 复杂度”。

常见错误 ​

  1. 把 Cache coherence 当作线程同步;
  2. 认为原子变量一定没有性能代价;
  3. 只按对象大小,不按访问字段设计布局;
  4. 未确认 NUMA 拓扑就绑定线程;
  5. 用平均延迟数字替代实际 Profiling。

深入原理与工程实践 ​

前面的内容负责建立统一心智模型;下面把同一主题继续拆到执行过程、代码、性能代价与工程判断。

<!-- migrated-deep-dive:start -->

完整迁入:原体系结构基础中的 Cache、内存与 NUMA ​

计算机架构基础——为什么 GPU 比 CPU 更快 / Computer Architecture Fundamentals: Why GPUs Outperform CPUs ​

📅 创建时间:2026-06-02 🏷️ 标签:#Hardware #CPU #Cache #内存层次 #NUMA #SIMD 📚 前置知识:[[00-hardware-overview]](硬件全景 + 工具链) 📚 相关知识:[[03-gpu-architecture]](GPU 与 CPU 的对比) [[02-parallel-computing-theory]](并行理论)


先抓住直觉 ​

CPU 像一位反应很快、能处理复杂订单的主厨;GPU 像一间有大量工位、适合重复流水作业的厨房。真正决定速度的不只有“厨师算得多快”,还包括食材是否就在手边。

  • 必须理解:CPU 与 GPU 的设计取舍;延迟与带宽;数据局部性。
  • 用到再查:具体 Cache 容量、纳秒数、NUMA API 和 SIMD 指令宽度。
  • 读完能回答:为什么连续遍历数组通常比随机访问快?为什么“GPU 核心更多”不等于任何程序都更快?

场景:为什么训练 AI 模型用的是 GPU 而不是 CPU? ​

┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  凌晨 2 点,你盯着训练日志发呆。                             │
│                                                             │
│  模型:A100 单卡,175B 参数                                 │
│  当前速度:每秒处理 1500 个 Token                           │
│  预计总训练时间:30 天                                      │
│                                                             │
│  你的同事在用同样的模型、同样的数据,却只需要 3 天。        │
│  区别:他用 8 卡并行训练                                    │
│                                                             │
│  你开始思考一个问题:                                       │
│  为什么 AI 训练用 GPU 而不是 CPU?                          │
│  多卡并行为什么能加速?理论极限在哪里?                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

这一章,我们从最底层出发,理解计算机的硬件结构,为后续理解 GPU 打下基础。


第1节:CPU 的设计哲学——单核要足够快 ​

CPU 是什么? ​

CPU(Central Processing Unit,中央处理器)是计算机的大脑。它的设计哲学是:单个任务要执行得足够快。

打开任务管理器,你会看到 CPU 的这些指标:

  • 频率:3.5 GHz(每秒 35 亿个时钟周期)
  • 核心数:8 核(现代桌面 CPU)
  • 线程数:16 线程(超线程技术)
CPU 的架构解剖 ​
┌─────────────────────────────────────────────────────────────┐
│                      CPU 核心内部结构                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────┐                                               │
│   │  Fetch  │ ← 取指:从内存取下一条指令                    │
│   └────┬────┘                                               │
│        ↓                                                    │
│   ┌─────────┐                                               │
│   │ Decode  │ ← 译码:解析这条指令要做什么                  │
│   └────┬────┘                                               │
│        ↓                                                    │
│   ┌─────────┐                                               │
│   │  ALU    │ ← 执行:算术逻辑运算(加减乘除、与或非)       │
│   └────┬────┘                                               │
│        ↓                                                    │
│   ┌─────────┐                                               │
│   │  Write  │ ← 写回:把结果写回寄存器                      │
│   └─────────┘                                               │
│                                                             │
│   ┌─────────┐                                               │
│   │Registers│ ← 寄存器:最快的存储,速度≈ CPU 同频           │
│   │ (64个) │  每个寄存器 64 位(8 字节)                   │
│   └─────────┘                                               │
│                                                             │
│   ┌─────────┐                                               │
│   │  L1 Cache│ ← 一级缓存:~32KB,极快,约 1ns              │
│   └────┬────┘                                               │
│        ↓                                                    │
│   ┌─────────┐                                               │
│   │  L2 Cache│ ← 二级缓存:~256KB,快,约 3ns                │
│   └────┬────┘                                               │
│        ↓                                                    │
│   ┌─────────┐                                               │
│   │  L3 Cache│ ← 三级缓存:~32MB,较慢,所有核心共享,约 10ns │
│   └────┬────┘                                               │
│        ↓                                                    │
│   ┌─────────┐                                               │
│   │  Memory  │ ← 主内存(DDR):~64GB,慢,约 100ns          │
│   └─────────┘                                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
C++ 程序员的视角 ​

当你写一个 C++ 循环时:

cpp
int sum = 0;
for (int i = 0; i < 1000000; i++) {
    sum += data[i];  // 每次访问 data[i],数据可能不在寄存器里
}
1
2
3
4

CPU 在背后做的事情:

第1次访问 data[0]:
  → 寄存器里没有 → 查 L1 Cache,没有
  → 查 L2 Cache,没有 → 查 L3 Cache,没有
  → 终于从主内存读出来了(100ns!)

第2次访问 data[1]:
  → 如果 data[1] 紧挨着 data[0],它在 L1 Cache 里(1ns!)

这就是 Cache 的意义:利用**数据局部性**(Spatial Locality),
把刚用过的数据和它附近的数据一起预取到 Cache 里。
1
2
3
4
5
6
7
8
9
10

第2节:内存层次——速度与容量的 trade-off ​

内存金字塔 ​
┌─────────────────────────────────────────────────────────────┐
│                      内存层次金字塔                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                          CPU 寄存器                          │
│                       [ 64个 × 8B ]                         │
│                        ≈ 0.5 KB                            │
│                      访问延迟: ~0.1ns                       │
│                     带宽: ∞(CPU 同频)                     │
│                              ↑                               │
│                           L1 Cache                          │
│                          ≈ 32-64 KB                         │
│                      访问延迟: ~1ns                         │
│                     带宽: ~1 TB/s                           │
│                              ↑                               │
│                           L2 Cache                          │
│                         ≈ 256 KB                            │
│                      访问延迟: ~3ns                         │
│                     带宽: ~500 GB/s                         │
│                              ↑                               │
│                           L3 Cache                          │
│                         ≈ 16-64 MB                         │
│                      访问延迟: ~10ns                        │
│                     带宽: ~200 GB/s                         │
│                              ↑                               │
│                          主内存 DDR4                          │
│                         ≈ 32-256 GB                         │
│                      访问延迟: ~100ns                       │
│                     带宽: ~50 GB/s                          │
│                              ↑                               │
│                        NVMe SSD                             │
│                       ≈ 1-8 TB                            │
│                   访问延迟: ~100,000ns (0.1ms)             │
│                      带宽: ~5 GB/s                         │
│                              ↑                               │
│                        机械硬盘                             │
│                       ≈ 10-20 TB                           │
│                 访问延迟: ~10,000,000ns (10ms)             │
│                      带宽: ~200 MB/s                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
关键指标:延迟 vs 带宽 ​
延迟(Latency):做一次操作需要多久
  - L1 Cache:1ns = 你眨眼速度的 3 亿倍
  - 主内存:100ns = L1 的 100 倍
  - NVMe SSD:100,000ns = L1 的 10 万倍

带宽(Bandwidth):单位时间内能传输多少数据
  - L1 Cache:~1 TB/s
  - 主内存:~50 GB/s(DDR4-3200)
  - A100 GPU HBM2e:~2 TB/s(比 CPU 内存快 40 倍!)

重要结论:
CPU 设计针对**低延迟**优化(分支预测、Cache 预取)。
GPU 设计针对**高带宽**优化(大量并行数据访问)。
1
2
3
4
5
6
7
8
9
10
11
12
13
Cache Line:内存访问的基本单位 ​

Cache 不是按字节传输的,而是按 Cache Line(缓存行)为单位传输。

Cache Line 大小 = 64 字节(现代 CPU 的标准)

假设你访问一个 int(4 字节):
data[0] = 0x1000 地址

CPU 实际做的是:
  → 把地址 0x1000 ~ 0x103F(64字节)全部读入 L1 Cache
  → 然后只取你要的 4 字节

这意味着:
访问 data[0] 后,data[1] ~ data[15] 也在 Cache 里了(如果它们连续)
这就是**顺序访问比随机访问快很多**的原因。
1
2
3
4
5
6
7
8
9
10
11
12
C++ 实战:如何让代码利用 Cache 局部性 ​
cpp
// ❌ 慢:Cache 不友好(跳跃式访问)
// stride = 4096 意味着每次跳过一个 Cache Line
// 每次访问都会触发从主内存加载(100ns)
for (int i = 0; i < 1000000; i += 4096) {
    sum += data[i];  // Cache 永远 miss
}

// ✅ 快:Cache 友好(顺序访问)
// data[0], data[1], data[2]... 都在同一个 Cache Line 里
// 只有每 16 个数才需要加载新的 Cache Line
for (int i = 0; i < 1000000; i++) {
    sum += data[i];  // 大部分命中 L1 Cache
}
1
2
3
4
5
6
7
8
9
10
11
12
13

第3节:为什么 CPU 不适合 AI 训练? ​

CPU 的串行瓶颈 ​

AI 训练的核心运算是矩阵乘法:

矩阵乘法:Y = W × X
          W: 4096 × 4096(权重矩阵)
          X: 4096 × 1(输入向量)
          Y: 4096 × 1(输出向量)

CPU 怎么做:
  for (int i = 0; i < 4096; i++) {      // 外层循环:4096 次
      for (int j = 0; j < 4096; j++) {  // 内层循环:4096 × 4096 = 1600万次
          Y[i] += W[i][j] * X[j];
      }
  }

一个 CPU 核心每次只能做一次乘加运算。
4096 × 4096 = 1600 万次乘加运算,
在 3.5 GHz 的 CPU 上,假设每个运算 1 个周期,需要 4.5ms。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CPU 的多核并行 ​

现代 CPU 有多核:

┌─────────────────────────────────────────────────────────────┐
│                     16 核 CPU 架构                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│     ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│     │Core│ │Core│ │Core│ │Core│ │Core│ │Core│ │Core│ │Core│
│     │ 0  │ │ 1  │ │ 2  │ │ 3  │ │ 4  │ │ 5  │ │ 6  │ │ 7  │
│     └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘ └────┘
│          │          │          │          │
│       L2 Cache  L2 Cache   L2 Cache   L2 Cache
│          └──────────┴──────────┴──────────┘
│                        ↑
│                    共享 L3 Cache
│                        │
│                   ┌────┴────┐
│                   │  内存控制器  │
│                   └────┬────┘
│                        │
│                     DDR4 内存
│                                                             │
└─────────────────────────────────────────────────────────────┘

所有核心共享同一条通往内存的通道(总线带宽有限)。
即使 16 核同时跑,内存带宽也是瓶颈。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CPU 的 SIMD:有限的向量化 ​

CPU 有 SIMD(Single Instruction Multiple Data,单指令多数据)单元:

cpp
// 普通 C++(标量):一次处理一对数
float a = x[i];
float b = y[i];
float c = a + b;

// SIMD(向量化):一次处理 8 对数(AVX-512 = 512位 = 16个float)
// 需要编译器支持,或者手动用 intrinsics
#include <immintrin.h>

__m512 va = _mm512_loadu_ps(&x[i]);   // 一次加载 16 个 float
__m512 vb = _mm512_loadu_ps(&y[i]);
__m512 vc = _mm512_add_ps(va, vb);     // 一次加 16 对
_mm512_storeu_ps(&z[i], vc);           // 一次存储 16 个结果
1
2
3
4
5
6
7
8
9
10
11
12
13

CPU 的 SIMD 限制:

  • 最多同时处理 16 个 float(AVX-512)
  • 而且 SIMD 单元数量有限,不是所有运算都能用上
  • 16 个并行 vs GPU 的 1024 个并行,差距是 64 倍

第4节:NUMA——多路服务器的特殊性 ​

为什么你的服务器有多个插槽? ​

高性能服务器通常有多个 CPU 插槽:

┌─────────────────────────────────────────────────────────────┐
│                   双路服务器架构(NUMA)                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌────────────────┐        ┌────────────────┐            │
│   │   CPU Socket 0 │        │   CPU Socket 1 │            │
│   │   (8 核心)    │        │   (8 核心)    │            │
│   └───────┬────────┘        └───────┬────────┘            │
│           │                            │                     │
│      L3 Cache                      L3 Cache                   │
│           │                            │                     │
│   ┌───────┴────────┐        ┌───────┴────────┐            │
│   │   DDR4 通道1    │        │   DDR4 通道2    │            │
│   │   (本地内存)    │        │   (本地内存)    │            │
│   │    128 GB      │        │    128 GB      │            │
│   └────────────────┘        └────────────────┘            │
│           │                            │                     │
│           └──────────┬────────────────┘                     │
│                      │                                      │
│                QPI/UPI 总线(跨插槽访问延迟高 2-3 倍)       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NUMA 的问题 ​
CPU Socket 0 访问本地内存:延迟 ~100ns
CPU Socket 0 访问 Socket 1 的内存:延迟 ~200-300ns(通过 QPI 总线)

如果你的程序没有考虑 NUMA:
  → 数据可能在 Socket 1 的内存里
  → 但 Socket 0 的核心在计算
  → 每次内存访问都要跨总线(慢 2-3 倍)
1
2
3
4
5
6
7
NUMA 感知编程(C++ 示例) ​
cpp
#include <numa.h>
#include <numaif.h>

int main() {
    numa_set_strict(1);  // 强制 NUMA 策略

    // 在 CPU Socket 0 上分配内存
    void* local_mem = numa_alloc_onnode(1024 * 1024 * 1024, 0);

    // 在 CPU Socket 0 上绑定线程
    struct bitmask* mask = numa_allocate_nodemask();
    numa_bitmask_setbit(mask, 0);  // 只用 Socket 0
    numa_bind(mask);

    // 之后的 malloc 都会在 Socket 0 上分配
    double* data = (double*)malloc(1024 * 1024 * 1024 * sizeof(double));

    // 或者用 OpenMP 指定 NUMA 策略
    #pragma omp parallel
    {
        int node = numa_node_of_cpu(omp_get_thread_num());
        // 每个线程绑定到它所在 NUMA 节点的 CPU
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

第5节:从 CPU 到 AI 芯片——为什么 GPU 天生适合矩阵运算 ​

矩阵乘法的本质 ​

回到 4096 × 4096 的矩阵乘法:

Y[i][j] = Σ W[i][k] × X[k][j](k 从 0 到 4095)

展开看:
Y[i][j] = W[i][0]×X[0][j] + W[i][1]×X[1][j] + ... + W[i][4095]×X[4095][j]

每个 Y[i][j] 的计算完全独立!
4096 × 4096 = 1600 万个输出元素,每个都可以**并行计算**。
1
2
3
4
5
6
7
CPU vs GPU 的核心差异 ​
┌─────────────────────────────────────────────────────────────┐
│                  CPU vs GPU:设计哲学差异                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  CPU:让一个任务执行得极快                                   │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                                                      │  │
│  │   [ALU][ALU][ALU][ALU]  ← 4-8 个算术单元            │  │
│  │         ↑                                            │  │
│  │   [大缓存] [复杂分支预测] [乱序执行] [超线程]         │  │
│  │         ↑                                            │  │
│  │   [通用控制逻辑](能处理任何类型的计算)             │  │
│  │                                                      │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
│  GPU:同时执行大量简单任务                                   │
│  ┌──────────────────────────────────────────────────────┐  │
│  │                                                      │  │
│  │   [ALU][ALU][ALU][ALU]...[ALU][ALU][ALU][ALU]       │  │
│  │   [ALU][ALU][ALU][ALU]...[ALU][ALU][ALU][ALU]       │  │
│  │   [ALU][ALU][ALU][ALU]...[ALU][ALU][ALU][ALU]       │  │
│  │          ↑                                            │  │
│  │   (几千个算术单元!)                                │  │
│  │          │                                            │  │
│  │   [小缓存] [简单控制逻辑] [专为矩阵运算优化]          │  │
│  │                                                      │  │
│  └──────────────────────────────────────────────────────┘  │
│                                                             │
│  总结:                                                     │
│  CPU:单核极快,但核心少(8-64核)                         │
│  GPU:单核简单,但核心极多(上千核)                        │
│                                                             │
│  矩阵乘法:1600 万个独立计算,正好用 GPU 的海量核心!        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
数字对比 ​
┌─────────────────────────────────────────────────────────────┐
│                    A100 vs 高端 CPU 对比                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  指标            │  A100 GPU        │  AMD EPYC 7763 (CPU)│
│  ────────────────┼──────────────────┼─────────────────────┤
│  核心数          │  6912 个 CUDA 核  │  64 核              │
│  峰值算力(FP32)  │  19.5 TFLOPS     │  ~1 TFLOPS          │
│  内存带宽        │  2.0 TB/s (HBM2e)│  ~200 GB/s          │
│  内存容量        │  40/80 GB        │  256 GB             │
│  功耗            │  400W            │  280W               │
│                                                             │
│  矩阵乘法性能差距:约 20-50 倍                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这就是为什么 AI 训练用 GPU 而不是 CPU:当你要做大量独立的简单计算时,GPU 的海量简单核心远胜 CPU 的少量复杂核心。


升华:这一章解决了什么问题? ​

┌─────────────────────────────────────────────────────────────┐
│              本章核心知识点串联                               │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  CPU 的设计哲学:单核极快 → 但核心少 → 矩阵乘法慢            │
│                                                             │
│  内存层次:L1 Cache(1ns) → L2(3ns) → L3(10ns) → 内存(100ns)│
│           → 理解 Cache Line 和局部性是性能优化的基础          │
│                                                             │
│  NUMA:多路服务器的内存访问延迟差异 → NUMA 感知编程重要性    │
│                                                             │
│  GPU 天生适合 AI:海量简单核心 + 高带宽内存                  │
│  → 这就是为什么大模型训练必须用 GPU                          │
│                                                             │
│  下一章预告:GPU 的理论加速比是多少?                        │
│  → 阿姆达尔定律告诉你答案                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

"AI 可查 vs 必须理解"清单 ​

AI 可查:
✅ L1/L2/L3 Cache 的大小和延迟的具体数值——各型号不同,文档里查
✅ AVX-512 intrinsics 的具体函数名——_mm512_add_ps 等,文档里有
✅ NUMA API 的具体函数——numa_alloc_onnode 等,头文件里查
✅ CPU 各型号的具体参数——核心数/频率/功耗,官网有

必须理解:
🔴 Cache Line = 64 字节,顺序访问比随机快
🔴 延迟:L1(1ns) < L2(3ns) < L3(10ns) < Memory(100ns) < SSD(100us)
🔴 CPU 适合低延迟串行任务,GPU 适合高带宽并行任务
🔴 矩阵乘法的 1600 万个独立计算正好适合 GPU 的海量核心
🔴 NUMA:跨插槽访问内存延迟高 2-3 倍
1
2
3
4
5
6
7
8
9
10
11
12

学习状态:🟡 开始学习


完整迁入:原内存层次专题 ​

内存层次:Cache、带宽、局部性与一致性 / Memory Hierarchies, Bandwidth, Locality, and Coherence ​

📅 创建时间:2026-07-20 🏷️ 标签:#内存层次 #Cache #带宽 #局部性 #一致性 📚 前置知识:[[03-cpu-parallelism]] 📚 相关知识:[[/02-systems-and-performance/02-computer-architecture-and-hardware/01-computer-architecture-basics]] [[/02-systems-and-performance/02-computer-architecture-and-hardware/05-cuda-kernel-and-memory]]


1. 为什么数据移动决定性能 ​

计算单元速度远高于主存。处理器可以在等待一次内存访问期间执行许多算术操作。

典型层次如下:

text
寄存器      最快、最小、线程私有
L1 Cache    很快、每核或局部共享
L2 Cache    较大、延迟更高
L3 Cache    多核共享、容量更大
主存 DRAM   容量大、延迟高
SSD/网络    更慢,但容量或范围更大
1
2
3
4
5
6

从上往下,容量增加,访问速度下降。


2. 时间局部性与空间局部性 ​

时间局部性 ​

最近访问过的数据很可能再次访问。

cpp
for (int k = 0; k < 100; ++k)
    sum += value; // value 被反复使用
1
2
空间局部性 ​

访问某个地址后,很可能继续访问附近地址。

cpp
for (int i = 0; i < n; ++i)
    sum += a[i]; // 连续访问
1
2

Cache 通常以 Cache Line 为单位搬运,例如 64 字节。连续访问能够充分利用一次搬运的数据。


3. 行优先矩阵的访问顺序 ​

C/C++ 二维数组按行连续存储:

cpp
// 友好:连续访问
for (int i = 0; i < n; ++i)
    for (int j = 0; j < n; ++j)
        sum += a[i][j];

// 不友好:跨行跳跃
for (int j = 0; j < n; ++j)
    for (int i = 0; i < n; ++i)
        sum += a[i][j];
1
2
3
4
5
6
7
8
9

两段代码计算量相同,但第二段可能产生更多 Cache Miss。


4. 分块提高数据复用 ​

矩阵乘法直接访问大矩阵时,数据可能在再次使用前已被逐出 Cache。分块把问题切成能放入 Cache 的小块:

text
大矩阵
┌────┬────┬────┐
│块00│块01│块02│
├────┼────┼────┤
│块10│块11│块12│
└────┴────┴────┘
1
2
3
4
5
6

GPU 的 Shared Memory 分块和 CPU 的 Cache Blocking,本质都是主动提高局部性。


5. 延迟与带宽 ​

  • 延迟:一次访问从发出到返回需要多久
  • 带宽:单位时间最多能搬运多少数据

链表随机访问常受延迟限制,向量连续扫描更容易接近带宽上限。

算术强度 ​
text
算术强度 = 浮点运算次数 / 内存传输字节数
1

向量加法读取两个数组并写入一个数组,每个元素只做一次加法,算术强度很低。矩阵乘法可以重复使用数据,算术强度较高。


6. Cache 一致性 ​

多个 CPU 核心可能缓存同一个内存地址。如果一个核心修改数据,其他核心的旧副本必须失效或更新。

一致性协议保证最终看到正确数据,但会产生:

  • Cache Line 在核心间转移
  • 共享写入导致一致性流量
  • 原子变量成为热点
  • 伪共享导致无意义失效

因此“共享内存编程方便”不代表共享数据是免费的。


7. 内存一致性模型 ​

编译器和 CPU 为提高性能可能重排指令。多线程程序不能只根据源代码顺序推断其他线程观察到的顺序。

C++ 使用原子操作和内存序建立线程间关系:

cpp
data = 42;
ready.store(true, std::memory_order_release);

if (ready.load(std::memory_order_acquire)) {
    use(data); // acquire/release 建立可见性关系
}
1
2
3
4
5
6

除非确实在实现底层并发结构,否则优先使用 Mutex、并发容器和成熟库,避免自行组合复杂内存序。


8. 内存优化顺序 ​

  1. 减少不必要的数据和拷贝。
  2. 让访问尽量连续。
  3. 选择紧凑的数据布局。
  4. 通过分块增加复用。
  5. 减少跨线程共享写入。
  6. 检查 NUMA 和设备间传输。
  7. 使用硬件计数器验证 Cache Miss 和带宽。

核心总结 ​

  • 现代并行计算经常受数据移动而不是计算能力限制。
  • 局部性决定 Cache 的有效程度。
  • 分块是 CPU、GPU 和科学计算中的通用优化思想。
  • 多核共享数据会触发一致性成本。
  • 必须同时分析延迟、带宽和算术强度。

下一篇:[[05-gpu-architecture]] <!-- migrated-deep-dive:end -->

面试速答 ​

什么是伪共享? ​

线程修改不同变量,但变量位于同一 Cache line,导致一致性所有权频繁迁移。它不破坏正确性,却可能严重降低扩展性。

Cache 一致性与 C++ 内存模型是什么关系? ​

一致性是硬件维护缓存副本的机制;C++ 内存模型规定程序中的原子、同步和 data race 语义。硬件有一致性不能使有 data race 的 C++ 程序合法。

自测与答案 ​

  1. 连续数组为什么通常比链表遍历快? **答案:**更好的空间局部性、预取、较少指针依赖,并更容易向量化。
  2. 两个线程写不同变量一定互不影响吗? **答案:**不一定,变量可能共享 Cache line 或其他硬件资源。
  3. NUMA 的“远端”是什么意思? **答案:**内存物理连接到另一个 node,访问要经过互连,通常具有更高延迟或更低有效带宽。

下一篇:GPU、SM、Warp 与显存

最后更新于:

Pager
上一篇3. CPU 流水线、乱序执行与分支预测
下一篇5. GPU、SM、Warp 与显存

持续记录,持续成长

Copyright © Tidenflow