0%

概述

动态插桩(Dynamic Instrumentation)是在程序运行时插入监控代码的技术,无需重新编译程序即可进行性能分析和调试。本文介绍 C/C++ 程序中常用的动态插桩工具,重点关注函数调用次数和耗时统计(平均、最小、最大、总计),以及是否支持 attach 到正在运行的进程。

重要说明:耗时统计的范围

不同工具在统计函数耗时时的行为存在重要差异:

  • 墙上时钟时间(Wall-clock Time):包括函数执行期间的所有时间,包括 CPU 执行时间、IO 等待时间、sleep 时间等。这是函数从开始到结束的”真实”耗时。
  • CPU 时间(CPU Time):只包括函数在 CPU 上实际执行的时间,不包括 IO 等待和 sleep 时间。
  • 用户态 CPU 时间(User CPU Time):只包括在用户态执行的时间,不包括内核态时间。

大多数动态插桩工具默认统计的是墙上时钟时间,这意味着如果函数中包含 IO 操作(如文件读写、网络通信)或 sleep,这些时间也会被计入总耗时。这对于理解函数的”真实”执行时间很有帮助,但需要注意区分 CPU 密集型操作和 IO 密集型操作。

动态插桩工具对比

工具 插桩方式 调用次数统计 耗时统计(avg/min/max/total) 耗时范围 Attach 支持 权限要求 开销 适用场景
eBPF/BCC 内核级动态插桩 墙上时钟时间 root 或 CAP_BPF 极低 Linux 现代系统分析
bptrace 内核级动态插桩(eBPF) 墙上时钟时间 root 或 CAP_BPF 极低 Linux 函数级性能分析
SystemTap 内核级动态插桩 墙上时钟时间 root 或 stapdev/stapusr 组 低-中 Linux 系统级分析
perf + uprobes 内核级动态插桩 墙上时钟时间(可配置) root 或 perf_event_paranoid Linux 系统级分析
DTrace 内核级动态插桩 墙上时钟时间 root 或特殊权限 Solaris/FreeBSD/macOS
Intel Pin 二进制插桩 墙上时钟时间 普通用户权限 详细分析,需要启动时插桩
DynamoRIO 二进制插桩 墙上时钟时间 普通用户权限 跨平台分析,需要启动时插桩
Valgrind Callgrind 二进制插桩 CPU 时间(不包括IO/sleep) 普通用户权限 极高 详细调用图分析
LD_PRELOAD 库函数拦截 墙上时钟时间 普通用户权限 简单场景,库函数级别
ltrace 库函数跟踪 部分 墙上时钟时间 普通用户权限(attach 需 ptrace) 库函数调用跟踪

详细工具介绍

1. eBPF/BCC

简介:基于 eBPF(Extended Berkeley Packet Filter)的现代动态跟踪工具集,BCC 提供了高级封装。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 极低开销,内核验证保证安全
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 丰富的工具集(funccount, funclatency, trace 等)
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • root 权限:拥有所有 eBPF/BCC 功能
  • 非 root 用户:需要 CAP_BPF 能力(Linux 5.8+):
    1
    2
    3
    4
    # 授予用户 CAP_BPF 能力
    sudo setcap cap_bpf+ep /usr/bin/python3
    # 或授予特定 BCC 工具
    sudo setcap cap_bpf+ep /usr/share/bcc/tools/funccount
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 加载 eBPF 程序:需要 root 权限或 CAP_BPF 能力(Linux 5.8+)
  • 读取内核符号:需要 root 权限或 CAP_SYS_ADMIN 能力

限制

  • 需要 Linux 4.1+ 内核(eBPF 支持)

使用示例

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# funclatency - 统计函数耗时分布
funclatency -p <pid> 'target_function'

# funccount - 统计函数调用次数
funccount -p <pid> 'target_function'

# 自定义 BCC 脚本统计详细指标
from bcc import BPF

bpf_text = """
#include <uapi/linux/ptrace.h>

BPF_HASH(start, u32);
BPF_HASH(count, u32);
BPF_HASH(total_time, u64);
BPF_HASH(min_time, u64);
BPF_HASH(max_time, u64);

int trace_entry(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
u64 ts = bpf_ktime_get_ns();
start.update(&pid, &ts);
u64 zero = 0;
count.update(&pid, &zero);
u64 *val = count.lookup(&pid);
if (val) {
(*val)++;
count.update(&pid, val);
}
return 0;
}

int trace_return(struct pt_regs *ctx) {
u32 pid = bpf_get_current_pid_tgid();
u64 *tsp = start.lookup(&pid);
if (tsp == 0) {
return 0;
}
u64 delta = bpf_ktime_get_ns() - *tsp;

// 更新统计信息
u64 *total = total_time.lookup(&pid);
u64 *min = min_time.lookup(&pid);
u64 *max = max_time.lookup(&pid);

if (total) {
*total += delta;
} else {
total_time.update(&pid, &delta);
}

if (!min || delta < *min) {
min_time.update(&pid, &delta);
}

if (!max || delta > *max) {
max_time.update(&pid, &delta);
}

start.delete(&pid);
return 0;
}
"""

# attach 到进程
b = BPF(text=bpf_text)
b.attach_uprobe(name="target_program", sym="target_function", fn_name="trace_entry")
b.attach_uretprobe(name="target_program", sym="target_function", fn_name="trace_return")

2. bptrace

简介:基于 eBPF 的轻量级动态追踪工具,专门用于监控和分析正在运行的 C/C++ 程序。bptrace 提供了简洁的命令行接口,可以方便地统计函数调用次数和执行时间。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 极低开销,基于 eBPF 技术
  • ✅ 简洁的命令行接口,易于使用
  • ✅ 无需修改程序源码或重新编译
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • root 权限:拥有所有 bptrace 功能
  • 非 root 用户:需要 CAP_BPF 能力(Linux 5.8+):
    1
    2
    # 授予用户 CAP_BPF 能力
    sudo setcap cap_bpf+ep /usr/bin/bptrace
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 加载 eBPF 程序:需要 root 权限或 CAP_BPF 能力(Linux 5.8+)

限制

  • 需要 Linux 内核支持 eBPF(通常 4.1+)
  • Linux 5.8+ 才支持非 root 用户使用 CAP_BPF
  • 主要适用于用户态函数追踪
  • 需要目标程序包含调试符号信息(或使用地址)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 统计函数调用次数
bptrace -p <pid> -f 'target_function' -c

# 统计函数耗时(包括平均、最小、最大、总耗时)
bptrace -p <pid> -f 'target_function' -t

# 同时统计调用次数和耗时
bptrace -p <pid> -f 'target_function' -c -t

# 统计多个函数
bptrace -p <pid> -f 'function1,function2' -c -t

# 指定输出格式
bptrace -p <pid> -f 'target_function' -t --format json

# 持续监控并定期输出统计信息
bptrace -p <pid> -f 'target_function' -t --interval 5

输出示例

1
2
3
4
5
6
Function: target_function
Call Count: 1000
Total Time: 50000 us
Average Time: 50 us
Min Time: 10 us
Max Time: 200 us

与 eBPF/BCC 的关系

  • bptrace 可以看作是 BCC 工具集的简化版本,专门针对函数级性能分析
  • 相比 BCC,bptrace 提供了更简洁的命令行接口,适合快速分析
  • 如果需要更复杂的自定义逻辑,仍需要使用 BCC 编写 Python/C 脚本

3. SystemTap

简介:Linux 系统级动态跟踪工具,功能强大,支持用户态和内核态插桩。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以精确统计函数调用次数和耗时(包括 min/max/avg/total)
  • ✅ 灵活的脚本语言,可以自定义统计逻辑
  • ✅ 低开销(取决于脚本复杂度)
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • root 权限:拥有所有 SystemTap 功能
  • 非 root 用户:需要加入特定组:
    • stapdev 组:可以加载任意 SystemTap 模块(需要 root 权限添加)
    • stapusr 组:只能使用预编译的 SystemTap 模块(更安全)
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 内核模块加载:需要 root 权限或 CAP_SYS_MODULE 能力

限制

  • 需要安装 kernel-devel 包(用于编译 SystemTap 模块)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 统计函数调用次数和耗时
probe process("/path/to/program").function("target_function") {
start_time = gettimeofday_us()
}

probe process("/path/to/program").function("target_function").return {
call_count++
elapsed = gettimeofday_us() - start_time
total_time += elapsed
if (elapsed < min_time || min_time == 0) min_time = elapsed
if (elapsed > max_time) max_time = elapsed
}

probe end {
printf("调用次数: %d\n", call_count)
printf("总耗时: %d us\n", total_time)
printf("平均耗时: %d us\n", total_time / call_count)
printf("最小耗时: %d us\n", min_time)
printf("最大耗时: %d us\n", max_time)
}

# attach 到运行中的进程
stap -x <pid> script.stp

4. perf + uprobes

简介:Linux 内核自带的性能分析工具,通过 uprobes 机制实现用户态动态插桩。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 低开销,基于采样和事件计数
  • ✅ 可以统计函数调用次数和耗时
  • ✅ 无需修改程序源码或重新编译
  • ⏱️ 耗时统计范围:默认统计墙上时钟时间(包括 IO、sleep 等),也可配置为统计 CPU 时间

权限要求

  • root 权限:最直接的方式,拥有所有 perf 功能
  • 非 root 用户:需要设置 /proc/sys/kernel/perf_event_paranoid
    • -1:允许所有用户使用 perf(不推荐,安全风险)
    • 0:允许用户分析自己的进程
    • 1:允许用户分析自己的进程和内核(默认值)
    • 2:只允许 root 使用 perf
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 查看内核符号:需要 root 权限或设置 perf_event_paranoid <= 1

限制

  • 统计详细耗时需要额外脚本处理

使用示例

1
2
3
4
5
6
7
# 统计函数调用次数
perf probe -x ./program function_name
perf record -e probe_program:function_name ./program

# 统计函数耗时(需要自定义脚本或结合其他工具)
perf record -g -p <pid> # attach 到运行中的进程
perf report

5. DTrace

简介:Sun Microsystems 开发的动态跟踪框架,现支持 Solaris、FreeBSD、macOS。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 低开销,功能强大
  • ✅ 支持聚合统计(aggregations)
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • macOS
    • 需要关闭 SIP(System Integrity Protection)或使用特殊权限
    • 或者使用 sudo 运行(需要管理员权限)
  • Solaris/FreeBSD
    • 需要 root 权限或 dtrace_kernel 权限
  • Linux
    • 支持有限(需要 Oracle Linux 或通过 SystemTap)
    • 通常需要 root 权限

限制

  • Linux 上支持有限(需要 Oracle Linux 或通过 SystemTap)

使用示例

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
#!/usr/sbin/dtrace -s

pid$target:target:function_name:entry
{
self->start = timestamp;
@count[probefunc] = count();
}

pid$target:target:function_name:return
/self->start/
{
this->elapsed = timestamp - self->start;
@time["total"] = sum(this->elapsed);
@time["avg"] = avg(this->elapsed);
@time["min"] = min(this->elapsed);
@time["max"] = max(this->elapsed);
self->start = 0;
}

END
{
printa("调用次数: %@d\n", @count);
printa("总耗时: %@d ns\n", @time["total"]);
printa("平均耗时: %@d ns\n", @time["avg"]);
printa("最小耗时: %@d ns\n", @time["min"]);
printa("最大耗时: %@d ns\n", @time["max"]);
}

# 使用方式
dtrace -s script.d -p <pid>

简介:基于 eBPF 的轻量级动态追踪工具,专门用于监控和分析正在运行的 C/C++ 程序。bptrace 提供了简洁的命令行接口,可以方便地统计函数调用次数和执行时间。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 极低开销,基于 eBPF 技术
  • ✅ 简洁的命令行接口,易于使用
  • ✅ 无需修改程序源码或重新编译
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • root 权限:拥有所有 bptrace 功能
  • 非 root 用户:需要 CAP_BPF 能力(Linux 5.8+):
    1
    2
    # 授予用户 CAP_BPF 能力
    sudo setcap cap_bpf+ep /usr/bin/bptrace
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 加载 eBPF 程序:需要 root 权限或 CAP_BPF 能力(Linux 5.8+)

限制

  • 需要 Linux 内核支持 eBPF(通常 4.1+)
  • Linux 5.8+ 才支持非 root 用户使用 CAP_BPF
  • 主要适用于用户态函数追踪
  • 需要目标程序包含调试符号信息(或使用地址)

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 统计函数调用次数
bptrace -p <pid> -f 'target_function' -c

# 统计函数耗时(包括平均、最小、最大、总耗时)
bptrace -p <pid> -f 'target_function' -t

# 同时统计调用次数和耗时
bptrace -p <pid> -f 'target_function' -c -t

# 统计多个函数
bptrace -p <pid> -f 'function1,function2' -c -t

# 指定输出格式
bptrace -p <pid> -f 'target_function' -t --format json

# 持续监控并定期输出统计信息
bptrace -p <pid> -f 'target_function' -t --interval 5

输出示例

1
2
3
4
5
6
Function: target_function
Call Count: 1000
Total Time: 50000 us
Average Time: 50 us
Min Time: 10 us
Max Time: 200 us

与 eBPF/BCC 的关系

  • bptrace 可以看作是 BCC 工具集的简化版本,专门针对函数级性能分析
  • 相比 BCC,bptrace 提供了更简洁的命令行接口,适合快速分析
  • 如果需要更复杂的自定义逻辑,仍需要使用 BCC 编写 Python/C 脚本

6. Intel Pin

简介:Intel 开发的动态二进制插桩框架,功能强大但开销较高。

特点

  • 不支持 attach,必须在程序启动时插桩
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 支持细粒度插桩(指令级)
  • ⚠️ 高开销(通常 10-100 倍)
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • 普通用户权限:Intel Pin 不需要特殊权限,普通用户即可使用
  • 读取目标程序:需要目标程序的读取权限
  • 写入输出文件:需要输出目录的写入权限

限制

  • 不支持 attach 到运行中的进程
  • 高开销,不适合生产环境
  • 主要适用于详细分析和研究

使用示例

1
2
3
4
5
# 使用 Pin 工具统计函数调用
pin -t source/tools/ManualExamples/obj-intel64/inscount0.so -- ./program

