Skip to content

Commit 4e59689

Browse files
committed
docs: 新增 MPI/GPU/I/O/亲和 NUMA 四个深度专题,补齐 SIMD 与 LTO 说明
- 新增 deep-dives:mpi-distributed(SPMD/死锁/集合通信/非阻塞重叠/ 域分解自验证/手动计时基准方法论)、gpu-offloading(路线图文档, CUDA/SYCL/HIP 对比与何时上 GPU 的判断标准)、io-performance (read/pread/mmap 对照与写缓冲 1300 倍差距实测)、 thread-affinity-numa(绑核三重校验/首触策略/依赖门控说明) - simd-internals 补 ARM NEON 小节(与 x86 概念对照表、gather 缺失 说明)、可移植 SIMD 方向(std::experimental::simd / C++26)与 Dispatch 基准说明;cmake-build-system 兑现 LTO 集成现状 - 侧边栏新增四个专题;getting-started 仓库结构与下一步链接同步; 首页与根 README 更新模块/主题/可选依赖清单 - 仓库源码引用沿用现有惯例(纯文本路径而非站点链接), vitepress build 与 docs 回归测试(7/7)全部通过
1 parent de48505 commit 4e59689

10 files changed

Lines changed: 358 additions & 15 deletions

File tree

README.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626

2727
- 现代 CMake 与 preset 驱动的构建流程
2828
- 内存与缓存布局取舍
29-
- 现代 C++ 的性能实践
30-
- SIMD 与向量化
31-
- 并发与无锁基础
29+
- 现代 C++ 的性能实践(含并行 STL 执行策略)
30+
- SIMD 与向量化(SSE2/AVX2/AVX-512/NEON)
31+
- 并发与无锁基础、线程亲和与 NUMA
32+
- 分布式内存与 MPI(可选模块)
33+
- 文件 I/O 性能(read/pread/mmap 与写缓冲)
3234
- 基于 benchmark 与 profiling 的性能分析
3335

