Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
---

<!-- COVERAGE_START -->
![English Coverage](https://img.shields.io/badge/en_coverage-84%25-green.svg) 494/588 docs translated
![English Coverage](https://img.shields.io/badge/en_coverage-92%25-green.svg) 534/582 docs translated
<!-- COVERAGE_END -->

## 这是什么项目
Expand Down
29 changes: 29 additions & 0 deletions code/volumn_codes/vol6-performance/ch02/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
cmake_minimum_required(VERSION 3.20)
project(vol6_ch02_cpu_microarchitecture LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# 02-01 存储层次:memory mountain(吞吐 size×stride)+ 指针追逐延迟阶梯
add_executable(memory_mountain memory_mountain.cpp)
target_compile_options(memory_mountain PRIVATE -O2 -Wall -Wextra -Wpedantic)

# 02-02 缓存行与局部性:步长扫描定位 64B 断崖 + 行优先 vs 列优先 2D 遍历
add_executable(cacheline_locality cacheline_locality.cpp)
target_compile_options(cacheline_locality PRIVATE -O2 -Wall -Wextra -Wpedantic)

# 02-03 流水线/ILP/分支预测
# ⚠️ pipeline_branch_ilp 必须用下面的反优化 flag,否则:
# - GCC 默认 -O2 会把 if 条件累加自动向量化 / 转 cmov → 分支 demo(4.2x)消失
# - 单累加器 dot1 会被循环不变量提升整体消除 → ILP demo 失真
# 这组 flag 是文章论证成立的前提,别「优化」掉。
add_executable(pipeline_branch_ilp pipeline_branch_ilp.cpp)
target_compile_options(pipeline_branch_ilp PRIVATE
-O2 -fno-tree-vectorize -fno-tree-slp-vectorize -fno-if-conversion
-Wall -Wextra -Wpedantic)

# 02-04 TLB / huge page:同工作集 4KB 页 vs 2MB 大页(THP)指针追逐
add_executable(tlb_hugepage tlb_hugepage.cpp)
target_compile_options(tlb_hugepage PRIVATE -O2 -Wall -Wextra -Wpedantic)
49 changes: 49 additions & 0 deletions code/volumn_codes/vol6-performance/ch02/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# vol6 ch02 · CPU 微架构与存储层次 — 代码示例

对应文章:`documents/vol6-performance/ch02-cpu-microarchitecture/`

四个程序,分别对应 ch02 的四篇文章,用本机实测数据佐证每一篇的核心结论。所有数字随 CPU / 编译器 / 运行环境(尤其是虚拟机/容器)变化,**关心比例和趋势,不要把某个绝对数当普适结论**。

## 构建

```bash
# 单独编译(最快,推荐一篇一篇跑)
g++ -O2 -std=c++17 memory_mountain.cpp -o memory_mountain && taskset -c 0 ./memory_mountain
g++ -O2 -std=c++17 cacheline_locality.cpp -o cacheline_locality && taskset -c 0 ./cacheline_locality
g++ -O2 -std=c++17 tlb_hugepage.cpp -o tlb_hugepage && taskset -c 0 ./tlb_hugepage

# ⚠️ pipeline_branch_ilp 必须带反优化 flag,否则 demo 失效(见下)
g++ -O2 -std=c++17 -fno-tree-vectorize -fno-tree-slp-vectorize -fno-if-conversion \
pipeline_branch_ilp.cpp -o pipeline_branch_ilp && taskset -c 0 ./pipeline_branch_ilp

# 或用 CMake 一次构建全部(CMakeLists 已为 pipeline 配好 flag)
cmake -B build && cmake --build build
```

> `taskset -c 0` 把进程绑到 0 号核,避免核间迁移把 cache 弄冷、给测量加噪声。这是 vol6 ch01 *测量方法论* 的标准动作。

## 程序对照

### memory_mountain.cpp —— 02-01 存储层次

两个实验:(A)经典 CSAPP memory mountain,读吞吐随 size × stride 变化;(B)指针追逐随机读延迟,画出 L1/L2/L3/DRAM 的延迟阶梯。要点:L1 ~1 ns、DRAM ~120 ns,差 100 倍。

### cacheline_locality.cpp —— 02-02 缓存行与局部性

(A)步长扫描:工作集固定,扫 stride,吞吐在 stride=64B 处断崖,反证 cacheline=64 字节;(B)行优先 vs 列优先 2D 遍历,~6 倍差距。

### pipeline_branch_ilp.cpp —— 02-03 流水线 / ILP / 分支预测

(A)分支预测:已排序 vs 打乱数组的条件累加,~4 倍差距(预测失败的冲刷代价);(B)ILP:单累加器(长依赖链)vs 4 累加器(并行链),~3 倍差距。

**这个程序必须用 `-fno-tree-vectorize -fno-tree-slp-vectorize -fno-if-conversion` 编译。** 默认 `-O2` 下 GCC 会把 `if` 条件累加自动向量化成 SIMD / 转成 `cmov`(分支消失,打乱=排序),并把单累加器点积当循环不变量整体消除(ILP demo 失真)。这组 flag 是文章论证成立的前提——也是文章里那个「你看到的分支开销取决于编译器把代码编译成了什么」教学点的直接证据。

### tlb_hugepage.cpp —— 02-04 TLB / huge page

同 256 MB 工作集,4 KB 页 vs `madvise(MADV_HUGEPAGE)` 请求 2 MB 大页,指针追逐比延迟。**注意**:本机(WSL2)实测两者无差别(`AnonHugePages: 0`,内核没实际兑现 THP)——这是个诚实的否定结果,演示「你以为开了的优化可能根本没生效」。换裸机 + 预分配 hugetlb 池或全局 THP=always,对 TLB 受限负载能测出提升。

## 怎么读结果

- **比例 > 绝对数**:L1/DRAM 的 100 倍、行/列的 6 倍、ILP 的 3 倍、分支的 4 倍——这些比例是硬件机制决定的,稳。绝对 ns / GB/s 随环境抖动。
- **WSL2 / 虚拟机噪声**:频率被宿主管、TLB/THP 行为可能和裸机不同、`perf` 可能没装。看到不合常理的数字,先怀疑环境,再怀疑结论。
- **汇编是最终裁判**:尤其 pipeline 那个程序,`-O2 -S` 看 `dot1` 是不是真单链、`sum_gt128` 是不是真有 `jcc`,能直接验证你测的是不是你想测的。
102 changes: 102 additions & 0 deletions code/volumn_codes/vol6-performance/ch02/cacheline_locality.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// cacheline_locality.cpp — vol6 ch02-02 用
// 两个实验:
// A. 步长扫描:固定工作集,扫步长,精确定位 cacheline 边界(吞吐在 stride=64 处断崖)
// B. 行优先 vs 列优先 2D 遍历:空间局部性最经典的对照
// 编译: g++ -O2 -std=c++17 cacheline_locality.cpp -o cacheline_locality
// 跑: taskset -c 0 ./cacheline_locality
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <vector>

using clk = std::chrono::steady_clock;
static std::vector<int> g_data;

inline void do_not_optimize(int v) {
asm volatile("" : "+r"(v)::"memory");
}

// ---------- A. 步长扫描:固定 size,扫 stride,测吞吐(元素/ns)----------
// 思路:工作集固定在 L3 区(够大,使每次新 cacheline 加载有成本)。
// stride < 64B 时,一个 cacheline 内的多次访问被摊薄 → 高吞吐;
// stride >= 64B 时,每次访问都触发新 cacheline → 吞吐断崖。
double stride_throughput(long elems, long stride_elem) {
const long ACCESSES = 64'000'000L;
long mask = elems - 1;
int sink = 0;
long idx = 0;
auto t0 = clk::now();
for (long i = 0; i < ACCESSES; ++i) {
sink += g_data[idx];
idx = (idx + stride_elem) & mask;
}
do_not_optimize(sink);
auto t1 = clk::now();
double secs = std::chrono::duration<double>(t1 - t0).count();
return (double)ACCESSES / secs / 1e6; // M 访问/秒
}

// ---------- B. 行优先 vs 列优先 2D 遍历 ----------
void walk_2d(int* a, int N, bool row_major) {
volatile int sink = 0;
int s = 0;
auto t0 = clk::now();
if (row_major) {
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j)
s += a[i * N + j]; // 顺序
} else {
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j)
s += a[j * N + i]; // 跨 N 个 int 跳
}
sink = s;
do_not_optimize(sink);
auto t1 = clk::now();
double secs = std::chrono::duration<double>(t1 - t0).count();
long bytes = (long)N * N * sizeof(int);
std::printf(" %s: %7.1f ms, %.1f GB/s\n",
row_major ? "行优先 row-major" : "列优先 col-major", secs * 1e3,
bytes / secs / 1e9);
}

int main() {
// ===== A. 步长扫描 =====
// 工作集 2 MB(落在 L3,且远大于 L1/L2,使 cacheline miss 成本显形)
const long ELEMS = (2L * 1024 * 1024) / sizeof(int); // 2MB / 4B = 512K 元素
g_data.resize(ELEMS);
for (long i = 0; i < ELEMS; ++i)
g_data[i] = (int)i;

std::printf("===== A. 步长扫描(工作集 2MB,落 L3)=====\n");
std::printf("%-12s %12s %12s\n", "stride(B)", "M访问/秒", "说明");
long strides_B[] = {4, 8, 16, 32, 48, 56, 64, 72, 96, 128, 256, 512};
for (long sb : strides_B) {
long stride_elem = sb / sizeof(int); // int=4B
// warmup
stride_throughput(ELEMS, stride_elem);
double m = stride_throughput(ELEMS, stride_elem);
const char* note = "";
if (sb < 64)
note = "< cacheline:同行的多次访问被摊薄";
else if (sb == 64)
note = "= cacheline:每次访问正好换一行";
else
note = "> cacheline:每次访问都是新行";
std::printf("%8ldB %10.1f %s\n", sb, m, note);
}

// ===== B. 行优先 vs 列优先 =====
const int N = 2048; // 2048*2048*4B = 16MB = L3 大小,放大局部性差异
std::vector<int> a((long)N * N);
for (long i = 0; i < (long)N * N; ++i)
a[i] = (int)i;
std::printf("\n===== B. 2D 遍历(N=%d,矩阵 %d MB = L3 大小)=====\n", N,
N * N * (int)sizeof(int) / (1024 * 1024));
// warmup
walk_2d(a.data(), N, true);
walk_2d(a.data(), N, true);
walk_2d(a.data(), N, false);
walk_2d(a.data(), N, false);
return 0;
}
137 changes: 137 additions & 0 deletions code/volumn_codes/vol6-performance/ch02/memory_mountain.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// memory_mountain.cpp — vol6 ch02-01 用
// 两个实验:
// A. 经典 CSAPP memory mountain(读吞吐, size × stride 网格)
// B. 指针追逐随机读延迟(真实访问延迟随工作集跨过 L1/L2/L3/DRAM 的断崖)
// 编译: g++ -O2 -std=c++17 memory_mountain.cpp -o memory_mountain
// 跑: taskset -c 0 ./memory_mountain (绑核降噪)
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <random>
#include <vector>

using clk = std::chrono::steady_clock;

// ---------- A. memory mountain: 顺序步长读吞吐 ----------
// data 大小 = elems * 8 字节。以 stride(单位:元素个数)环形遍历。
// 固定总访问次数 ACCESSES,测吞吐。
static std::vector<int64_t> g_data;

// 把 v 强制「被消费」+ 内存屏障,等同 Google Benchmark 的 DoNotOptimize 语义
inline void do_not_optimize(int64_t v) {
asm volatile("" : "+r"(v)::"memory");
}

double throughput(long elems, long stride) {
const long ACCESSES = 64'000'000L; // 总访问次数(够大以摊薄计时噪声)
long mask = elems - 1; // elems 必须是 2 的幂
int64_t sink = 0;
long idx = 0;
auto t0 = clk::now();
for (long i = 0; i < ACCESSES; ++i) {
sink += g_data[idx]; // g_data 非 const,load 无法被消除
idx = (idx + stride) & mask; // 环形,始终落在 [0, elems)
}
do_not_optimize(sink); // sink 必须求值
auto t1 = clk::now();
double secs = std::chrono::duration<double>(t1 - t0).count();
// 每次访问读 8 字节
return (double)ACCESSES * 8.0 / secs / 1e9; // GB/s
}

// ---------- B. 指针追逐真实随机读延迟 ----------
// 构造一条贯穿全部节点的环形链,idx_next = perm 置乱,硬件预取器无法预测
// (下一个地址依赖本次 load 的结果),测出真实访存延迟。
double chase_latency(long elems) {
// 构造置乱单环
std::vector<long> perm(elems);
for (long i = 0; i < elems; ++i)
perm[i] = i;
std::mt19937_64 rng(0xC0FFEEull);
for (long i = elems - 1; i > 0; --i) {
long j = (long)(rng() % (i + 1));
std::swap(perm[i], perm[j]);
}
std::vector<long> nxt(elems);
for (long i = 0; i < elems; ++i)
nxt[perm[i]] = perm[(i + 1) % elems];

const long ROUNDS = 8; // 每个元素访问 ROUNDS 次
long total_steps = elems * ROUNDS;
volatile int64_t sink = 0;
long idx = 0;
auto t0 = clk::now();
for (long s = 0; s < total_steps; ++s) {
idx = nxt[idx];
sink = sink + g_data[idx]; // 读 data[idx] 制造真依赖
}
auto t1 = clk::now();
double secs = std::chrono::duration<double>(t1 - t0).count();
if (sink == 0x12345)
std::printf("");
return secs / total_steps * 1e9; // ns/访问
}

int main() {
// 给 data 分配到足够大(最大覆盖到 DRAM 区:64M 元素 = 512MB)
// 用非常量填充:g_data[i] = i,确保每次 load 无法被编译器折叠成常量
const long MAX_ELEMS = 1L << 26; // 64M * 8B = 512 MB
g_data.resize(MAX_ELEMS);
for (long i = 0; i < MAX_ELEMS; ++i)
g_data[i] = i;

// ===== A. memory mountain =====
std::printf("===== A. memory mountain: 读吞吐 (GB/s) =====\n");
std::printf("size\\stride(B)");
long strides_elem[] = {1, 2, 4, 8, 16, 32, 64}; // 元素步长
long strides_B[] = {8, 16, 32, 64, 128, 256, 512}; // 字节步长
int ns = (int)(sizeof(strides_elem) / sizeof(strides_elem[0]));
for (int k = 0; k < ns; ++k)
std::printf("%8ldB", strides_B[k]);
std::printf("\n");

long size_KB[] = {1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768};
int nsize = (int)(sizeof(size_KB) / sizeof(size_KB[0]));
for (int s = 0; s < nsize; ++s) {
long elems = (size_KB[s] * 1024L) / 8; // 元素个数
if (elems > MAX_ELEMS)
break;
std::printf("%7ldK", size_KB[s]);
for (int k = 0; k < ns; ++k) {
// warmup 一次
throughput(elems, strides_elem[k]);
double gb = throughput(elems, strides_elem[k]);
std::printf("%8.1f", gb);
std::fflush(stdout);
}
std::printf("\n");
}

// ===== B. 指针追逐延迟阶梯 =====
std::printf("\n===== B. 指针追逐随机读延迟 (ns/访问) =====\n");
std::printf("%10s %10s %10s %12s\n", "size", "elems", "ns/access", "level(推断)");
long lat_KB[] = {4, 8, 16, 32, 64, 128, 256, 512,
1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072};
int nl = (int)(sizeof(lat_KB) / sizeof(lat_KB[0]));
for (int s = 0; s < nl; ++s) {
long elems = (lat_KB[s] * 1024L) / 8;
if (elems > MAX_ELEMS)
break;
// warmup
chase_latency(elems);
double ns = chase_latency(elems);
const char* lvl = "";
if (lat_KB[s] <= 32)
lvl = "L1d"; // <=32K
else if (lat_KB[s] <= 512)
lvl = "L2"; // <=512K
else if (lat_KB[s] <= 16384)
lvl = "L3"; // <=16M
else
lvl = "DRAM";
std::printf("%8ldK %10ld %10.2f %12s\n", lat_KB[s], elems, ns, lvl);
std::fflush(stdout);
}
return 0;
}
Loading
Loading