# 自定义 Pin 工具统计函数耗时
# 需要编写 Pin 工具(C++)

7. DynamoRIO

简介:跨平台的动态二进制插桩框架,支持 Windows、Linux、macOS。

特点

  • 不支持 attach,必须在程序启动时插桩
  • ✅ 可以统计函数调用次数和耗时(min/max/avg/total)
  • ✅ 跨平台支持
  • ⚠️ 高开销
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间)

权限要求

  • 普通用户权限:DynamoRIO 不需要特殊权限,普通用户即可使用
  • 读取目标程序:需要目标程序的读取权限
  • 写入输出文件:需要输出目录的写入权限
  • Windows:可能需要管理员权限(取决于目标程序)

限制

  • 不支持 attach 到运行中的进程
  • 高开销
  • 需要编写客户端工具

使用示例

1
2
3
4
5
6
# 使用 DynamoRIO 工具
drrun -tool calltrace -- ./program
drrun -tool memtrace -- ./program

# 自定义工具统计函数耗时
# 需要编写 DynamoRIO 客户端(C++)

8. Valgrind Callgrind

简介:Valgrind 工具集中的调用图分析工具。

特点

  • 不支持 attach,必须在程序启动时插桩
  • ✅ 可以统计函数调用次数和耗时
  • ✅ 生成详细的调用图
  • ⚠️ 极高开销(通常 20-100 倍)
  • ⏱️ 耗时统计范围:统计 CPU 时间(不包括 IO 等待和 sleep 时间),只统计函数在 CPU 上实际执行的时间

权限要求

  • 普通用户权限:Valgrind 不需要特殊权限,普通用户即可使用
  • 读取目标程序:需要目标程序的读取权限
  • 写入输出文件:需要输出目录的写入权限
  • 内存访问:Valgrind 需要访问进程内存,但不需要 root 权限

限制

  • 不支持 attach
  • 极高开销,不适合生产环境
  • 主要用于开发阶段的详细分析

使用示例

1
2
3
4
5
6
# 使用 Callgrind 分析
valgrind --tool=callgrind ./program

# 查看结果
callgrind_annotate callgrind.out.<pid>
kcachegrind callgrind.out.<pid> # GUI 工具

9. LD_PRELOAD + 自定义库

简介:通过 LD_PRELOAD 机制拦截库函数调用,实现简单的动态插桩。