3436
每个主题都尽量做到 **可阅读、可构建、可测量**
@@ -41,7 +43,9 @@
4143
| `examples/02-memory-cache/` | AOS vs SOA、伪共享、对齐、预取 |
4244
| `examples/03-modern-cpp/` | constexpr、移动语义、reserve、ranges |
4345
| `examples/04-simd-vectorization/` | 自动向量化、intrinsics、SIMD 封装 |
44-
| `examples/05-concurrency/` | 原子操作、无锁队列、OpenMP |
46+
| `examples/05-concurrency/` | 原子操作、无锁队列、OpenMP、线程亲和、NUMA |
47+
| `examples/06-distributed-mpi/` | MPI 点对点、集合通信、域分解(可选:`-DHPC_ENABLE_MPI=ON`|
48+
| `examples/07-io-performance/` | read/pread/mmap 读路径对照、写缓冲开销(Linux) |
4549
| `docs/` | VitePress 文档站,覆盖深度专题 / 实战指南 / API 参考 |
4650

4751
## 快速开始
@@ -82,7 +86,8 @@ cmake --preset=ubsan && cmake --build build/ubsan && ctest --preset=ubsan
8286
- **语言:** C++20
8387
- **构建:** CMake 3.20+、Ninja
8488
- **测试:** Google Test、RapidCheck
85-
- **基准测试:** Google Benchmark
89+
- **基准测试:** Google Benchmark(MPI 通信基准为手动计时,避免集合通信死锁)
90+
- **可选依赖:** MPI(06 模块)、Intel TBB(并行 STL par 策略)、libnuma(NUMA 示例)
8691
- **文档:** VitePress + GitHub Pages
8792
- **性能分析:** perf、FlameGraph、Valgrind、VTune
8893

docs/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ export default defineConfig({
6868
items: [
6969
{ text: '内存布局与缓存', link: '/zh/deep-dives/memory-layout' },
7070
{ text: 'SIMD 向量化', link: '/zh/deep-dives/simd-internals' },
71+
{ text: 'MPI 分布式并行', link: '/zh/deep-dives/mpi-distributed' },
7172
{ text: '无锁并发', link: '/zh/deep-dives/lock-free-queue' },
73+
{ text: '线程亲和与 NUMA', link: '/zh/deep-dives/thread-affinity-numa' },
74+
{ text: 'I/O 性能', link: '/zh/deep-dives/io-performance' },
75+
{ text: 'GPU 卸载加速', link: '/zh/deep-dives/gpu-offloading' },
7276
{ text: '现代 CMake 构建', link: '/zh/deep-dives/cmake-build-system' },
7377
{ text: 'C++20 性能实践', link: '/zh/deep-dives/modern-cpp-perf' },
7478
],

docs/zh/deep-dives/cmake-build-system.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
**编译优化标志。** `-O3` 启用激进优化(循环展开、向量化、内联);`-march=native` 允许编译器使用当前 CPU 的全部指令集(AVX2、FMA);`-ffast-math` 放松浮点语义以换取向量化机会。缺少这些标志的 Release 构建可能只有峰值性能的 20-30%。
88

9-
**链接时优化(LTO)。** 跨翻译单元内联、死代码消除、全局常量传播。没有 LTO,编译器只能在单个 `.cpp` 文件内优化。(注意:本仓库当前尚未集成 LTO,扩展方式见文末 preset 工作流一节。
9+
**链接时优化(LTO)。** 跨翻译单元内联、死代码消除、全局常量传播。没有 LTO,编译器只能在单个 `.cpp` 文件内优化。本仓库提供可选开关 `-DHPC_ENABLE_LTO=ON`(默认关闭),详见文末 preset 工作流一节。
1010

1111
**调试信息与性能分析。** `-g` 生成调试符号,`RelWithDebInfo` 配置在保持 `-O2` 优化的同时保留符号信息,使 perf/VTune 等工具能将热点映射回源码行。
1212

@@ -206,9 +206,12 @@ ctest --preset=tsan
206206
cat build/release/compile_commands.json | head -20
207207
```
208208

209-
> **关于 LTO:** 本仓库目前尚未集成链接时优化(没有 `HPC_ENABLE_LTO` 之类的开关)。
210-
> 如需启用,可在 `cmake/CompilerOptions.cmake` 中为 Release 配置添加 `-flto=auto`
211-
> (GCC/Clang)或设置 `CMAKE_INTERPROCEDURAL_OPTIMIZATION`,然后用
212-
> `cmake --build build/release --verbose 2>&1 | grep -i flto` 验证是否生效。
209+
> **关于 LTO:** 本仓库已集成链接时优化开关(`cmake/CompilerOptions.cmake`):
210+
> `cmake --preset=release -DHPC_ENABLE_LTO=ON` 后,Release/RelWithDebInfo 的
211+
> 全部示例/基准/测试目标统一附加 `-flto=auto`(GCC/Clang,编译与链接两侧)
212+
> `/GL`+`/LTCG`(MSVC)。configure 阶段用 `CheckIPOSupported` 探测,
213+
> 工具链不支持 IPO 时会给出警告而非链接期失败。默认关闭的原因:LTO 显著
214+
> 拖慢链接、放大跨翻译单元的未定义行为暴露面,性能验证场景应按需开启。
215+
> 验证方式:`grep -o "\-flto=auto" build/release/build.ninja | head -1`
213216
214217
构建系统本身也是"可验证的性能工程"的一部分:相同的源码 + 相同的 preset = 相同的二进制行为,任何性能回归都可以定位到具体的代码变更而非构建配置漂移。
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# GPU 卸载加速概览
2+
3+
> 本篇是路线图文档:GPU 工具链(CUDA/SYCL/HIP)重、硬件依赖强,本仓库不提供 GPU 代码示例,这里回答的是"什么时候该上 GPU、选哪条技术路线、怎么和已有的 SIMD/MPI 工作衔接"。
4+
5+
## 什么时候 GPU 才值得
6+
7+
GPU 的优势建立在一个交换上:**用极高的并行度和带宽,换取更贵的控制流与更慢的单线程**。判断标准是三条:
8+
9+
1. **计算密度足够高**:同一份数据要执行大量算术(如矩阵乘、stencil、FFT)。每字节只加一次的流式操作在 GPU 上往往打不过 CPU SIMD + 大页。
10+
2. **批量足够大**:核函数(kernel)要能展开成数千个线程。元素数小于几万时,启动开销(µs 级)会吃掉全部收益。
11+
3. **传输开销可摊销**:CPU↔GPU 走 PCIe/NVLink,PCIe 4.0 x16 实测 ~25 GB/s,比本机内存带宽低一个数量级。**数据必须留在 GPU 上反复使用**,每个算法步骤都来回搬运一定亏。
12+
13+
一句话:GPU 适合"搬进去一次、算很久、搬出来一次"的工作负载。
14+
15+
## 三条技术路线
16+
17+
| 路线 | 抽象层级 | 生态 | 适用 |
18+
|------|----------|------|------|
19+
| CUDA | 线程块/网格显式管理 | 最成熟(cuBLAS/cuDNN/Thrust/CUB) | NVIDIA 专用,性能上限最高 |
20+
| SYCL (oneAPI) | 单源 C++17/20,lambda 即 kernel | 跨厂商(Intel/AMD/NVIDIA 后端) | 想保持 C++ 代码形态、多硬件 |
21+
| HIP | CUDA 的移植方言 | AMD 主场,可回编 NVIDIA | AMD 硬件或双平台 |
22+
| OpenACC/OpenMP offload | pragma 指令式 | 编译器生成 kernel | 遗留 Fortran/C 代码快速卸载 |
23+
24+
对现代 C++ 项目,SYCL 的吸引力在于 kernel 就是 lambda、内存管理有 RAII 风格 accessor;代价是编译链更重、调优手段不如 CUDA 直接。性能关键路径(如 GEMM)通常直接调用厂商库(cuBLAS/oneMKL),自己写的 kernel 只覆盖库不提供的部分。
25+
26+
## 编程模型最小集(以 CUDA 心智模型为例)
27+
28+
```cpp
29+
// 伪代码:向量加法的 kernel 与启动
30+
__global__ void add(const float* a, const float* b, float* c, int n) {
31+
int i = blockIdx.x * blockDim.x + threadIdx.x; // 全局线程编号
32+
if (i < n) c[i] = a[i] + b[i];
33+
}
34+
35+
add<<<(n + 255) / 256, 256>>>(d_a, d_b, d_c, n); // 网格 × 线程块
36+
```
37+
38+
三个必须理解的概念:
39+
40+
- **线程层次**:线程组成块(block,同块可共享内存/同步),块组成网格(grid)。调度以块为单位,所以块数要远超 SM 数量才能占满硬件。
41+
- **内存层次**:寄存器 → shared memory(块内手动管理,~ns)→ global memory(HBM,百 ns 但带宽 1–3 TB/s)。优化的本质是把访问模式搬进前两层:合并访问(coalescing)、分块(tiling)。
42+
- **流与异步**:拷贝与计算放进不同 stream 才能重叠,`cudaMemcpyAsync` + pinned 内存是 H2D/D2H overlap 的前提。
43+
44+
## 与本仓库主题的衔接
45+
46+
GPU 不推翻前面学的内容,而是放大它们:
47+
48+
- **内存布局**([内存布局与缓存](memory-layout.md)):GPU 的合并访问就是"线程 i 访问第 i 个元素",AOS 在 GPU 上同样是反模式,SOA 收益比 CPU 更大。
49+
- **SIMD**([SIMD 向量化](simd-internals.md)):GPU 的 warp/wavefront(32/64 线程锁步执行)本质是超宽 SIMD,分支发散(warp divergence)就是 SIMT 版的"坏的模式"。
50+
- **MPI**([MPI 分布式并行](mpi-distributed.md)):多机多卡 = MPI 管节点间、GPU 管节点内。现代实现支持 GPUDirect RDMA,让网卡直接读写显存,绕过主机内存。
51+
- **测量纪律**:GPU 的基准更容易撒谎——kernel 异步执行,不 `cudaDeviceSynchronize` 计时就是假的;缓存预热、显存复用都要和 CPU 基准一样对待。
52+
53+
## 不引入 GPU 代码的原因
54+
55+
本仓库的原则是**每个示例可在标准 CI 上构建验证**。GPU 工具链(CUDA Toolkit 数 GB、需要设备节点)与这一原则冲突,且 GPU 硬件差异(消费卡/数据中心卡/集成卡)会让"参考数据"失去意义。若未来引入,也应遵循现有模式:可选依赖、configure 期探测、无设备时优雅跳过。
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# I/O 性能
2+
3+
> 本文所有示例均可在本仓库中直接编译运行(Linux)。I/O 的性能模型与计算完全不同:瓶颈在系统调用次数、页缓存与拷贝次数,而不是指令吞吐。
4+
5+
## 心智模型:一次 read() 到底发生了什么
6+
7+
`read(fd, buf, n)` 的内核路径大致是:
8+
9+
1. 陷入内核(上下文切换,几百 ns 起步);
10+
2. 在页缓存(page cache)中查找文件对应的页——命中则无磁盘动作;
11+
3. 把页内数据**拷贝**到用户缓冲区 `buf`
12+
4. 返回用户态。
13+
14+
两个直接推论:
15+
16+
- **系统调用是固定成本**。传 1 字节和传 1 MiB 的陷入开销几乎相同,所以单次传输量越小,单位数据付出的固定成本越高;
17+
- **顺序读热文件时磁盘不是瓶颈**,页缓存命中后拼的是拷贝与调用开销。要测真实设备必须 `O_DIRECT` 绕过页缓存——那是另一类实验。
18+
19+
## 读路径对照:read vs pread vs mmap
20+
21+
`examples/07-io-performance/src/mmap_vs_read.cpp` 用同一份页缓存驻留文件对照三条路径:
22+
23+
| 路径 | 机制 | 特点 |
24+
|------|------|------|
25+
| `read()` 循环 | 每次调用拷贝一段进用户缓冲 | 通用;顺序偏移由 fd 维护 |
26+
| `pread()` 循环 | 带显式偏移的读,不动 fd 偏移 | 多线程可共享一个 fd,无需加锁 |
27+
| `mmap` + 顺序触摸 | 建立页表映射,按页缺页载入 | 无前置拷贝;访问即 I/O |
28+
29+
关键结论(与直觉相反的部分):**顺序全扫描时 mmap 不会快一个数量级**——最终读的页完全相同。本仓库实测(32 MiB 缓存驻留文件)mmap 约快 20%,来自省掉的内核→用户拷贝与更少的调用次数。mmap 真正的优势场景是:
30+
31+
- **随机访问**:直接按地址取数,无需 seek+read 组合;
32+
- **共享映射**:多进程映射同一文件页,省去重复读取与拷贝;
33+
- **大文件稀疏访问**:只有被触摸的页才产生 I/O。
34+
35+
示例用 FNV-1a 校验和验证三条路径读到的内容逐字节一致——对照实验的前提是"做的是同一件事"。
36+
37+
## 写路径:系统调用开销的极端演示
38+
39+
`examples/07-io-performance/src/buffered_write.cpp` 把同一份 1 MiB 数据用三种方式写出:
40+
41+
| 方式 | 系统调用次数 | 本仓库实测 |
42+
|------|--------------|-----------|
43+
| 逐字节 `write(fd, &b, 1)` | 1,048,576 | ~0.7 MiB/s |
44+
| 4 KiB 缓冲块 | 256 | ~930 MiB/s |
45+
| 单次大块写 | 1 | ~1 GiB/s |
46+
47+
**1300 倍**的差距全部来自系统调用固定成本。这就是 `fwrite``std::ofstream`、日志库默认带缓冲的原因,也是"每条日志一次 write"在高频路径上必然后悔的原因。
48+
49+
## 规范库与基准
50+
51+
`include/hpc/io_utils.hpp` 提供模块共用的 POSIX 封装:RAII `FileDescriptor`(含 `O_CREAT` 强制 mode 参数,避免经典的缺 mode 未定义行为)、RAII `MmapView``MAP_SHARED`)、`pread` 整读、确定性图案临时文件。
52+
53+
`examples/07-io-performance/bench/io_bench.cpp` 用 Google Benchmark 在 1–128 MiB 尺寸上复测三条读路径,每个尺寸只创建一次临时文件(setup 不计入计时)。
54+
55+
## 边界说明
56+
57+
- 本模块是 Linux/POSIX 专属(`mmap`/`pread`/`mkstemp`),非 Linux 平台配置时自动跳过;
58+
- `io_uring`(Linux 5.1+ 的异步 I/O 接口,靠提交/完成队列摊薄系统调用)值得单独成篇,本仓库暂不提供示例——它依赖 liburing 且收益场景集中在高并发小 I/O,与本文的缓存驻留实验不同题;
59+
- 测设备真实带宽/延迟需要 `O_DIRECT` + 对齐缓冲 + 控制预读(`posix_fadvise`),并注意 SSD 写放大与 GC 干扰。

0 commit comments

Comments
 (0)