特点

  • 不支持 attach,需要在启动时设置环境变量
  • ✅ 可以统计库函数调用次数和耗时
  • ✅ 低开销
  • ⚠️ 只能拦截库函数,不能拦截静态函数
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间),取决于使用的计时函数(如 gettimeofday()

权限要求

  • 普通用户权限:LD_PRELOAD 不需要特殊权限,普通用户即可使用
  • 读取目标程序:需要目标程序的读取权限
  • 加载共享库:需要共享库的读取权限
  • 设置环境变量:需要设置 LD_PRELOAD 环境变量的权限(通常都有)

限制

  • 不支持 attach
  • 只能拦截库函数,不能拦截静态函数或内联函数
  • 需要手动编写包装代码

使用示例

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
43
44
45
46
// wrapper.c - 包装库函数
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <time.h>
#include <sys/time.h>

static unsigned long call_count = 0;
static unsigned long total_time = 0;
static unsigned long min_time = ULONG_MAX;
static unsigned long max_time = 0;

void __attribute__((constructor)) init() {
// 初始化
}

void __attribute__((destructor)) fini() {
printf("调用次数: %lu\n", call_count);
printf("总耗时: %lu us\n", total_time);
if (call_count > 0) {
printf("平均耗时: %lu us\n", total_time / call_count);
printf("最小耗时: %lu us\n", min_time);
printf("最大耗时: %lu us\n", max_time);
}
}

// 包装目标函数
int target_function(int arg) {
struct timeval start, end;
gettimeofday(&start, NULL);

// 调用原始函数
int (*original_func)(int) = dlsym(RTLD_NEXT, "target_function");
int result = original_func(arg);

gettimeofday(&end, NULL);
unsigned long elapsed = (end.tv_sec - start.tv_sec) * 1000000 +
(end.tv_usec - start.tv_usec);

call_count++;
total_time += elapsed;
if (elapsed < min_time) min_time = elapsed;
if (elapsed > max_time) max_time = elapsed;

return result;
}
1
2
3
4
5
# 编译包装库
gcc -shared -fPIC -o wrapper.so wrapper.c -ldl

# 使用
LD_PRELOAD=./wrapper.so ./program

10. ltrace

简介:Linux 库函数调用跟踪工具。

特点

  • ✅ 支持 attach 到正在运行的进程
  • ✅ 可以统计库函数调用次数
  • ⚠️ 只能统计库函数,不能统计自定义函数
  • ⚠️ 耗时统计功能有限
  • ⏱️ 耗时统计范围:统计墙上时钟时间(包括 IO、sleep 等所有时间),但功能有限

权限要求

  • 跟踪自己的进程:普通用户权限即可
  • attach 到其他用户的进程:需要 root 权限或 CAP_SYS_PTRACE 能力
  • 读取目标程序:需要目标程序的读取权限
  • ptrace 系统调用:attach 功能依赖 ptrace,受 /proc/sys/kernel/yama/ptrace_scope 限制:
    • 0:允许同一用户调试其权限范围内的任意进程
    • 1:只允许调试直接子进程(默认值)
    • 2:只有 root 或具备 CAP_SYS_PTRACE 的进程可以使用 ptrace
    • 3:完全禁用 ptrace

限制

  • 只能跟踪库函数
  • 耗时统计功能有限
  • 不适合统计自定义函数

使用示例

1
2
3
4
5
# 跟踪库函数调用
ltrace -p <pid> -c # 统计调用次数

# 跟踪特定函数
ltrace -p <pid> -e 'malloc+free'

耗时统计范围详解

墙上时钟时间 vs CPU 时间

理解不同工具统计的耗时范围对于正确解读性能数据至关重要:

1. 墙上时钟时间(Wall-clock Time)

包括的内容

  • ✅ CPU 执行时间
  • ✅ IO 等待时间(文件读写、网络通信等)
  • ✅ sleep 时间(sleep(), usleep(), nanosleep() 等)
  • ✅ 线程阻塞时间(等待锁、条件变量等)
  • ✅ 上下文切换时间

适用场景

  • 了解函数的”真实”执行时间
  • 分析 IO 密集型函数的性能
  • 诊断包含阻塞操作的函数
  • 评估用户体验相关的性能指标

示例

1
2
3
4
5
6
7
8
9
10
11
12
void slow_function() {
// CPU 执行:1ms
do_computation();

// IO 等待:100ms
read_from_disk();

// sleep:50ms
sleep(0.05);

// 总墙上时钟时间:~151ms
}

使用墙上时钟时间的工具

  • SystemTap、DTrace、eBPF/BCC、bptrace
  • Intel Pin、DynamoRIO
  • LD_PRELOAD(使用 gettimeofday() 等)
  • perf(默认配置)

2. CPU 时间(CPU Time)

包括的内容

  • ✅ CPU 执行时间
  • 不包括 IO 等待时间
  • 不包括 sleep 时间
  • 不包括 线程阻塞时间

适用场景

  • 分析 CPU 密集型函数的性能
  • 评估算法的计算复杂度
  • 识别 CPU 热点
  • 优化计算逻辑

示例

1
2
3
4
5
6
7
8
9
10
11
12
void slow_function() {
// CPU 执行:1ms(计入)
do_computation();

// IO 等待:100ms(不计入)
read_from_disk();

// sleep:50ms(不计入)
sleep(0.05);

// CPU 时间:~1ms(只包括 CPU 执行时间)
}

使用 CPU 时间的工具

  • Valgrind Callgrind(主要统计 CPU 时间)

3. 实际应用建议

选择统计范围的原则

  1. IO 密集型函数:使用墙上时钟时间

    • 文件操作、网络通信、数据库查询
    • 需要了解包括等待时间在内的总耗时
  2. CPU 密集型函数:两种时间都关注

    • 算法计算、数据处理
    • CPU 时间用于评估算法效率
    • 墙上时钟时间用于评估用户体验
  3. 混合型函数:优先使用墙上时钟时间

    • 大多数实际应用中的函数
    • 墙上时钟时间更能反映真实性能

注意事项

  • ⚠️ 多线程环境:墙上时钟时间可能小于 CPU 时间(并行执行)
  • ⚠️ IO 操作:如果函数包含 IO,墙上时钟时间会显著大于 CPU 时间
  • ⚠️ sleep 操作:如果函数包含 sleep,墙上时钟时间会包含 sleep 时间
  • ⚠️ 上下文切换:频繁的上下文切换会增加墙上时钟时间

如何区分 CPU 时间和 IO 时间

如果使用统计墙上时钟时间的工具,可以通过以下方式区分:

  1. 结合系统调用跟踪:使用 straceperf trace 查看 IO 系统调用
  2. 分析函数内部:如果函数耗时很长但 CPU 使用率低,可能是 IO 等待
  3. 使用 perf 的 CPU 时间模式perf record -e cpu-clock 可以统计 CPU 时间

权限要求总结

权限类型说明

1. root 权限

  • 含义:拥有系统最高权限
  • 获取方式:使用 sudo 或切换到 root 用户
  • 适用工具:perf、SystemTap、DTrace、eBPF/BCC、bptrace(默认需要)

2. Linux Capabilities(能力)

现代 Linux 系统使用 capabilities 机制,允许非 root 用户执行特定操作:

  • CAP_BPF:加载 eBPF 程序(Linux 5.8+)
    1
    sudo setcap cap_bpf+ep /path/to/tool
  • CAP_SYS_PTRACE:使用 ptrace attach 到其他进程
    1
    sudo setcap cap_sys_ptrace+ep /path/to/tool
  • CAP_SYS_ADMIN:访问内核符号和系统管理功能
  • CAP_SYS_MODULE:加载内核模块

3. 普通用户权限

  • 含义:不需要特殊权限,普通用户即可使用
  • 适用工具:Intel Pin、DynamoRIO、Valgrind Callgrind、LD_PRELOAD

4. 组权限

  • stapdev 组:SystemTap 开发组,可以加载任意模块
  • stapusr 组:SystemTap 用户组,只能使用预编译模块

权限配置示例

配置 perf 非 root 使用

1
2
3
4
5
6
# 允许用户分析自己的进程
echo 0 | sudo tee /proc/sys/kernel/perf_event_paranoid

# 或永久配置
echo "kernel.perf_event_paranoid = 0" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

配置 eBPF/BCC 非 root 使用(Linux 5.8+)

1
2
3
4
5
6
# 授予 Python 解释器 CAP_BPF 能力
sudo setcap cap_bpf+ep /usr/bin/python3

# 或授予特定 BCC 工具
sudo setcap cap_bpf+ep /usr/share/bcc/tools/funccount
sudo setcap cap_bpf+ep /usr/share/bcc/tools/funclatency

配置 SystemTap 非 root 使用

1
2
3
4
# 将用户添加到 stapusr 组
sudo usermod -a -G stapusr $USER

# 需要重新登录使组权限生效

配置 ptrace(用于 attach 功能)

1
2
3
4
5
6
7
8
# 查看当前 ptrace_scope 设置
cat /proc/sys/kernel/yama/ptrace_scope

# 允许同一用户调试其权限范围内的进程(开发环境)
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope

# 或永久配置
echo "kernel.yama.ptrace_scope = 0" | sudo tee -a /etc/sysctl.conf

权限要求快速参考

工具 跟踪自己的进程 attach 到其他用户的进程 内核级插桩
eBPF/BCC CAP_BPF(Linux 5.8+) root root
bptrace CAP_BPF(Linux 5.8+) root root
SystemTap stapusr 组 root root
perf 普通用户(需配置) root root
DTrace root root root
Intel Pin 普通用户 N/A N/A
DynamoRIO 普通用户 N/A N/A
Valgrind 普通用户 N/A N/A
LD_PRELOAD 普通用户 N/A N/A
ltrace 普通用户 root 或 CAP_SYS_PTRACE N/A

安全注意事项

⚠️ 生产环境建议

  • 避免使用 perf_event_paranoid = -1(允许所有用户)
  • 避免将用户添加到 stapdev 组(安全风险)
  • 谨慎配置 ptrace_scope = 0(允许任意进程调试)
  • 使用 capabilities 而非 root 权限(最小权限原则)
  • 定期审查已授予的 capabilities

工具选择建议

需要 attach 到运行中进程

  1. eBPF/BCC(推荐):现代、低开销、功能强大
  2. bptrace(推荐):简洁易用,专门针对函数级性能分析
  3. SystemTap:功能强大,脚本灵活
  4. perf + uprobes:系统自带,简单易用
  5. DTrace:如果使用 Solaris/FreeBSD/macOS

不需要 attach(可以重新启动程序)

  1. Intel Pin / DynamoRIO:需要详细分析时使用
  2. Valgrind Callgrind:需要调用图分析时使用
  3. LD_PRELOAD:简单场景,只统计库函数

统计指标对比

工具 调用次数 平均耗时 最小耗时 最大耗时 总耗时
eBPF/BCC
bptrace
SystemTap
perf + uprobes ⚠️ 需脚本 ⚠️ 需脚本 ⚠️ 需脚本 ⚠️ 需脚本
DTrace
Intel Pin
DynamoRIO
Valgrind Callgrind
LD_PRELOAD
ltrace

总结

对于 C/C++ 程序的动态插桩,推荐使用以下工具:

  1. 生产环境 + 需要 attachbptraceeBPF/BCCSystemTap
  2. 开发调试 + 详细分析Intel PinDynamoRIO
  3. 简单场景 + 库函数统计LD_PRELOAD
  4. 系统级分析perf + uprobes

选择工具时需要考虑:

  • 是否需要 attach 到运行中的进程
  • 对性能开销的容忍度
  • 需要统计的详细程度
  • 系统平台和权限限制
  • 耗时统计范围:大多数工具统计墙上时钟时间(包括 IO、sleep),只有 Valgrind Callgrind 统计 CPU 时间(不包括 IO、sleep)
  • 权限要求
    • 内核级插桩工具(perf、SystemTap、eBPF/BCC、bptrace)通常需要 root 权限或特殊 capabilities
    • 二进制插桩工具(Intel Pin、DynamoRIO、Valgrind)通常只需要普通用户权限
    • attach 到其他用户的进程需要 root 权限或 CAP_SYS_PTRACE 能力

代码

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
template <typename Predicate>
void SpinWaitWhile(Predicate pred) {
int count = 0;
while (pred()) {
if (count < 100) {
tbb::detail::machine_pause(10);
++count;
} else if (count < 200) {
utils::yield();
++count;
} else {
std::this_thread::sleep_for(std::chrono::microseconds(count/100));
if (count < 10000) {
count += 100;
}
}
}
}

static inline void machine_pause(int32_t delay) {
#if __TBB_x86_64 || __TBB_x86_32
while (delay-- > 0) { _mm_pause(); }
#elif __ARM_ARCH_7A__ || __aarch64__
while (delay-- > 0) { __asm__ __volatile__("isb sy" ::: "memory"); }
#else /* Generic */
(void)delay; // suppress without including _template_helpers.h
yield();
#endif
}

函数说明与CPU周期分析

该自旋锁退避算法采用三级退避策略,逐步降低CPU占用:

1. 第一阶段:machine_pause() (count < 100)

x86架构 (_mm_pause())

  • 功能:执行 PAUSE 指令,提示CPU当前处于自旋等待状态
  • 系统调用:❌ - 纯用户态CPU指令
  • 进入内核态:❌ - 完全在用户态执行
  • 调用调度器:❌ - 不涉及操作系统调度
  • CPU周期
    • Intel处理器:约 140个周期(Skylake及以后架构)
    • AMD处理器:约 10-40个周期
    • 代码中调用 machine_pause(10),实际执行10次 PAUSE 指令
  • 开销来源
    • CPU指令执行延迟(主要开销)
    • 流水线停顿(Pipeline Stall)
    • 无系统调用开销、无上下文切换开销
  • 作用
    • 降低CPU功耗(避免超线程的忙等待)
    • 减少内存顺序违规(Memory Order Violation)
    • 在超线程环境下,让出执行资源给同核心的另一个线程
  • 总开销:每次循环约 1400-4000个周期(Intel)或 100-400个周期(AMD)

ARM架构 (isb sy)

  • 功能:指令同步屏障(Instruction Synchronization Barrier),确保所有指令完成
  • 系统调用:❌ - 纯用户态CPU指令
  • 进入内核态:❌ - 完全在用户态执行
  • 调用调度器:❌ - 不涉及操作系统调度
  • CPU周期:约 10-50个周期(取决于具体ARM核心)
  • 开销来源
    • CPU指令执行延迟(主要开销)
    • 指令流水线同步等待
    • 无系统调用开销、无上下文切换开销
  • 作用:确保内存访问顺序,避免乱序执行导致的问题
  • 总开销:每次循环约 100-500个周期

2. 第二阶段:utils::yield() (100 ≤ count < 200)

  • 功能:主动让出CPU时间片,通常对应 sched_yield() 系统调用
  • 系统调用:✅ - 调用 sched_yield() 系统调用(Linux)或 SwitchToThread()(Windows)
  • 进入内核态:✅ - 必须进入内核态执行系统调用
  • 调用调度器:✅ - 主动调用内核调度器,将当前线程移出运行队列
  • CPU周期
    • 系统调用开销:约 1000-3000个周期
      • 用户态到内核态切换:~200-500周期
      • 系统调用处理:~300-1000周期
      • 内核态到用户态切换:~200-500周期
    • 调度器开销:约 2000-5000个周期
      • 调度器决策:~500-1500周期
      • 运行队列操作:~300-800周期
    • 上下文切换开销:约 3000-15000个周期(如果发生线程切换)
      • 寄存器保存/恢复:~1000-3000周期
      • TLB刷新:~500-2000周期
      • 缓存失效:~1500-10000周期
    • 总计:6000-23000个周期
  • 开销来源
    • 系统调用开销:用户态/内核态切换(主要开销之一)
    • 调度器开销:调度决策和运行队列操作
    • 上下文切换开销:如果调度器选择运行其他线程(主要开销)
    • 缓存失效:线程切换导致L1/L2缓存失效
  • 作用
    • 让调度器有机会运行其他线程
    • 避免长时间占用CPU核心
    • 在锁竞争激烈时,给持有锁的线程更多执行机会

3. 第三阶段:sleep_for() (count ≥ 200)

  • 功能:线程主动睡眠指定时间
  • 系统调用:✅ - 调用 nanosleep()futex() 系统调用(Linux)
  • 进入内核态:✅ - 必须进入内核态执行系统调用
  • 调用调度器:✅ - 将当前线程移出运行队列,放入等待队列
  • CPU周期
    • 系统调用开销:约 1000-3000个周期
      • 用户态到内核态切换:~200-500周期
      • 系统调用处理(设置定时器):~500-1500周期
      • 内核态到用户态切换:~200-500周期
    • 调度器开销:约 3000-8000个周期
      • 调度器决策:~500-1500周期
      • 运行队列操作:~500-1000周期
      • 等待队列操作:~500-1000周期
      • 定时器设置:~1500-4500周期
    • 上下文切换开销:约 7000-42000个周期(必然发生)
      • 寄存器保存/恢复:~1000-3000周期
      • TLB刷新:~500-2000周期
      • 缓存失效:~5500-37000周期(L1/L2/L3缓存)
    • 睡眠时间:count/100 微秒(例如 count=200 时睡眠2微秒)
    • 总计:11000-53000个周期 + 睡眠时间
  • 开销来源
    • 系统调用开销:用户态/内核态切换(主要开销之一)
    • 调度器开销:调度决策、队列操作、定时器管理(主要开销之一)
    • 上下文切换开销:线程必然被切换,寄存器、TLB、缓存全部失效(主要开销)
    • 定时器开销:内核定时器设置和管理
    • 唤醒开销:定时器到期后唤醒线程的开销(未计入上述周期)
  • 作用
    • 大幅降低CPU占用
    • 睡眠时间随 count 增长而增加(最大100微秒)
    • 适用于锁竞争非常激烈或持锁时间较长的情况

退避策略总结

阶段 循环次数 主要操作 系统调用 内核态 调度器 CPU周期/次 适用场景
阶段1 0-99 machine_pause(10) 100-4000 锁很快释放,短时间等待
阶段2 100-199 yield() 6000-23000 中等竞争,需要让出CPU
阶段3 200+ sleep_for() 11000-53000+ 高竞争或长持锁时间

开销来源对比

开销类型 阶段1 (machine_pause) 阶段2 (yield) 阶段3 (sleep_for)
CPU指令执行 ✅ 主要开销
系统调用 ✅ 主要开销之一 ✅ 主要开销之一
用户态/内核态切换 ✅ 主要开销之一 ✅ 主要开销之一
调度器调用 ✅ 主要开销之一 ✅ 主要开销之一
上下文切换 ⚠️ 可能发生 ✅ 必然发生
缓存失效 ⚠️ 可能发生 ✅ 必然发生
定时器管理 ✅ 额外开销

这种分级退避策略能够在锁快速释放时保持低延迟(阶段1,纯用户态,无系统调用开销),同时在竞争激烈时避免浪费CPU资源(阶段2和3,通过系统调用和调度器协作降低CPU占用)。

PAUSE指令与普通指令的区别

在自旋锁实现中,为什么使用 PAUSE 指令而不是普通的空操作指令(如 NOP)?两者在硬件层面的行为有显著差异:

1. 执行延迟差异

特性 PAUSE指令 普通指令(如NOP)
执行周期 约140个周期(Intel) 1个周期
流水线行为 主动暂停流水线 正常流水线执行
功耗 降低功耗 正常功耗

为什么PAUSE需要更多周期?

  • PAUSE 指令被设计为延迟执行,而不是快速完成
  • CPU会主动等待一段时间,让内存子系统有机会完成待处理的操作
  • 这140个周期是有意为之的延迟,而非性能缺陷

2. 内存顺序违规(Memory Order Violation)处理

普通指令的问题

1
2
3
4
// 使用普通循环的自旋锁
while (lock.load() == LOCKED) {
// 空循环或NOP - 可能导致内存顺序违规
}
  • 在超线程(Hyper-Threading)环境下,两个逻辑核心共享执行单元
  • 如果线程A在自旋等待,线程B在修改锁变量,CPU的乱序执行可能导致:
    • 线程A持续读取锁的旧值(缓存未更新)
    • 即使线程B已经释放锁,线程A仍无法感知
    • 造成虚假的自旋等待

PAUSE指令的解决方案

1
2
3
4
// 使用PAUSE的自旋锁
while (lock.load() == LOCKED) {
_mm_pause(); // 提示CPU这是自旋等待
}
  • PAUSE 指令向CPU发出信号:当前处于自旋等待状态
  • CPU会:
    • 暂停内存推测执行(Memory Speculation)
    • 刷新内存访问队列
    • 等待内存子系统完成待处理操作
  • 减少内存顺序违规,确保能及时看到锁状态变化

3. 超线程资源分配

普通指令

  • 两个超线程竞争相同的执行资源
  • 自旋线程占用大量执行单元,影响同核心另一个线程的性能
  • CPU无法区分”有用工作”和”忙等待”

PAUSE指令

  • CPU识别出这是自旋等待,而非实际工作
  • 在超线程环境下,优先将执行资源分配给同核心的另一个线程
  • 提高整体CPU利用率

4. 功耗管理

普通指令

  • CPU持续高速执行,功耗较高
  • 在自旋等待期间浪费能源

PAUSE指令

  • CPU可以降低执行单元的频率或暂停部分单元
  • 显著降低功耗(特别是在移动设备和服务器上)

5. 实际性能对比

1
2
3
4
5
6
7
8
9
10
11
12
13
// 方案A:普通空循环(不推荐)
void spin_with_nop() {
while (lock.load() == LOCKED) {
// 空循环或NOP
}
}

// 方案B:使用PAUSE(推荐)
void spin_with_pause() {
while (lock.load() == LOCKED) {
_mm_pause();
}
}

性能差异

  • 延迟感知:方案B能更快感知到锁释放(减少内存顺序违规)
  • 吞吐量:在超线程环境下,方案B的整体吞吐量更高
  • 功耗:方案B功耗显著更低
  • 单线程延迟:方案A可能略快(1周期 vs 140周期),但实际应用中差异可忽略

6. 为什么不能简单用空循环?

如果只是简单的空循环:

1
for (volatile int i = 0; i < 100; ++i); // 不推荐

问题:

  • ❌ 无法解决内存顺序违规
  • ❌ 无法优化超线程资源分配
  • ❌ 功耗较高
  • ❌ CPU无法识别这是自旋等待

总结

对比项 PAUSE指令 普通指令/NOP
执行周期 ~140周期(有意延迟) 1周期
内存顺序 ✅ 减少违规 ❌ 可能违规
超线程优化 ✅ 资源优先分配 ❌ 无优化
功耗 ✅ 降低 ❌ 正常
锁感知速度 ✅ 更快 ❌ 可能延迟
适用场景 ✅ 自旋等待 ❌ 不适合

结论PAUSE 指令虽然执行周期更长,但这是有意设计的延迟,用于优化自旋等待的整体性能。在自旋锁场景中,PAUSE 指令带来的收益(减少内存违规、优化资源分配、降低功耗)远大于其执行延迟的成本。

逻辑核心与执行单元

在理解超线程(Hyper-Threading)和PAUSE指令的作用时,需要明确逻辑核心执行单元的概念:

1. 物理核心(Physical Core)

物理核心是CPU中实际存在的、独立的处理单元,包含:

  • 执行单元(Execution Units):ALU(算术逻辑单元)、FPU(浮点单元)、加载/存储单元等
  • 寄存器文件(Register File):通用寄存器、浮点寄存器
  • 一级缓存(L1 Cache):指令缓存(L1I)和数据缓存(L1D)
  • 分支预测器(Branch Predictor)
  • 指令解码器(Instruction Decoder)
  • 重排序缓冲区(Reorder Buffer, ROB)

2. 逻辑核心(Logical Core)

逻辑核心是操作系统看到的”CPU核心”,在超线程技术中:

  • 一个物理核心可以对应两个逻辑核心
  • 每个逻辑核心有独立的寄存器文件程序计数器(PC)
  • 共享执行单元和其他硬件资源

示例

1
2
3
4
5
6
7
8
9
10
11
12
物理核心1
├── 逻辑核心0(线程0)
│ ├── 寄存器文件(独立)
│ └── 程序计数器(独立)
├── 逻辑核心1(线程1)
│ ├── 寄存器文件(独立)
│ └── 程序计数器(独立)
└── 共享资源
├── 执行单元(ALU、FPU等)
├── L1缓存
├── 分支预测器
└── 指令解码器

3. 执行单元(Execution Units)

执行单元是CPU中实际执行指令的硬件单元,包括:

执行单元类型 功能 示例指令
ALU(算术逻辑单元) 整数运算 ADD, SUB, AND, OR
FPU(浮点单元) 浮点运算 FADD, FMUL, FDIV
加载单元(Load Unit) 从内存/缓存加载数据 MOV, LOAD
存储单元(Store Unit) 将数据写入内存/缓存 STORE, MOV [mem]
分支单元(Branch Unit) 处理分支指令 JMP, CALL, RET
SIMD单元 向量运算 SSE, AVX 指令

4. 超线程的工作原理

超线程(Hyper-Threading)允许一个物理核心同时运行两个线程:

1
2
3
4
5
6
7
8
时间线示例(单物理核心,双逻辑核心):

时钟周期 | 逻辑核心0(线程A) | 逻辑核心1(线程B) | 执行单元使用情况
---------|------------------------|------------------------|------------------
1 | ADD指令(使用ALU) | 等待 | ALU: 线程A
2 | FPU指令(使用FPU) | LOAD指令(使用Load) | FPU: 线程A, Load: 线程B
3 | 等待(指令解码) | ADD指令(使用ALU) | ALU: 线程B
4 | STORE指令(使用Store) | FPU指令(使用FPU) | Store: 线程A, FPU: 线程B

关键点

  • 两个逻辑核心共享执行单元
  • 当一个线程的指令在等待(如等待内存访问)时,另一个线程可以使用空闲的执行单元
  • 这提高了硬件利用率,但两个线程会竞争相同的执行资源

5. 为什么PAUSE指令能优化超线程?

问题场景

1
2
3
4
5
6
7
// 线程A在逻辑核心0上自旋等待
while (lock.load() == LOCKED) {
// 空循环 - 持续占用执行单元
}

// 线程B在逻辑核心1上(同一物理核心)执行实际工作
do_real_work();

没有PAUSE的问题

  • 线程A的空循环持续占用ALU等执行单元
  • 线程B的指令无法使用这些执行单元
  • 两个逻辑核心竞争执行资源,整体性能下降

使用PAUSE的优化

1
2
3
4
5
6
7
// 线程A在逻辑核心0上自旋等待
while (lock.load() == LOCKED) {
_mm_pause(); // 提示CPU这是自旋等待
}

// 线程B在逻辑核心1上执行实际工作
do_real_work();

PAUSE的作用

  • CPU识别出这是自旋等待,而非实际工作
  • 优先将执行单元分配给线程B(执行实际工作)
  • 线程A的PAUSE指令执行时,让出执行资源
  • 提高整体CPU利用率

6. 执行单元的竞争与调度

执行单元调度策略(简化模型):

场景 线程A状态 线程B状态 执行单元分配
无PAUSE 自旋(空循环) 执行工作 竞争激烈,性能下降
有PAUSE 自旋(PAUSE) 执行工作 优先分配给线程B
双工作线程 执行工作 执行工作 公平分配,交替使用

实际效果

  • 使用PAUSE时,自旋线程对执行单元的占用减少
  • 同核心的另一个线程获得更多执行资源
  • 整体吞吐量提升(特别是在超线程环境下)

7. 物理核心 vs 逻辑核心 vs 执行单元

层级关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CPU芯片
└── 物理核心1
├── 逻辑核心0(操作系统看到的CPU 0)
│ └── 线程A运行在此
├── 逻辑核心1(操作系统看到的CPU 1)
│ └── 线程B运行在此
└── 共享执行单元
├── ALU(算术逻辑单元)
├── FPU(浮点单元)
├── Load/Store单元
└── 其他执行单元

物理核心2
├── 逻辑核心2(操作系统看到的CPU 2)
├── 逻辑核心3(操作系统看到的CPU 3)
└── 共享执行单元(独立于物理核心1)

关键区别

概念 数量关系 独立性 共享资源
物理核心 1个 完全独立 不共享
逻辑核心 1-2个/物理核心 部分独立(寄存器独立) 共享执行单元
执行单元 多个/物理核心 硬件资源 被逻辑核心共享

8. 实际应用示例

查看系统核心信息

1
2
3
4
5
6
7
8
9
# Linux系统
lscpu
# 输出示例:
# CPU(s): 8 # 逻辑核心数
# Thread(s) per core: 2 # 每个物理核心的逻辑核心数
# Core(s) per socket: 4 # 每个CPU插槽的物理核心数
# Socket(s): 1 # CPU插槽数
#
# 含义:1个物理插槽 × 4个物理核心 × 2个逻辑核心 = 8个逻辑核心

在代码中的体现

1
2
3
4
5
6
// 操作系统看到8个逻辑核心(CPU 0-7)
std::thread t1([]() { /* 运行在CPU 0或1 */ });
std::thread t2([]() { /* 运行在CPU 2或3 */ });

// 如果t1和t2运行在同一物理核心的不同逻辑核心上
// 它们会共享执行单元,PAUSE指令能优化资源分配

总结

  • 逻辑核心:操作系统看到的CPU核心,每个逻辑核心可以运行一个线程
  • 执行单元:实际执行指令的硬件单元(ALU、FPU等),被逻辑核心共享
  • 超线程:一个物理核心提供两个逻辑核心,共享执行单元
  • PAUSE指令:让CPU识别自旋等待,优先将执行单元分配给执行实际工作的线程

理解这些概念有助于理解为什么PAUSE指令在超线程环境下能提高整体性能。

参考资料

  1. TBB spin_barrier.h

1. NUMA概念

NUMA(Non-Uniform Memory Access,非统一内存访问)是一种计算机内存设计架构,主要用于多处理器系统中。

在NUMA架构中:

  • 每个CPU处理器都有本地内存(Local Memory),访问速度较快
  • 每个CPU也可以访问其他CPU的内存(Remote Memory),但访问速度较慢
  • 多个CPU和其本地内存组成一个NUMA节点(NUMA Node)
  • 系统通过NUMA拓扑来管理内存和CPU的分配

NUMA架构的优势:

  • 减少内存访问延迟(本地内存访问更快)
  • 提高系统整体性能
  • 支持更大规模的多处理器系统

NUMA架构的挑战:

  • 需要合理分配进程和内存到对应的NUMA节点
  • 跨节点访问内存会带来性能损失
  • 需要应用程序或系统管理员进行优化

2. CPU亲和性概念

CPU亲和性(CPU Affinity)是指将进程或线程绑定到特定的CPU核心上运行的机制。通过设置CPU亲和性,可以:

  1. 提高缓存命中率:进程始终运行在同一个CPU核心上,可以更好地利用CPU的L1/L2/L3缓存
  2. 减少进程迁移开销:避免进程在不同CPU核心间频繁迁移带来的性能损失
  3. 资源隔离:将不同进程绑定到不同CPU核心,实现资源隔离和负载均衡

CPU亲和性的类型:

  • 硬亲和性(Hard Affinity):强制绑定,进程/线程只能运行在指定的CPU核心上,调度器会严格遵循这个限制
    • 例如:如果绑定到CPU 0,进程/线程就只能调度到CPU 0上运行,不能调度到CPU 1或其他CPU核心上
    • 通过 sched_setaffinity()tasksetnumactl --cpunodebindcgroup cpuset.cpus 等方法设置
  • 软亲和性(Soft Affinity):偏好设置,系统会尽量将进程调度到指定的CPU核心,但不强制
    • 这是调度器的默认行为,在没有显式设置CPU亲和性时,调度器会尽量保持进程在同一个CPU核心上运行
    • 设置方法
      1. 默认方法:不设置任何CPU亲和性,直接运行程序,调度器自动实现软亲和性
      2. numactl:使用 numactl --preferred=<node> 只设置内存偏好,不设置CPU绑定(CPU使用默认软亲和性)
      3. nice值:通过 nice -n <value>renice <value> <pid> 调整进程优先级,间接影响调度器行为
      4. cgroup:使用 cpu.shares(v1)或 cpu.weight(v2)设置CPU时间权重,影响调度器分配
      5. **set_mempolicy()**:使用 MPOL_PREFERRED 策略设置内存分配偏好,CPU使用默认软亲和性
      6. SCHED_NORMAL策略:使用默认的CFS调度器(SCHED_NORMAL),自动实现软亲和性

注意事项:

  • CPU亲和性设置会影响操作系统的调度器行为
  • 过度绑定可能导致CPU负载不均衡
  • 即使通过硬亲和性强制绑定到单个CPU核心,进程仍可能创建多个线程,这些线程会在该CPU核心上通过时间片轮转的方式轮流调度执行

验证硬亲和性:

以下示例演示如何验证硬亲和性的效果。创建一个多线程程序:

affinity_test.cview raw
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
// affinity_test.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

#define NUM_THREADS 200

void* worker_thread(void* arg) {
int thread_id = *(int*)arg;
unsigned long long count = 0;

// 执行一些计算工作
for (int i = 0; i < 100000000; i++) {
count += i;
}

printf("Thread %d completed, count = %llu\n", thread_id, count);
return NULL;
}

int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];

printf("Creating %d threads...\n", NUM_THREADS);

for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, worker_thread, &thread_ids[i]);
}

for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}

printf("All threads completed\n");
return 0;
}

编译程序:

1
gcc -o affinity_test affinity_test.c -lpthread

测试1:绑定到单个CPU核心

1
taskset -c 0 ./affinity_test

使用Oracle Developer Studio Performance Analyzer观察:

Threads View示例输出:

affinity_test_threads_view_cpu0.txtview raw
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
Threads sorted by metric: Exclusive Total CPU Time

Exclusive

| Total CPU Time (sec.) | Name |
|-----------------------|-------------------------|
| 2.001 | <Total> |
| 0.010 | Process 1, Thread 1 |
| 0.010 | Process 1, Thread 2 |
| 0.010 | Process 1, Thread 3 |
| 0.010 | Process 1, Thread 4 |
| 0.010 | Process 1, Thread 5 |
| 0.010 | Process 1, Thread 6 |
| 0.010 | Process 1, Thread 7 |
| 0.010 | Process 1, Thread 8 |
| 0.010 | Process 1, Thread 9 |
| 0.010 | Process 1, Thread 10 |
| 0.010 | Process 1, Thread 11 |
| 0.010 | Process 1, Thread 12 |
| 0.010 | Process 1, Thread 13 |
| 0.010 | Process 1, Thread 14 |
| 0.010 | Process 1, Thread 15 |
| 0.010 | Process 1, Thread 16 |
| 0.010 | Process 1, Thread 17 |
| 0.010 | Process 1, Thread 18 |
| 0.010 | Process 1, Thread 19 |
| 0.010 | Process 1, Thread 20 |
| 0.010 | Process 1, Thread 21 |
| 0.010 | Process 1, Thread 22 |
| 0.010 | Process 1, Thread 23 |
| 0.010 | Process 1, Thread 24 |
| 0.010 | Process 1, Thread 25 |
| 0.010 | Process 1, Thread 26 |
| 0.010 | Process 1, Thread 27 |
| 0.010 | Process 1, Thread 28 |
| 0.010 | Process 1, Thread 29 |
| 0.010 | Process 1, Thread 30 |
| 0.010 | Process 1, Thread 31 |
| 0.010 | Process 1, Thread 32 |
| 0.010 | Process 1, Thread 33 |
| 0.010 | Process 1, Thread 34 |
| 0.010 | Process 1, Thread 35 |
| 0.010 | Process 1, Thread 36 |
| 0.010 | Process 1, Thread 37 |
| 0.010 | Process 1, Thread 38 |
| 0.010 | Process 1, Thread 39 |
| 0.010 | Process 1, Thread 40 |
| 0.010 | Process 1, Thread 41 |
| 0.010 | Process 1, Thread 42 |
| 0.010 | Process 1, Thread 43 |
| 0.010 | Process 1, Thread 44 |
| 0.010 | Process 1, Thread 45 |
| 0.010 | Process 1, Thread 46 |
| 0.010 | Process 1, Thread 47 |
| 0.010 | Process 1, Thread 48 |
| 0.010 | Process 1, Thread 49 |
| 0.010 | Process 1, Thread 50 |
| 0.010 | Process 1, Thread 51 |
| 0.010 | Process 1, Thread 52 |
| 0.010 | Process 1, Thread 53 |
| 0.010 | Process 1, Thread 54 |
| 0.010 | Process 1, Thread 55 |
| 0.010 | Process 1, Thread 56 |
| 0.010 | Process 1, Thread 57 |
| 0.010 | Process 1, Thread 58 |
| 0.010 | Process 1, Thread 59 |
| 0.010 | Process 1, Thread 60 |
| 0.010 | Process 1, Thread 61 |
| 0.010 | Process 1, Thread 62 |
| 0.010 | Process 1, Thread 63 |
| 0.010 | Process 1, Thread 64 |
| 0.010 | Process 1, Thread 65 |
| 0.010 | Process 1, Thread 66 |
| 0.010 | Process 1, Thread 67 |
| 0.010 | Process 1, Thread 68 |
| 0.010 | Process 1, Thread 69 |
| 0.010 | Process 1, Thread 70 |
| 0.010 | Process 1, Thread 71 |
| 0.010 | Process 1, Thread 72 |
| 0.010 | Process 1, Thread 73 |
| 0.010 | Process 1, Thread 74 |
| 0.010 | Process 1, Thread 75 |
| 0.010 | Process 1, Thread 76 |
| 0.010 | Process 1, Thread 77 |
| 0.010 | Process 1, Thread 78 |
| 0.010 | Process 1, Thread 79 |
| 0.010 | Process 1, Thread 80 |
| 0.010 | Process 1, Thread 81 |
| 0.010 | Process 1, Thread 82 |
| 0.010 | Process 1, Thread 83 |
| 0.010 | Process 1, Thread 84 |
| 0.010 | Process 1, Thread 85 |
| 0.010 | Process 1, Thread 86 |
| 0.010 | Process 1, Thread 87 |
| 0.010 | Process 1, Thread 88 |
| 0.010 | Process 1, Thread 89 |
| 0.010 | Process 1, Thread 90 |
| 0.010 | Process 1, Thread 91 |
| 0.010 | Process 1, Thread 92 |
| 0.010 | Process 1, Thread 93 |
| 0.010 | Process 1, Thread 94 |
| 0.010 | Process 1, Thread 95 |
| 0.010 | Process 1, Thread 96 |
| 0.010 | Process 1, Thread 97 |
| 0.010 | Process 1, Thread 98 |
| 0.010 | Process 1, Thread 99 |
| 0.010 | Process 1, Thread 100 |
| 0.010 | Process 1, Thread 101 |
| 0.010 | Process 1, Thread 102 |
| 0.010 | Process 1, Thread 103 |
| 0.010 | Process 1, Thread 104 |
| 0.010 | Process 1, Thread 105 |
| 0.010 | Process 1, Thread 106 |
| 0.010 | Process 1, Thread 107 |
| 0.010 | Process 1, Thread 108 |
| 0.010 | Process 1, Thread 109 |
| 0.010 | Process 1, Thread 110 |
| 0.010 | Process 1, Thread 111 |
| 0.010 | Process 1, Thread 112 |
| 0.010 | Process 1, Thread 113 |
| 0.010 | Process 1, Thread 114 |
| 0.010 | Process 1, Thread 115 |
| 0.010 | Process 1, Thread 116 |
| 0.010 | Process 1, Thread 117 |
| 0.010 | Process 1, Thread 118 |
| 0.010 | Process 1, Thread 119 |
| 0.010 | Process 1, Thread 120 |
| 0.010 | Process 1, Thread 121 |
| 0.010 | Process 1, Thread 122 |
| 0.010 | Process 1, Thread 123 |
| 0.010 | Process 1, Thread 124 |
| 0.010 | Process 1, Thread 125 |
| 0.010 | Process 1, Thread 126 |
| 0.010 | Process 1, Thread 127 |
| 0.010 | Process 1, Thread 128 |
| 0.010 | Process 1, Thread 129 |
| 0.010 | Process 1, Thread 130 |
| 0.010 | Process 1, Thread 131 |
| 0.010 | Process 1, Thread 132 |
| 0.010 | Process 1, Thread 133 |
| 0.010 | Process 1, Thread 134 |
| 0.010 | Process 1, Thread 135 |
| 0.010 | Process 1, Thread 136 |
| 0.010 | Process 1, Thread 137 |
| 0.010 | Process 1, Thread 138 |
| 0.010 | Process 1, Thread 139 |
| 0.010 | Process 1, Thread 140 |
| 0.010 | Process 1, Thread 141 |
| 0.010 | Process 1, Thread 142 |
| 0.010 | Process 1, Thread 143 |
| 0.010 | Process 1, Thread 144 |
| 0.010 | Process 1, Thread 145 |
| 0.010 | Process 1, Thread 146 |
| 0.010 | Process 1, Thread 147 |
| 0.010 | Process 1, Thread 148 |
| 0.010 | Process 1, Thread 149 |
| 0.010 | Process 1, Thread 150 |
| 0.010 | Process 1, Thread 151 |
| 0.010 | Process 1, Thread 152 |
| 0.010 | Process 1, Thread 153 |
| 0.010 | Process 1, Thread 154 |
| 0.010 | Process 1, Thread 155 |
| 0.010 | Process 1, Thread 156 |
| 0.010 | Process 1, Thread 157 |
| 0.010 | Process 1, Thread 158 |
| 0.010 | Process 1, Thread 159 |
| 0.010 | Process 1, Thread 160 |
| 0.010 | Process 1, Thread 161 |
| 0.010 | Process 1, Thread 162 |
| 0.010 | Process 1, Thread 163 |
| 0.010 | Process 1, Thread 164 |
| 0.010 | Process 1, Thread 165 |
| 0.010 | Process 1, Thread 166 |
| 0.010 | Process 1, Thread 167 |
| 0.010 | Process 1, Thread 168 |
| 0.010 | Process 1, Thread 169 |
| 0.010 | Process 1, Thread 170 |
| 0.010 | Process 1, Thread 171 |
| 0.010 | Process 1, Thread 172 |
| 0.010 | Process 1, Thread 173 |
| 0.010 | Process 1, Thread 174 |
| 0.010 | Process 1, Thread 175 |
| 0.010 | Process 1, Thread 176 |
| 0.010 | Process 1, Thread 177 |
| 0.010 | Process 1, Thread 178 |
| 0.010 | Process 1, Thread 179 |
| 0.010 | Process 1, Thread 180 |
| 0.010 | Process 1, Thread 181 |
| 0.010 | Process 1, Thread 182 |
| 0.010 | Process 1, Thread 183 |
| 0.010 | Process 1, Thread 184 |
| 0.010 | Process 1, Thread 185 |
| 0.010 | Process 1, Thread 186 |
| 0.010 | Process 1, Thread 187 |
| 0.010 | Process 1, Thread 188 |
| 0.010 | Process 1, Thread 189 |
| 0.010 | Process 1, Thread 190 |
| 0.010 | Process 1, Thread 191 |
| 0.010 | Process 1, Thread 192 |
| 0.010 | Process 1, Thread 193 |
| 0.010 | Process 1, Thread 194 |
| 0.010 | Process 1, Thread 195 |
| 0.010 | Process 1, Thread 196 |
| 0.010 | Process 1, Thread 197 |
| 0.010 | Process 1, Thread 198 |
| 0.010 | Process 1, Thread 199 |
| 0.010 | Process 1, Thread 200 |

CPU View示例输出:

affinity_test_cpu_view_cpu0.txtview raw
1
2
3
4
5
6
7
8
CPUs sorted by metric: Exclusive Total CPU Time

Exclusive

| Total CPU Time (sec.) | Name |
|-----------------------|----------|
| 2.001 | <Total> |
| 2.001 | CPU 0 |

观察结果: 所有200个线程都运行在CPU 0上,CPU 0的利用率为100%,其他CPU未被使用。这证明了硬亲和性:即使创建了多个线程,它们也只能在绑定的CPU核心上运行。

测试2:绑定到两个CPU核心

1
taskset -c 0-1 ./affinity_test

Threads View示例输出:

affinity_test_threads_view_cpu01.txtview raw
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
Threads sorted by metric: Exclusive Total CPU Time

Exclusive

| Total CPU Time (sec.) | Name |
|-----------------------|-------------------------|
| 2.152 | <Total> |
| 0.020 | Process 1, Thread 18 |
| 0.020 | Process 1, Thread 22 |
| 0.020 | Process 1, Thread 24 |
| 0.020 | Process 1, Thread 50 |
| 0.020 | Process 1, Thread 64 |
| 0.020 | Process 1, Thread 66 |
| 0.020 | Process 1, Thread 68 |
| 0.020 | Process 1, Thread 97 |
| 0.020 | Process 1, Thread 100 |
| 0.020 | Process 1, Thread 104 |
| 0.020 | Process 1, Thread 117 |
| 0.020 | Process 1, Thread 123 |
| 0.020 | Process 1, Thread 128 |
| 0.020 | Process 1, Thread 130 |
| 0.020 | Process 1, Thread 135 |
| 0.010 | Process 1, Thread 1 |
| 0.010 | Process 1, Thread 2 |
| 0.010 | Process 1, Thread 3 |
| 0.010 | Process 1, Thread 4 |
| 0.010 | Process 1, Thread 5 |
| 0.010 | Process 1, Thread 6 |
| 0.010 | Process 1, Thread 7 |
| 0.010 | Process 1, Thread 8 |
| 0.010 | Process 1, Thread 9 |
| 0.010 | Process 1, Thread 10 |
| 0.010 | Process 1, Thread 11 |
| 0.010 | Process 1, Thread 12 |
| 0.010 | Process 1, Thread 13 |
| 0.010 | Process 1, Thread 14 |
| 0.010 | Process 1, Thread 15 |
| 0.010 | Process 1, Thread 16 |
| 0.010 | Process 1, Thread 17 |
| 0.010 | Process 1, Thread 19 |
| 0.010 | Process 1, Thread 20 |
| 0.010 | Process 1, Thread 21 |
| 0.010 | Process 1, Thread 23 |
| 0.010 | Process 1, Thread 25 |
| 0.010 | Process 1, Thread 26 |
| 0.010 | Process 1, Thread 27 |
| 0.010 | Process 1, Thread 28 |
| 0.010 | Process 1, Thread 29 |
| 0.010 | Process 1, Thread 30 |
| 0.010 | Process 1, Thread 31 |
| 0.010 | Process 1, Thread 32 |
| 0.010 | Process 1, Thread 33 |
| 0.010 | Process 1, Thread 34 |
| 0.010 | Process 1, Thread 35 |
| 0.010 | Process 1, Thread 36 |
| 0.010 | Process 1, Thread 37 |
| 0.010 | Process 1, Thread 38 |
| 0.010 | Process 1, Thread 39 |
| 0.010 | Process 1, Thread 40 |
| 0.010 | Process 1, Thread 41 |
| 0.010 | Process 1, Thread 42 |
| 0.010 | Process 1, Thread 43 |
| 0.010 | Process 1, Thread 44 |
| 0.010 | Process 1, Thread 45 |
| 0.010 | Process 1, Thread 46 |
| 0.010 | Process 1, Thread 47 |
| 0.010 | Process 1, Thread 48 |
| 0.010 | Process 1, Thread 49 |
| 0.010 | Process 1, Thread 51 |
| 0.010 | Process 1, Thread 52 |
| 0.010 | Process 1, Thread 53 |
| 0.010 | Process 1, Thread 54 |
| 0.010 | Process 1, Thread 55 |
| 0.010 | Process 1, Thread 56 |
| 0.010 | Process 1, Thread 57 |
| 0.010 | Process 1, Thread 58 |
| 0.010 | Process 1, Thread 59 |
| 0.010 | Process 1, Thread 60 |
| 0.010 | Process 1, Thread 61 |
| 0.010 | Process 1, Thread 62 |
| 0.010 | Process 1, Thread 63 |
| 0.010 | Process 1, Thread 65 |
| 0.010 | Process 1, Thread 67 |
| 0.010 | Process 1, Thread 69 |
| 0.010 | Process 1, Thread 70 |
| 0.010 | Process 1, Thread 71 |
| 0.010 | Process 1, Thread 72 |
| 0.010 | Process 1, Thread 73 |
| 0.010 | Process 1, Thread 74 |
| 0.010 | Process 1, Thread 75 |
| 0.010 | Process 1, Thread 76 |
| 0.010 | Process 1, Thread 77 |
| 0.010 | Process 1, Thread 78 |
| 0.010 | Process 1, Thread 79 |
| 0.010 | Process 1, Thread 80 |
| 0.010 | Process 1, Thread 81 |
| 0.010 | Process 1, Thread 82 |
| 0.010 | Process 1, Thread 83 |
| 0.010 | Process 1, Thread 84 |
| 0.010 | Process 1, Thread 85 |
| 0.010 | Process 1, Thread 86 |
| 0.010 | Process 1, Thread 87 |
| 0.010 | Process 1, Thread 88 |
| 0.010 | Process 1, Thread 89 |
| 0.010 | Process 1, Thread 90 |
| 0.010 | Process 1, Thread 91 |
| 0.010 | Process 1, Thread 92 |
| 0.010 | Process 1, Thread 93 |
| 0.010 | Process 1, Thread 94 |
| 0.010 | Process 1, Thread 95 |
| 0.010 | Process 1, Thread 96 |
| 0.010 | Process 1, Thread 98 |
| 0.010 | Process 1, Thread 99 |
| 0.010 | Process 1, Thread 101 |
| 0.010 | Process 1, Thread 102 |
| 0.010 | Process 1, Thread 103 |
| 0.010 | Process 1, Thread 105 |
| 0.010 | Process 1, Thread 106 |
| 0.010 | Process 1, Thread 107 |
| 0.010 | Process 1, Thread 108 |
| 0.010 | Process 1, Thread 109 |
| 0.010 | Process 1, Thread 110 |
| 0.010 | Process 1, Thread 111 |
| 0.010 | Process 1, Thread 112 |
| 0.010 | Process 1, Thread 113 |
| 0.010 | Process 1, Thread 114 |
| 0.010 | Process 1, Thread 115 |
| 0.010 | Process 1, Thread 116 |
| 0.010 | Process 1, Thread 118 |
| 0.010 | Process 1, Thread 119 |
| 0.010 | Process 1, Thread 120 |
| 0.010 | Process 1, Thread 121 |
| 0.010 | Process 1, Thread 122 |
| 0.010 | Process 1, Thread 124 |
| 0.010 | Process 1, Thread 125 |
| 0.010 | Process 1, Thread 126 |
| 0.010 | Process 1, Thread 127 |
| 0.010 | Process 1, Thread 129 |
| 0.010 | Process 1, Thread 131 |
| 0.010 | Process 1, Thread 132 |
| 0.010 | Process 1, Thread 133 |
| 0.010 | Process 1, Thread 134 |
| 0.010 | Process 1, Thread 136 |
| 0.010 | Process 1, Thread 137 |
| 0.010 | Process 1, Thread 138 |
| 0.010 | Process 1, Thread 139 |
| 0.010 | Process 1, Thread 140 |
| 0.010 | Process 1, Thread 141 |
| 0.010 | Process 1, Thread 142 |
| 0.010 | Process 1, Thread 143 |
| 0.010 | Process 1, Thread 144 |
| 0.010 | Process 1, Thread 145 |
| 0.010 | Process 1, Thread 146 |
| 0.010 | Process 1, Thread 147 |
| 0.010 | Process 1, Thread 148 |
| 0.010 | Process 1, Thread 149 |
| 0.010 | Process 1, Thread 150 |
| 0.010 | Process 1, Thread 151 |
| 0.010 | Process 1, Thread 152 |
| 0.010 | Process 1, Thread 153 |
| 0.010 | Process 1, Thread 154 |
| 0.010 | Process 1, Thread 155 |
| 0.010 | Process 1, Thread 156 |
| 0.010 | Process 1, Thread 157 |
| 0.010 | Process 1, Thread 158 |
| 0.010 | Process 1, Thread 159 |
| 0.010 | Process 1, Thread 160 |
| 0.010 | Process 1, Thread 161 |
| 0.010 | Process 1, Thread 162 |
| 0.010 | Process 1, Thread 163 |
| 0.010 | Process 1, Thread 164 |
| 0.010 | Process 1, Thread 165 |
| 0.010 | Process 1, Thread 166 |
| 0.010 | Process 1, Thread 167 |
| 0.010 | Process 1, Thread 168 |
| 0.010 | Process 1, Thread 169 |
| 0.010 | Process 1, Thread 170 |
| 0.010 | Process 1, Thread 171 |
| 0.010 | Process 1, Thread 172 |
| 0.010 | Process 1, Thread 173 |
| 0.010 | Process 1, Thread 174 |
| 0.010 | Process 1, Thread 175 |
| 0.010 | Process 1, Thread 176 |
| 0.010 | Process 1, Thread 177 |
| 0.010 | Process 1, Thread 178 |
| 0.010 | Process 1, Thread 179 |
| 0.010 | Process 1, Thread 180 |
| 0.010 | Process 1, Thread 181 |
| 0.010 | Process 1, Thread 182 |
| 0.010 | Process 1, Thread 183 |
| 0.010 | Process 1, Thread 184 |
| 0.010 | Process 1, Thread 185 |
| 0.010 | Process 1, Thread 186 |
| 0.010 | Process 1, Thread 187 |
| 0.010 | Process 1, Thread 188 |
| 0.010 | Process 1, Thread 189 |
| 0.010 | Process 1, Thread 190 |
| 0.010 | Process 1, Thread 191 |
| 0.010 | Process 1, Thread 192 |
| 0.010 | Process 1, Thread 193 |
| 0.010 | Process 1, Thread 194 |
| 0.010 | Process 1, Thread 195 |
| 0.010 | Process 1, Thread 196 |
| 0.010 | Process 1, Thread 197 |
| 0.010 | Process 1, Thread 198 |
| 0.010 | Process 1, Thread 199 |
| 0.010 | Process 1, Thread 200 |

CPU View示例输出:

affinity_test_cpu_view_cpu01.txtview raw
1
2
3
4
5
6
7
8
9
CPUs sorted by metric: Exclusive Total CPU Time

Exclusive

| Total CPU Time (sec.) | Name |
|-----------------------|----------|
| 2.152 | <Total> |
| 1.591 | CPU 0 |
| 0.560 | CPU 1 |

观察结果: 200个线程被分配到CPU 0和CPU 1上,两个CPU的利用率都是100%,其他CPU未被使用。这证明了硬亲和性限制了线程只能在指定的CPU核心范围内运行。

结论: 硬亲和性确实强制限制了线程只能在绑定的CPU核心上运行,即使创建了多个线程,它们也无法使用绑定范围之外的CPU核心。

并行库适配说明:

CPU绑定(如 tasksetsched_setaffinitynumactl --cpunodebind)是硬亲和性(强制性的),能有效限制并行库的线程数。例如,TBB(Threading Building Blocks)在初始化时会根据操作系统报告的硬件资源(通过 sysconf(_SC_NPROCESSORS_ONLN) 等API)来决定线程池大小和调度策略。如果使用 tasksetnumactl 将进程绑定到部分CPU核心,TBB只会看到这些CPU核心,从而自动调整线程池大小。这意味着通过限制可见的CPU资源,可以间接控制并行库的行为。

NUMA架构下CPU亲和性的特殊性:

在NUMA架构系统中,CPU亲和性具有特殊的重要性:

  1. 内存访问性能依赖CPU位置

    • 进程运行在节点A的CPU上,如果内存分配在节点B,会产生远程内存访问,性能显著下降
    • 因此,NUMA架构下的CPU亲和性设置必须配合内存分配策略,才能获得最佳性能
  2. 节点级别的CPU绑定

    • 在NUMA系统中,通常以NUMA节点为单位进行CPU绑定,而不是单个CPU核心
    • 绑定到某个NUMA节点的CPU意味着可以使用该节点的所有CPU核心,同时配合该节点的本地内存
  3. CPU亲和性与内存策略的协同

    • 仅设置CPU亲和性而不设置内存策略可能导致远程内存访问
    • NUMA优化需要同时考虑CPU绑定和内存分配策略

总结:NUMA架构是CPU亲和性在多处理器系统中的特殊应用场景。在NUMA系统中,CPU亲和性不仅影响CPU调度,还直接影响内存访问性能,因此需要与内存分配策略协同使用。

3. CPU亲和性的设置

方法对比总览

方法 易用性 NUMA感知 内存控制 CPU绑定类型 适用场景
taskset ⭐⭐⭐⭐⭐ ✅ 硬亲和性 简单CPU绑定
sched_setaffinity ⭐⭐⭐ ✅ 硬亲和性 程序内部控制
cgroup ⭐⭐ ✅ 硬亲和性 系统级资源管理
numactl ⭐⭐⭐⭐ ✅ 硬亲和性 NUMA优化(详见NUMA章节)

方法一:taskset(通用CPU绑定)

taskset 是一个用于设置或查看进程CPU亲和性的命令行工具,适用于所有系统(包括非NUMA系统)。

基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看进程的CPU亲和性
taskset -p <pid>
taskset -cp <pid> # 更易读的格式

# 设置进程的CPU亲和性(使用CPU掩码)
taskset -p 0x3 <pid> # 绑定到CPU 0和1(二进制:11)

# 使用CPU列表格式
taskset -cp 0,1,2,3 <pid> # 绑定到CPU 0,1,2,3

# 启动新进程并设置CPU亲和性
taskset -c 0-3 ./your_program # 绑定到CPU 0-3
taskset -c 0,2,4,6 ./your_program # 绑定到CPU 0,2,4,6

taskset的特点:

  • ✅ 简单易用,命令行工具
  • ✅ 直接指定CPU编号,精确控制
  • ✅ CPU绑定是硬亲和性(强制绑定)
  • ❌ 不感知NUMA拓扑结构
  • ❌ 无法控制内存分配策略

适用场景:

  • 非NUMA系统(单节点系统)
  • 只需要简单的CPU绑定,不关心内存位置
  • 系统没有NUMA架构

方法二:sched_setaffinity系统调用

sched_setaffinity() 是Linux系统提供的系统调用,用于设置进程或线程的CPU亲和性。这是 taskset 命令的底层实现。

C语言示例:

sched_setaffinity.cview raw
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
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
cpu_set_t cpuset;
CPU_ZERO(&cpuset);

// 设置CPU亲和性:绑定到CPU 0和1
CPU_SET(0, &cpuset);
CPU_SET(1, &cpuset);

// 设置当前进程的CPU亲和性
if (sched_setaffinity(0, sizeof(cpuset), &cpuset) != 0) {
perror("sched_setaffinity failed");
return 1;
}

// 验证设置结果
CPU_ZERO(&cpuset);
if (sched_getaffinity(0, sizeof(cpuset), &cpuset) != 0) {
perror("sched_getaffinity failed");
return 1;
}

printf("进程绑定到CPU: ");
for (int i = 0; i < CPU_SETSIZE; i++) {
if (CPU_ISSET(i, &cpuset)) {
printf("%d ", i);
}
}
printf("\n");

return 0;
}

多线程示例:

sched_setaffinity_threads.cview raw
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
43
44
#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define NUM_THREADS 4

void *worker_thread(void *arg) {
int thread_id = *(int *)arg;
cpu_set_t cpuset;
CPU_ZERO(&cpuset);

// 每个线程绑定到不同的CPU
CPU_SET(thread_id, &cpuset);

// 设置线程的CPU亲和性
if (pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset) != 0) {
perror("pthread_setaffinity_np failed");
return NULL;
}

// 获取当前运行的CPU
int cpu = sched_getcpu();
printf("线程 %d 运行在CPU %d\n", thread_id, cpu);

return NULL;
}

int main() {
pthread_t threads[NUM_THREADS];
int thread_ids[NUM_THREADS];

for (int i = 0; i < NUM_THREADS; i++) {
thread_ids[i] = i;
pthread_create(&threads[i], NULL, worker_thread, &thread_ids[i]);
}

for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}

return 0;
}

编译命令:

1
gcc -o sched_affinity sched_affinity.c -lpthread

API说明:

  • sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask):设置进程的CPU亲和性
    • pid = 0:设置当前进程
    • pid > 0:设置指定进程
  • sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *mask):获取进程的CPU亲和性
  • pthread_setaffinity_np(pthread_t thread, size_t cpusetsize, const cpu_set_t *mask):设置线程的CPU亲和性
  • pthread_getaffinity_np(pthread_t thread, size_t cpusetsize, cpu_set_t *mask):获取线程的CPU亲和性

sched_setaffinity的特点:

  • ✅ 系统级API,功能强大
  • ✅ 可以在程序内部精确控制CPU绑定
  • ✅ 支持进程和线程级别的绑定
  • ✅ CPU绑定是硬亲和性(强制绑定)
  • ❌ 需要编写代码,不如命令行工具方便
  • ❌ 不感知NUMA拓扑

使用场景:

  • 需要在程序运行时动态调整CPU绑定
  • 需要为不同线程设置不同的CPU亲和性
  • 需要精确控制CPU绑定的应用程序

方法三:cgroup(系统级资源管理)

cgroup是Linux内核提供的资源管理机制,可以通过cgroup v1或cgroup v2来限制进程组的CPU使用。

cgroup v1方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建cgroup
sudo mkdir -p /sys/fs/cgroup/cpuset/mygroup

# 设置可用的CPU核心(例如CPU 0-3)
echo "0-3" | sudo tee /sys/fs/cgroup/cpuset/mygroup/cpuset.cpus

# 设置内存节点(NUMA节点,如果系统支持)
echo "0" | sudo tee /sys/fs/cgroup/cpuset/mygroup/cpuset.mems

# 将进程添加到cgroup
echo <pid> | sudo tee /sys/fs/cgroup/cpuset/mygroup/cgroup.procs

# 或者启动新进程
sudo cgexec -g cpuset:mygroup ./your_program

cgroup v2方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 挂载cgroup v2(如果未挂载)
sudo mount -t cgroup2 none /sys/fs/cgroup2

# 创建cgroup
sudo mkdir -p /sys/fs/cgroup2/mygroup

# 设置CPU范围(例如CPU 0-3)
echo "0-3" | sudo tee /sys/fs/cgroup2/mygroup/cpuset.cpus

# 设置内存节点(如果系统支持)
echo "0" | sudo tee /sys/fs/cgroup2/mygroup/cpuset.mems

# 将进程添加到cgroup
echo <pid> | sudo tee /sys/fs/cgroup2/mygroup/cgroup.procs

systemd使用cgroup:

1
2
3
4
5
# 创建systemd服务单元文件
sudo systemd-run --unit=myapp --scope \
--property=CPUSetCPUs=0-3 \
--property=CPUSetMems=0 \
./your_program

cgroup的特点:

  • ✅ 系统级资源管理,功能强大
  • ✅ 支持NUMA感知(可以设置内存节点)
  • ✅ 可以同时管理CPU和内存
  • ✅ 支持进程组管理
  • 硬亲和性(强制绑定),保证进程组只在指定CPU上运行
  • ⚠️ 需要root权限
  • ⚠️ 配置相对复杂

使用场景:

  • 容器化环境(Docker、Kubernetes等)
  • 需要同时管理多个进程的资源分配
  • 需要持久化的资源限制配置
  • 系统级资源管理需求

4. NUMA的设置

查询和监控NUMA信息

1. 查看NUMA节点拓扑

使用 numactl --hardware 命令查看NUMA硬件信息:

1
2
3
4
5
6
7
8
9
10
11
12
$ numactl --hardware
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 8 9 10 11
node 0 size: 16384 MB
node 0 free: 1024 MB
node 1 cpus: 4 5 6 7 12 13 14 15
node 1 size: 16384 MB
node 1 free: 2048 MB
node distances:
node 0 1
0: 10 21
1: 21 10

结果说明:

  • available: 2 nodes (0-1): 系统有2个NUMA节点,编号为0和1
  • node 0 cpus: 0 1 2 3 8 9 10 11: 节点0包含的CPU核心编号
  • node 0 size: 16384 MB: 节点0的内存大小(16GB)
  • node 0 free: 1024 MB: 节点0的可用内存
  • node distances: 节点间访问距离矩阵
    • 节点0到节点0的距离是10(本地访问)
    • 节点0到节点1的距离是21(远程访问,距离越大性能越差)

2. 查看CPU和NUMA节点的映射关系

使用 lscpu 命令:

1
2
3
4
$ lscpu | grep -i numa
NUMA node(s): 2
NUMA node0 CPU(s): 0-3,8-11
NUMA node1 CPU(s): 4-7,12-15

3. 查看NUMA统计信息

使用 numastat 查看NUMA统计信息:

1
2
3
4
5
6
7
8
$ numastat
node0 node1
numa_hit 12345678901 23456789012
numa_miss 123456789 234567890
numa_foreign 234567890 123456789
interleave_hit 12345 12345
local_node 12345678901 23456789012
other_node 123456789 234567890

4. 查看进程的NUMA策略

使用 numactl --show 查看当前进程的NUMA策略:

1
2
3
4
5
6
7
$ numactl --show
policy: default
preferred node: current
physcpubind: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
cpubind: 0 1
nodebind: 0 1
membind: 0 1

5. 查看系统NUMA节点详细信息

查看 /sys/devices/system/node/ 目录:

1
2
3
4
5
6
7
8
9
10
$ ls /sys/devices/system/node/
node0 node1

$ cat /sys/devices/system/node/node0/cpulist
0-3,8-11

$ cat /sys/devices/system/node/node0/meminfo
Node 0 MemTotal: 16777216 kB
Node 0 MemFree: 1048576 kB
Node 0 MemUsed: 15728640 kB

numactl:NUMA架构下的CPU亲和性和内存策略工具

numactl 是NUMA架构优化的首选工具,可以同时控制CPU和内存分配策略。

基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 将进程绑定到节点0的CPU和内存(CPU是硬亲和性,内存是强制绑定)
numactl --cpunodebind=0 --membind=0 ./your_program

# 使用节点0的CPU(硬亲和性),但允许使用所有节点的内存
numactl --cpunodebind=0 ./your_program

# 使用节点0和1的CPU(硬亲和性),优先使用节点0的内存(偏好设置)
numactl --cpunodebind=0,1 --preferred=0 ./your_program

# 只绑定内存到节点0(强制绑定),CPU可以使用所有节点
numactl --membind=0 ./your_program

# 交错分配内存到多个节点
numactl --interleave=all ./your_program

numactl选项说明:

选项 CPU绑定类型 内存绑定类型 说明
--cpunodebind=<nodes> 硬亲和性(强制绑定) - 进程只能运行在指定节点的CPU上
--membind=<nodes> - 强制绑定 内存只能从指定节点分配
--preferred=<node> - ⚠️ 偏好设置 优先从指定节点分配内存,但允许从其他节点分配
--interleave=<nodes> - ⚠️ 交错分配 在指定节点间轮询分配内存
--localalloc - ⚠️ 本地优先 优先在本地节点分配内存

详细说明:

  • --cpunodebind=<nodes>:将进程绑定到指定NUMA节点的CPU(硬亲和性,强制绑定)
    • 示例:--cpunodebind=0 绑定到节点0的所有CPU
    • 示例:--cpunodebind=0,1 绑定到节点0和1的所有CPU
  • --membind=<nodes>:设置内存分配策略为强制绑定到指定节点(强制)
    • 示例:--membind=0 内存只能从节点0分配
    • 如果节点0内存不足,进程可能无法运行
  • --preferred=<node>:设置内存分配偏好节点(偏好设置)
    • 示例:--preferred=0 优先从节点0分配内存
    • 如果节点0内存不足,允许从其他节点分配
  • --interleave=<nodes>:在多个节点间交错分配内存
    • 示例:--interleave=0,1 在节点0和1间轮询分配
    • 适用于需要均匀使用多个节点内存的场景

taskset与numactl的区别:

特性 taskset numactl
主要功能 CPU亲和性设置 NUMA节点和内存策略管理
CPU绑定 ✅ 支持(基于CPU编号) ✅ 支持(基于NUMA节点)
内存管理 ❌ 不支持 ✅ 支持(内存节点绑定)
NUMA感知 ❌ 不感知NUMA拓扑 ✅ 完全NUMA感知
使用场景 简单的CPU绑定需求 NUMA架构优化

关键区别:

  1. CPU绑定方式不同

    • taskset:直接指定CPU编号(如CPU 0, 1, 2, 3)
    • numactl:基于NUMA节点指定(如节点0,自动包含该节点的所有CPU)
  2. 内存管理能力

    • taskset无法控制内存分配,进程可能从任意NUMA节点分配内存
    • numactl:可以控制内存分配策略,确保内存分配在特定NUMA节点
  3. NUMA架构优化

    • taskset:在NUMA系统中,即使绑定了CPU,内存仍可能从远程节点分配,导致性能问题
    • numactl:可以同时绑定CPU和内存,确保本地内存访问,获得最佳性能

实际示例对比:

1
2
3
4
5
6
7
# 使用taskset:只绑定CPU,不控制内存
taskset -c 0-3 ./program
# 问题:CPU在节点0,但内存可能从节点1分配(远程访问,性能差)

# 使用numactl:同时绑定CPU和内存
numactl --cpunodebind=0 --membind=0 ./program
# 优势:CPU和内存都在节点0(本地访问,性能好)

NUMA函数和命令对比

下表列出了常用的NUMA相关函数和命令,以及它们的作用和绑定类型:

函数/命令 作用 绑定类型 说明
numa_alloc_onnode() 在指定NUMA节点分配内存 强制保证 内存一定分配在指定节点上,失败返回NULL
numa_sched_setaffinity() 设置进程/线程的CPU亲和性 硬亲和性 进程/线程只能运行在指定的CPU核心上
numactl --cpunodebind 绑定进程到指定节点的CPU 硬亲和性 进程只能运行在指定节点的CPU上
numactl --membind 绑定进程的内存分配策略 强制绑定 内存只能从指定节点分配,失败则进程无法运行
numa_run_on_node() 绑定进程到指定节点(CPU+内存) ⚠️ 混合 CPU绑定是硬亲和性,内存分配是偏好设置
numactl --preferred 设置内存分配偏好节点 ⚠️ 偏好设置 优先从指定节点分配,但允许从其他节点分配
malloc() / calloc() 普通内存分配 ⚠️ 受策略影响 numa_run_on_node()后,会优先在绑定节点分配,但不保证
set_mempolicy() 设置内存分配策略 取决于策略类型 MPOL_BIND强制,MPOL_PREFERRED偏好,MPOL_INTERLEAVE交错
numa_node_to_cpus() 获取节点的CPU列表 查询函数 仅查询,不设置任何策略
get_mempolicy() 获取内存策略 查询函数 仅查询,不设置任何策略

关键区别说明:

  1. 强制保证 vs 偏好设置

    • 强制保证(硬亲和性):系统会严格遵循设置,如果无法满足(如内存不足),操作会失败
    • 偏好设置(软亲和性):系统会尽量满足设置,但在资源不足时允许从其他节点分配,不会失败
  2. numa_run_on_node()的特殊性

    • CPU绑定是强制的(硬亲和性):进程只能运行在指定节点的CPU上
    • 内存分配是偏好的:优先在指定节点分配,但允许从其他节点分配
    • 这是为了平衡性能和可用性:如果绑定节点内存不足,进程仍能正常运行
  3. 内存分配策略类型

    • MPOL_BIND(对应--membind):强制绑定,内存必须从指定节点分配
    • MPOL_PREFERRED(对应--preferred):偏好设置,优先从指定节点分配
    • MPOL_INTERLEAVE:交错分配,在多个节点间轮询分配
    • MPOL_DEFAULT:默认策略,由系统决定

使用建议:

  • 需要严格保证内存位置:使用 numa_alloc_onnode()numactl --membind
  • 需要高性能但允许灵活性:使用 numa_run_on_node()numactl --preferred
  • 需要精确控制CPU(硬亲和性):使用 numactl --cpunodebindnuma_sched_setaffinity()taskset
  • 需要系统级强制CPU绑定(硬亲和性):使用 cgroupcpuset.cpus

编程示例

示例1:C语言中使用libnuma库

numa_basic.cview raw
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
43
44
45
#include <numa.h>
#include <numaif.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main() {
// 检查NUMA是否可用
if (numa_available() < 0) {
printf("NUMA不可用\n");
return 1;
}

// 获取NUMA节点数量
int max_node = numa_max_node();
printf("系统有 %d 个NUMA节点\n", max_node + 1);

// 获取当前进程运行的节点
int current_node = numa_node_of_cpu(sched_getcpu());
printf("当前CPU属于节点: %d\n", current_node);

// 分配内存到指定NUMA节点
size_t size = 1024 * 1024 * 100; // 100MB
void *mem = numa_alloc_onnode(size, 0); // 在节点0上分配内存

if (mem == NULL) {
perror("内存分配失败");
return 1;
}

// 获取内存所在的节点
int mem_node;
if (get_mempolicy(&mem_node, NULL, 0, mem, MPOL_F_NODE | MPOL_F_ADDR) == 0) {
printf("分配的内存位于节点: %d\n", mem_node);
}

// 使用内存...
memset(mem, 0, size);

// 释放内存
numa_free(mem, size);

return 0;
}

编译命令:

1
gcc -o numa_example numa_basic.c -lnuma

示例2:Python中使用numa库

numa_python.pyview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import subprocess
import os

def get_numa_nodes():
"""获取NUMA节点信息"""
result = subprocess.run(['numactl', '--hardware'],
capture_output=True, text=True)
return result.stdout

def bind_to_numa_node(node_id, command):
"""将命令绑定到指定的NUMA节点执行"""
cmd = ['numactl', '--cpunodebind={}'.format(node_id),
'--membind={}'.format(node_id)] + command
return subprocess.run(cmd)

# 示例:获取NUMA信息
print(get_numa_nodes())

# 示例:在节点0上运行Python脚本
bind_to_numa_node(0, ['python', 'your_script.py'])

示例3:多线程程序中的NUMA优化

方法一:不同线程绑定到不同NUMA节点

当不同线程需要绑定到不同NUMA节点时,需要在线程内部手动设置CPU亲和性和内存分配:

numa_threads_different_nodes.cview raw
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <numa.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>

#define NUM_THREADS 4
#define ARRAY_SIZE 1000000

typedef struct {
int thread_id;
int numa_node;
double *array;
int size;
} thread_data_t;

void *worker_thread(void *arg) {
thread_data_t *data = (thread_data_t *)arg;

// 方法1:手动设置CPU亲和性
struct bitmask *cpuset = numa_allocate_cpumask();
numa_node_to_cpus(data->numa_node, cpuset);
numa_sched_setaffinity(0, cpuset);
numa_free_cpumask(cpuset);

// 在指定节点上分配内存
data->array = (double *)numa_alloc_onnode(
data->size * sizeof(double), data->numa_node);

if (data->array == NULL) {
fprintf(stderr, "线程 %d: 内存分配失败\n", data->thread_id);
return NULL;
}

// 执行计算(使用本地内存)
for (int i = 0; i < data->size; i++) {
data->array[i] = i * 1.0;
}

int cpu = sched_getcpu();
printf("线程 %d 在CPU %d (节点 %d) 上完成计算\n",
data->thread_id, cpu, data->numa_node);

return NULL;
}

int main() {
if (numa_available() < 0) {
fprintf(stderr, "NUMA不可用\n");
return 1;
}

int max_node = numa_max_node();
pthread_t threads[NUM_THREADS];
thread_data_t thread_data[NUM_THREADS];

// 创建线程,每个线程分配到不同的NUMA节点
for (int i = 0; i < NUM_THREADS; i++) {
thread_data[i].thread_id = i;
thread_data[i].numa_node = i % (max_node + 1);
thread_data[i].size = ARRAY_SIZE;

pthread_create(&threads[i], NULL, worker_thread, &thread_data[i]);
}

// 等待所有线程完成
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
if (thread_data[i].array) {
numa_free(thread_data[i].array,
thread_data[i].size * sizeof(double));
}
}

return 0;
}

方法二:所有线程绑定到同一NUMA节点

numa_run_on_node() 是一个便捷函数,它会将当前进程绑定到指定NUMA节点的所有CPU上,并设置内存分配偏好策略为该节点。注意:CPU绑定是硬亲和性(强制的),但内存分配只是偏好设置(不强制保证)。

使用场景:

  • 需要将整个进程(包括所有线程)绑定到特定NUMA节点
  • 希望简化代码,避免手动管理CPU掩码和内存策略
  • 在进程启动时进行NUMA绑定

如果所有线程都需要绑定到同一个NUMA节点,可以在主线程中使用 numa_run_on_node() 统一绑定:

numa_threads_same_node.cview raw
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#include <numa.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>

#define NUM_THREADS 4
#define ARRAY_SIZE 1000000

typedef struct {
int thread_id;
double *array;
int size;
} thread_data_t;

void *worker_thread(void *arg) {
thread_data_t *data = (thread_data_t *)arg;

// 注意:numa_run_on_node在主线程中调用后,
// 所有线程都会继承这个绑定,但每个线程仍可能运行在不同CPU上
// 内存分配会优先在绑定的节点上进行(偏好设置,不强制保证)

// 分配内存(会优先在绑定的节点上分配,但不保证)
data->array = (double *)malloc(data->size * sizeof(double));

if (data->array == NULL) {
fprintf(stderr, "线程 %d: 内存分配失败\n", data->thread_id);
return NULL;
}

// 执行计算
for (int i = 0; i < data->size; i++) {
data->array[i] = i * 1.0;
}

int cpu = sched_getcpu();
int node = numa_node_of_cpu(cpu);
printf("线程 %d 在CPU %d (节点 %d) 上完成计算\n",
data->thread_id, cpu, node);

return NULL;
}

int main() {
if (numa_available() < 0) {
fprintf(stderr, "NUMA不可用\n");
return 1;
}

int target_node = 0; // 所有线程绑定到节点0

// 在主线程中绑定到目标节点
// 这会影响到所有后续创建的线程
if (numa_run_on_node(target_node) != 0) {
perror("绑定到NUMA节点失败");
return 1;
}

printf("主进程绑定到节点 %d,创建 %d 个工作线程\n", target_node, NUM_THREADS);

pthread_t threads[NUM_THREADS];
thread_data_t thread_data[NUM_THREADS];

// 创建线程(会自动继承NUMA绑定)
for (int i = 0; i < NUM_THREADS; i++) {
thread_data[i].thread_id = i;
thread_data[i].size = ARRAY_SIZE;
pthread_create(&threads[i], NULL, worker_thread, &thread_data[i]);
}

// 等待所有线程完成
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
if (thread_data[i].array) {
free(thread_data[i].array);
}
}

return 0;
}

numa_run_on_node的特点:

  • 优点:代码简单,一个函数调用完成绑定
  • 优点:CPU绑定是硬亲和性(强制的),进程只能运行在指定节点的CPU上
  • ⚠️ 注意:内存分配是偏好设置,会优先在绑定的节点上分配,但不强制保证
  • ⚠️ 限制:所有线程都绑定到同一个节点,无法为不同线程分配不同节点
  • ⚠️ 注意numa_run_on_node() 会影响整个进程及其所有线程

选择建议:

  • 如果所有线程需要绑定到同一个NUMA节点:使用 numa_run_on_node()(方法二)
  • 如果不同线程需要绑定到不同的NUMA节点:使用手动设置CPU亲和性(方法一)

编译命令:

1
2
gcc -o numa_threads_different_nodes numa_threads_different_nodes.c -lnuma -lpthread
gcc -o numa_threads_same_node numa_threads_same_node.c -lnuma -lpthread

NUMA优化建议

  1. 进程绑定:将进程绑定到特定的NUMA节点,减少跨节点访问
  2. 内存本地化:在进程运行的节点上分配内存
  3. 数据局部性:确保数据访问模式与NUMA拓扑匹配
  4. 监控工具:使用 numastatnumactl 监控和调整NUMA策略
  5. 应用程序设计:在应用程序设计时考虑NUMA架构,合理分配线程和内存
  6. 并行库适配:注意并行库(如TBB、OpenMP)在初始化时会根据操作系统报告的硬件资源来决定线程池大小,通过限制可见的CPU资源可以间接控制并行库的线程数

参考资料

  • man numactl - numactl命令手册
  • man numa - NUMA库函数手册
  • /proc/sys/kernel/numa_balancing - NUMA平衡配置

常用 C++ 并发调试工具 ThreadSanitizer (TSan)

  • LLVM/Clang 提供的运行时检测工具
    • 能精准捕获 数据竞争,输出冲突内存地址、访问栈回溯和涉及线程
  • Helgrind (Valgrind 工具集)
    • 专门用于检测 死锁 和 锁误用
    • 通过监视线程对共享内存的访问来发现潜在冲突
  • rr (Record and Replay Debugger)
    • 可以记录程序执行过程并重放
    • 适合调试 难以复现的并发 bug,如竞态条件和原子性问题
  • Clang Static Analyzer
    • 静态分析工具,在编译阶段发现潜在的并发问题
    • 可检测未加锁访问共享变量、内存序错误等
  • PVS-Studio
    • 商业静态分析工具
    • 能在 CI 流程中提前发现并发安全隐患

适用场景

  • 数据竞争 (Data Race) → 使用 ThreadSanitizer
  • 死锁 (Deadlock) → 使用 Helgrind
  • 难复现的竞态条件 → 使用 rr

编译阶段预防 → 使用 Clang Static Analyzer / PVS-Studio

这些工具往往需要结合使用:例如 TSan + Helgrind + rr 的组合拳,可以覆盖运行时检测、死锁分析和重放调试
,从而大幅提升并发问题定位效率

工具 语言/平台 主要功能 适用场景
ThreadSanitizer C/C++ (Clang/LLVM) 数据竞争检测 运行时发现共享变量冲突
Helgrind C/C++ (Valgrind) 死锁检测、锁误用 多线程同步问题
rr C/C++ (Linux) 执行记录与重放 难复现的竞态条件
Clang Static Analyzer C/C++ 静态分析并发隐患 编译阶段预防
PVS-Studio C/C++ 商业静态分析 CI 流程集成
Oracle Thread Analyzer Java (Oracle 工具链) 线程可视化、死锁检测 Java 并发调试

性能分析工具

工具 类型/定位 attach 支持 分析范围 特点 与 perf 的关系 开源/商业授权 适用人群
Oracle Performance Analyzer (PA) 企业级性能分析器,Oracle Developer Studio 套件 用户态程序 (C/C++/Java/Fortran/Scala),并行应用 GUI 丰富,低开销采样,函数/源代码/指令级分析 独立工具 商业软件 (Oracle Developer Studio) 企业/科研环境下的应用开发者
perf Linux 内核自带性能分析框架 用户态 + 内核态 + 硬件事件 功能全面,支持调用链、硬件计数器,生态丰富 (eBPF, flamegraph) 基础框架 开源 (GPL,Linux 内核自带) 系统工程师,底层性能调优
gprofng GNU 新一代应用性能分析器 用户态程序 (C/C++/Java/Scala),跨平台 基于实验目录,支持 GUI,跨语言,继承 Oracle PA 思路 独立工具 开源 (GNU 工具链) 应用开发者,跨平台性能优化
gprof 经典 GNU 性能分析器 (1980s) 用户态程序 (C/C++) 需编译时 -pg,生成 gmon.out,功能有限,不支持多线程/attach 独立工具 开源 (GNU 工具链) 学术/教学场景,简单函数级分析
Intel PMU Tools Intel 提供的硬件性能计数器工具集 硬件事件 (CPU pipeline、cache、branch、memory) 基于 PMU,提供 top-down 分析,解释微架构瓶颈 依赖 perf 收集数据,属于 perf 的增强解释层 开源 (MIT/Apache 许可,Intel GitHub 提供) 系统/性能工程师,硬件级调优
Intel Advisor 高级性能优化工具 (Intel oneAPI 套件) 用户态程序,尤其是 HPC 应用 提供矢量化分析、内存访问优化、并行化建议,GUI 支持 独立工具 商业软件 (Intel oneAPI 套件,部分免费版) HPC 开发者,科学计算优化
Intel VTune Profiler 深度性能分析器 CPU/GPU/FPGA、内存、线程、I/O、微架构 微架构级剖析,支持 cache、pipeline、分支预测,功能全面 独立工具,但可结合 perf 数据 商业软件 (Intel oneAPI 套件,提供免费社区版) 系统/性能工程师,硬件与应用优化
Linaro MAP (Arm Forge MAP) HPC 并行应用性能分析器 MPI、OpenMP、UPC 等大规模并行应用 低开销采样,跨节点整体分析,源代码行级耗时展示,GUI 可视化 独立工具 商业软件 (Arm Forge 套件,需许可证) HPC 开发者,超级计算机环境

差异总结

  • Oracle PA / gprofng:应用层性能分析,强调 GUI 和跨语言支持。
  • perf:系统级通用工具,是底层框架。
  • gprof:老工具,功能有限,不支持 attach。
  • Intel PMU Tools:基于 perf,提供 Intel CPU 微架构解释,是 perf 的增强解释层。
  • Intel Advisor:智能优化建议工具,适合 HPC 和科学计算。
  • Intel VTune Profiler:深度剖析工具,能揭示硬件微架构瓶颈,适合单节点和复杂应用。
  • Linaro MAP:专为 HPC 并行应用设计,低开销,跨节点整体性能分析。

结论

  • 应用层性能分析:Oracle PA、gprofng
  • 系统/内核级调优:perf
  • 硬件级微架构分析:Intel PMU Tools、Intel VTune Profiler
  • 智能优化建议 (HPC):Intel Advisor
  • 大规模并行应用分析 (HPC):Linaro MAP

👉 可以把它们看作一个层次结构:

  • gprof → gprofng/Oracle PA → perf → Intel PMU Tools/VTune → Advisor/MAP
  • 逐步从函数级分析 → 应用性能 → 系统级 → 硬件级 → HPC 并行整体优化。

Performance Analyzer

Oracle Developer Studio 提供了多种工具:Performance Analyzer、Thread Analyzer。

Performance Analyzer 官方文档

收集数据

使用 collect 命令收集数据
官方文档)。

1
collect collect-options program program-arguments

开始性能分析

使用 analyzer 命令进行性能分析
官方文档)。

1
analyzer [control-options] [experiment | experiment-list]

例如:

1
analyzer -c test.1.er test.4.er

Thread Analyzer

Thread Analyzer 官方文档

计时

计时工具 time

1
$ /usr/bin/time -p ls

Or,

1
$ time ls

其中(参
链接),

1
2
3
$ type -a time
time is a shell keyword
time is /usr/bin/time

软件和硬件定时器

https://weedge.github.io/perf-book-cn/zh/chapters/2-Measuring-Performance/2-5_SW_and_HW_Timers_cn.html

概念

Bad Speculation 指不好的预测,尤其是分支预测。

概念

Core Bound 指 CPU 核心计算能力受限。包括两种:

  1. 硬件的计算资源不足(吞吐量受限)
  • port 冲突:例如 Intel Skylake 架构上,除法和平方根运算都会被分配到 port 0 上,如果大量的类似耗时操
    作排队,那么就会体现为硬件算力的限制。

Port 指的是 CPU 内部的 执行端口(execution ports)。

现代 CPU(尤其是 Intel、AMD 的 x86-64 架构)采用 超标量、乱序执行,有多个功能单元可以并行执行不同
类型的指令。

每个功能单元挂在一个 端口 (port) 上,负责特定类型的操作,比如:

  • 整数运算端口(加减、逻辑运算)
  • 浮点运算端口(乘法、除法、加法)
  • 加载端口(从内存读取数据)
  • 存储端口(写数据到内存)

CPU 的调度器会把指令分配到合适的端口执行。

如果某类端口资源不足,就会出现 port bound(端口受限),性能瓶颈来自于某个端口的拥塞。

👉 举例: Intel Skylake 架构有 8 个端口:

  • Port 0/1:整数和浮点运算
  • Port 2/3:加载(Load)
  • Port 4:存储地址计算
  • Port 5:存储数据
  • Port 6:分支预测
  • Port 7:整数运算
  1. 指令间的依赖(增加延迟)

例如链表的遍历,CPU 无法对其并行。

1
2
3
while (n) {
n = n->next;
}

优化方法

第 1 种计算能力受限的问题,最好的办法是升级 CPU,换成具有更多除数的型号,或者将计算任务卸载到加速器
上。

第 2 种数据依赖链的问题,可能需要重写算法。下面介绍一些有名的优化方法:

  1. 向量化
  2. 函数 inline 化
  3. 循环转换
  4. 编译器内建函数
  5. 其他

目的是减少执行的指令或用更好的汇编指令替代。

CPU 硬件层次概念

  • Package → 整个处理器封装,包含一个或多个 die。
  • Socket (S) → 主板上的物理 CPU 插槽
  • Die (D) → 封装里的裸片(可能有多个 chiplet/die)
  • Core (C) → die 上的计算核心
  • Thread (T) → 核心里的硬件线程(SMT/超线程),即逻辑处理器

假设一台双路服务器,每个 socket 上的 CPU 封装里有两个 die,每个 die 有 8 个核心:

  • S0-D0-C0 → Socket 0 上 Die 0 的 Core 0
  • S0-D1-C3 → Socket 0 上 Die 1 的 Core 3
  • S1-D0-C7 → Socket 1 上 Die 0 的 Core 7

如果开启超线程,还可能进一步细分为:

  • S0-D0-C0-T0 → Socket 0, Die 0, Core 0, Thread 0
  • S0-D0-C0-T1 → Socket 0, Die 0, Core 0, Thread 1

可以用命令 lscpucat /proc/cpuinfo 来查看逻辑 CPU ID 与 Socket/Core/Thread 的对应关系。

1
2
3
4
5
6
$ lscpu
...
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
...
1
总逻辑 CPU 数量 = 1 × 4 × 2 = 8

nproc 输出的是 逻辑 CPU 数

1
2
$ nproc
8

CPU 微架构概念

  • Pipeline:指令执行的分阶段过程(取指、解码、执行、写回)。
  • Width:每周期最多能发射多少条 uops(如 4‑wide)。
  • Slot:每周期的发射机会,宽度决定 slot 数。
  • ROB (Reorder Buffer):乱序执行的关键结构,保证指令按程序顺序退休。
  • Scheduler:调度器,决定哪些 uops 在某周期进入执行端口。
  • Execution Ports:后端的执行单元入口,比如整数 ALU、浮点 FPU、Load/Store。
  • Branch Predictor:预测分支走向,减少流水线停顿。

存储与层次结构

  • Registers:CPU 内部的寄存器,最快的存储。
  • Cache:分层缓存(L1、L2、L3),用于减少访存延迟。
  • LLC (Last Level Cache):最后一级缓存,通常是 L3,多个核心共享。
  • Memory Controller:负责和 DRAM 通信。
  • NUMA (Non-Uniform Memory Access):多 socket 系统里,内存访问延迟因位置不同而不同。

性能分析相关

  • IPC (Instructions Per Cycle):每周期平均执行的指令数。
  • ILP (Instruction-Level Parallelism):指令级并行度,程序能提供多少独立指令。
  • Topdown Metrics:Retiring、Bad Speculation、Frontend Bound、Backend Bound。
  • BE/Core vs BE/Mem:后端瓶颈是算力不足还是访存延迟。
  • CPI (Cycles Per Instruction):每条指令平均耗费的周期数。

Top-Down Microarchitecture Analysis (TMA)

这是 Intel 提出的一个 CPU 性能瓶颈分析框架。它的核心思想是:把 CPU 每个周期的 发射机会(slot) 分门
别类,逐层细分,最终定位到性能瓶颈的根源。

Topdown 是分层树状结构:

  • Level 1:Retiring / Bad Speculation / Frontend Bound / Backend Bound
  • Level 2:Backend Bound → BE/Core、BE/Mem
  • Level 3:BE/Mem → L1 Bound、L2 Bound、DRAM Bound
  1. Level 1: 顶层四大分类

在每个周期的 slot 中,CPU 的工作被划分为四类:

  • Retiring

    • 指令成功退休(完成执行并写回结果)。
    • 这是“有用工作”,比例越高说明 CPU 利用率越好。
  • Bad Speculation

    • 由于错误预测(如分支预测失败、错误路径执行)导致的浪费。
    • 这些 slot 最终没有产生有效结果。
  • Frontend Bound (FE)

    • 前端受限:取指、解码、指令缓存不足。
    • CPU 等待指令进入流水线。
  • Backend Bound (BE)

    • 后端受限:执行单元或数据不可用。
    • CPU 等待算力资源或内存数据。

这四类加起来 ≈ 100%,覆盖了所有 slot 的去向。

  1. Level 2: Backend 的进一步细分
  • BE/Core
    • 后端瓶颈主要来自核心执行资源不足(算术逻辑单元、浮点单元、端口冲突)。
    • 程序算力密集。
  • BE/Mem
    • 后端瓶颈主要来自访存延迟(缓存未命中、DRAM 访问慢)。
    • 程序内存密集。
  1. Level 3: BE/Mem 的进一步细分
  • L1 Bound
  • L2 Bound
  • L3 Bound
  • DRAM Bound

Roofline

性能分析工具

  1. perf (Linux)
  2. Intel® pmu-tools :对 perf 的封装
  3. Intel Advisor

参考

  1. perf-ninja: 代码 +
    视频

relaxed

  • 问:load(relaxed) 看到的是不是最新值?
  • 答:是的。

经过多次测试(包括 TSAN),CAS(relaxed) 计数器都是正确的:

1
g++ -fsanitize=thread test_memory_order_relaxed.cpp -lpthread
test_memory_order_relaxed.cppview raw
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <atomic>
#include <chrono>
#include <iostream>
#include <random>
#include <thread>
using namespace std;

constexpr int run_times = 10;
constexpr uint32_t num_threads = 10;
constexpr uint64_t increments_per_thread = 1024 * 1024 * 32;
constexpr uint64_t max_count = increments_per_thread * num_threads;

atomic<uint64_t> counter;

/** Random Delay in microseonds */
std::random_device rd;
thread_local std::mt19937 gen(rd());
thread_local std::uniform_int_distribution<> dis(0, 4095);

void unpredictableDelay(int extra = 0) {
if (dis(gen) == 0) {
this_thread::sleep_for(chrono::nanoseconds(2000 + extra));
}
}

/** thread function for counting */
void worker(int id) {
for (int i = 0; i < increments_per_thread; ++i) {
uint64_t old = counter.load(memory_order_relaxed);
// 如果 load(relaxed) 不能看到当前最新值
// 那么 CAS 就会加多次,最终结果会大于 max_count
while (old < max_count &&
!counter.compare_exchange_weak(old, old + 1, memory_order_relaxed)) {
// old is updated with the current value of counter
unpredictableDelay(dis(gen));
}
}
// cout << "Worker " << id << " done." << endl;
}

/** main function */
int main() {
for (int run = 0; run < run_times; ++run) {
counter.store(0, memory_order_relaxed);
cout << "Run " << run << ": ";
thread threads[num_threads];

for (int i = 0; i < num_threads; ++i) {
threads[i] = thread(worker, i);
}

for (int i = 0; i < num_threads; ++i) {
threads[i].join();
}

cout << (max_count == counter.load() ? "Correct" : "Wrong") << endl;
}

return 0;
}