0%

CLI11 简介

CLI11 是一个用于处理命令行参数和选项的 C++ 库,旨在简化 C++ 应用程序的命令行界面开发。其主要特点包括:

  1. 简单易用:提供直观的 API,使开发者能够轻松定义和解析命令行选项
  2. 现代 C++ 支持:充分利用现代 C++ 特性,如类型推导和 lambda 表达式
  3. 丰富的选项支持:支持标志选项、位置参数、可选参数和必选参数等
  4. 类型安全:在解析和处理命令行参数时提供类型安全的机制
  5. 灵活的错误处理:提供多种错误处理方式,包括参数验证失败时的错误提示和帮助信息的自动生成
  6. 跨平台支持:可在主流操作系统上运行,包括 Windows、macOS 和各种 Linux 发行版

下载和安装

CLI11 是一个单头文件库,安装非常简单。有以下几种安装方式:

方式一:单文件头文件(推荐)

  1. CLI11 GitHub 仓库 下载最新的 CLI11.hpp 文件
  2. CLI11.hpp 复制到您的项目包含目录中
  3. 在代码中直接包含即可使用:
    1
    #include "CLI11.hpp"

方式二:使用 CMake 集成

如果您的项目使用 CMake,可以通过以下方式集成:

  1. 作为 Git 子模块

    1
    git submodule add https://github.com/CLIUtils/CLI11.git

    CMakeLists.txt 中:

    1
    2
    add_subdirectory(CLI11)
    target_link_libraries(your_target CLI11::CLI11)
  2. 使用 FetchContent(CMake 3.11+):

    1
    2
    3
    4
    5
    6
    7
    8
    include(FetchContent)
    FetchContent_Declare(
    CLI11
    GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git
    GIT_TAG v2.4.1 # 使用最新版本标签
    )
    FetchContent_MakeAvailable(CLI11)
    target_link_libraries(your_target CLI11::CLI11)

方式三:包管理器安装

  • vcpkgvcpkg install cli11
  • Conanconan install CLI11/2.4.1@cliutils/stable
  • Homebrew(macOS):brew install cli11

方式四:全局安装

CLI11.hpp 复制到系统共享文件夹位置(如 /opt/CLI11/usr/local/include),然后在 CMake 中:

1
include_directories(/opt/CLI11)

注意CLI11.hpp 包含整个命令行解析库的核心功能。如果需要使用单独的实用工具(如 Timer、AutoTimer),需要单独复制相应的头文件。

CLI11 API 层级关系

CLI11 的核心数据结构

CLI11 有三个核心数据结构:

  1. App - 应用根对象

    • 所有选项和子命令的容器
    • 代表整个命令行应用程序
    • Subcommand 实际上就是 App 对象,不是独立的数据结构。
      • add_subcommand() 返回 App*,所以 Subcommand 可以无限嵌套。
  2. Option - 选项对象

    • 通过 add_flag()add_option() 创建
    • add_flag() → 返回 Option*
      • 布尔标志,不需要值
      • 示例: app.add_flag("-v", verbose)
    • add_option() → 返回 Option*
      • 需要值的选项
      • 示例: app.add_option("-f", file, "File path")
  3. Option_group - 选项组(继承自 App

    • 继承关系: class Option_group : public App
    • 本质上是 App 对象,可以使用 App 的所有方法
    • 用于组织相关选项
    • 通过 add_option_group() 创建,返回 Option_group*(可转换为 App*
    • 用于实现 Suboption 功能
    • 嵌套支持: 因为继承自 App,可以在 Option_group 中再创建 Option_group 实现多层嵌套

层级结构

层级 0: App(应用根对象)

1
CLI::App app("description");
  • 所有选项和子命令的顶层容器

层级 1: App 的直接子级(三种平级对象)

Option(选项) - 通过 add_flag()add_option() 添加

  • add_flag(): 布尔标志,不需要值
  • add_option(): 需要值的选项
  • 两者都返回 Option*,独立存在

Subcommand(子命令) - 通过 add_subcommand() 添加

  • 返回 App* 对象,可以继续调用 add_subcommand() 实现无限嵌套
  • 独立存在,本身也是 App 类型

Option_group(选项组) - 通过 add_option_group() 添加

  • 返回 Option_group*(继承自 App)
  • 用于组织选项,实现 Suboption 功能

层级 2: 依赖关系

Suboption(子选项) - 通过 Option_group + needs() 实现

1
2
3
4
app.add_flag("-add", add_flag);  // 父选项
CLI::Option_group *add_group = app.add_option_group("add_suboptions", "Sub-options for -add");
add_group->add_option("-file", file_path); // 子选项
add_group->needs(app.get_option("-add")); // 建立依赖
  • 使用: myprog -add -file path.txt(必须先有 -add
  • 多层嵌套: 在 Option_group 中再创建 Option_group

Subcommand 的 Option - 属于 Subcommand

1
2
CLI::App *start = app.add_subcommand("start");
start->add_option("-f", file_path); // 子命令的选项
  • 使用: myprog start -f file.txt

关键区别

类型 API 函数 返回类型 层级 独立性 示例
App CLI::App app("desc") App 0 根对象 应用根对象
Option (flag) app.add_flag() Option* 1 独立 myprog -v
Option (option) app.add_option() Option* 1 独立 myprog -f file.txt
Subcommand app.add_subcommand() App* 1 独立,可嵌套 myprog start
Suboption Option_group + needs() - 2 依赖父 Option myprog -add -file path.txt
Subcommand 的 Option subcmd->add_option() Option* 2 属于 Subcommand myprog start -f file.txt

一个 Suboption 的示例程序

以下是一个 Suboption 的示例程序,不支持子命令使用单横线作为长选项支持子选项
我们选择这个示例是因为 Subcommand 的实现比较简单(就是 App 的无限嵌套),所以我们写了一个支持子选项的示例程序来演示更复杂的用法。

示例程序的层级结构

示例程序演示了三级层级结构:

1
2
3
4
5
6
7
8
9
10
App (myprog)
├── Option (-add) ← Level 1: 顶级选项 (flag)
├── Option (-del) ← Level 1: 顶级选项 (flag, 与 -add 平级)
├── Option (-force) ← Level 1: 顶级选项 (flag, 与 -add 平级)
└── Option_group (add_suboptions) ← Level 2: 子选项组
├── Option (-file) ← Level 2: -add 的子选项 (需要值)
├── Option (-recursive)← Level 2: -add 的子选项 (flag, 不需要值)
└── Option_group (file_suboptions) ← Level 3: -file 的子选项组
├── Option (-encoding) ← Level 3: -file 的子选项 (需要值)
└── Option (-overwrite) ← Level 3: -file 的子选项 (flag, 不需要值)

依赖关系通过以下方式建立:

  • add_group->needs(add_option) - 使选项组要求 -add 选项必须存在
  • file_group->needs(file_option) - 使文件子选项组要求 -file 选项必须存在

示例命令行:

  • 仅 Level 1: myprog -add -del -force
  • Level 1 + 2: myprog -add -file path/to/file.txt -recursive -del -force
  • Level 1 + 2 + 3: myprog -add -file path/to/file.txt -encoding utf8 -overwrite -del -force

文件说明

suboption_example.cpp - 示例程序

suboption_example.cpp 是一个可执行的示例程序,用于演示子选项功能。可以直接运行并测试功能。

suboption_example.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
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
// Copyright (c) 2017-2025, University of Cincinnati, developed by Henry Schreiner
// under NSF AWARD 1414736 and by the respective contributors.
// All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause

// 示例程序:演示子选项功能(三级层级)
// 编译: g++ -std=c++11 example.cpp -I../../include -o example
// 运行示例:
// ./example -add -file path/to/file.txt -recursive -del -force
// ./example -add -file path/to/file.txt -encoding utf8 -overwrite -del -force

#ifdef CLI11_SINGLE_FILE
#include "CLI11.hpp"
#else
#include "CLI/CLI.hpp"
#endif

#include <iostream>
#include <string>

int main(int argc, char **argv) {
CLI::App app{"SubOption Example Program"};

bool add_flag = false;
bool del_flag = false;
bool force_flag = false;
std::string file_path; // Level 2: 需要值的选项
bool recursive_flag = false; // Level 2: 标志选项
std::string encoding; // Level 3: 需要值的选项
bool overwrite_flag = false; // Level 3: 标志选项

// 允许非标准选项名(单破折号后面跟多个字符)
app.allow_non_standard_option_names();

// 平级选项:-add, -del, -force
app.add_flag("-add", add_flag, "Add option");
app.add_flag("-del", del_flag, "Delete option");
app.add_flag("-force", force_flag, "Force option");

// -add 的子选项:-file (需要值) 和 -recursive (标志)
// 使用选项组来实现子选项功能
CLI::Option_group *add_group = app.add_option_group("add_suboptions", "Sub-options for -add");
add_group->allow_non_standard_option_names(); // 选项组也需要启用非标准选项名
add_group->add_option("-file", file_path, "File path (requires a value)");
add_group->add_flag("-recursive", recursive_flag, "Process recursively (flag, no value needed)");

// 子选项需要 -add 选项存在
CLI::Option *add_option = app.get_option("-add");
add_group->needs(add_option);

// 第三级层级:-file 的子选项
// 在 -file 选项组下再创建一个选项组
CLI::Option_group *file_group = add_group->add_option_group("file_suboptions", "Sub-options for -file");
file_group->allow_non_standard_option_names();
file_group->add_option("-encoding", encoding, "File encoding (requires a value, e.g., utf8, gbk)");
file_group->add_flag("-overwrite", overwrite_flag, "Overwrite existing file (flag, no value needed)");

// 子子选项需要 -file 选项存在
auto *file_option = add_group->get_option("-file");
file_group->needs(file_option);

// 解析命令行参数
try {
app.parse(argc, argv);
} catch(const CLI::ParseError &e) {
return app.exit(e);
}

// 输出结果
std::cout << "解析结果:\n";
std::cout << " -add: " << (add_flag ? "true" : "false") << "\n";
std::cout << " -del: " << (del_flag ? "true" : "false") << "\n";
std::cout << " -force: " << (force_flag ? "true" : "false") << "\n";

if(add_flag) {
std::cout << " -file: " << (file_path.empty() ? "(未设置)" : file_path) << "\n";
std::cout << " -recursive: " << (recursive_flag ? "true" : "false") << "\n";

if(!file_path.empty()) {
std::cout << " -encoding: " << (encoding.empty() ? "(未设置)" : encoding) << "\n";
std::cout << " -overwrite: " << (overwrite_flag ? "true" : "false") << "\n";
}
}

return 0;
}
构建和运行示例程序
1
2
3
4
5
6
cd /home/zhigaoz/code/CLI11/build
cmake --build . --target suboption_example
# Run examples
./tests/suboption_test/suboption_example --help
./tests/suboption_test/suboption_example -add
./tests/suboption_test/suboption_example -add -file path/to/file.txt -encoding utf8 -overwrite -del -force

实现细节

测试使用 CLI11 的 Option_group 功能和 needs() 方法,确保子选项只有在父选项存在时才有效。

关键实现细节

  1. 非标准选项名:

    • 使用 app.allow_non_standard_option_names() 允许单破折号后面跟多个字符的选项(例如 -add 而不是 --add
  2. 选项组创建:

    1
    2
    CLI::Option_group *file_group = add_group->add_option_group("file_suboptions", "Sub-options for -file");
    file_group->allow_non_standard_option_names();
  3. 依赖关系:

    1
    2
    CLI::Option *add_option = app.get_option("-add");
    add_group->needs(add_option); // 子选项需要 -add 选项存在
  4. 添加子选项:

    1
    2
    3
    4
    // 注意:CLI11 没有 add_suboption() 函数
    // 子选项通过在 Option_group 中使用 add_option() 或 add_flag() 实现
    add_group->add_option("-file", file_path, "File path (requires a value)");
    add_group->add_flag("-recursive", recursive_flag, "Process recursively (flag, no value needed)");

引言

InfiniBand/RDMA 提供了两种截然不同的数据传输模式:Send/RecvRead/Write。这两种模式在底层实现机制、CPU 参与度、对端感知性和传输控制策略方面存在根本性差异。

核心差异对比

基本特性对比

维度 Send/Recv Read/Write
操作码 IBV_WR_SEND IBV_WR_RDMA_WRITE
IBV_WR_RDMA_READ
CPU 参与 接收端必须参与(post recv) 对端 CPU 不参与
对端感知 完全感知(CQ 事件) 不感知(无事件)
流控机制 有(RNR 机制)
传输速率控制 受接收端节制 不受对端节制
内存注册要求 发送端和接收端都需要 发起端和对端都需要(对端需设置 REMOTE 权限)
远程地址信息 不需要 需要(remote_addr + rkey)
完成事件 双方都有 仅发起端有
使用复杂度 需要双方协调 只需发起端操作
性能 高(硬件加速) 最高(零拷贝,无 CPU 参与)

工作机制对比

步骤 Send/Recv Read/Write
1. 准备工作 接收端预先 post recv 对端注册内存并传递 R_Key
2. 发起操作 发送端 post send 发起端 post RDMA Read/Write
3. 数据传输 HCA 匹配 Receive WR 并写入缓冲区 HCA 直接访问远程内存
4. 完成通知 双方 CQ 都有完成事件 仅发起端 CQ 有完成事件
5. 对端状态 接收端知道数据到达 对端 CPU 完全无感知

CPU 参与度与对端感知性

方面 Send/Recv Read/Write
接收端操作 必须预先 post recv 无需任何操作
接收端 CPU 必须参与,处理 CQ 事件 完全不参与
接收端控制 可以控制接收速率 无法控制传输速率
对端 CPU 状态 需要处理接收事件 可以执行其他任务或休眠
数据到达感知 完全感知(CQ 事件) 不感知
传输时机控制 可以控制接收时机 无法控制传输时机
数据来源信息 知道数据来源(QP 编号) 不知道数据来源
数据大小信息 知道数据大小 不知道传输大小
传输完成感知 通过 CQ 事件知道 无法直接知道
同步机制 通过 CQ 事件同步 需要额外机制(Send/Recv 或轮询)

传输速率控制

方面 Send/Recv Read/Write
流控机制 RNR(Receiver Not Ready)机制 无流控机制
速率控制方 接收端控制(通过 post recv 频率) 发起端完全控制
RNR 处理 发送端收到 RNR NACK,等待重试 不适用
对端限制能力 可以限制接收速率 无法限制传输速率
潜在风险 接收端可能过载(但可通过流控避免) 对端可能被数据淹没
性能特点 受接收端处理能力限制 可达到网络带宽上限

RNR 机制参数

参数 说明 典型值
rnr_retry RNR 重试次数(0-7,7 表示无限重试) 7
min_rnr_timer 最小 RNR 等待时间(单位:655.36 微秒) 0x12
1
2
3
// 在 modify_qp_to_rts 中设置
attr.rnr_retry = 7; // RNR 重试次数
attr.min_rnr_timer = 0x12; // 最小 RNR 等待时间

API 与代码示例

关键函数

ibv_post_recv()

接收端必须预先调用此函数提交 Receive WR:

1
2
3
4
5
#include <infiniband/verbs.h>

int ibv_post_recv(struct ibv_qp *qp,
struct ibv_recv_wr *wr,
struct ibv_recv_wr **bad_wr);
参数 说明
qp 队列对句柄
wr Receive Work Request 链表头
bad_wr 如果提交失败,返回失败的 WR 指针

返回值:成功返回 0,失败返回错误码

ibv_post_send()

发送端调用此函数提交 Send WR 或 RDMA Read/Write WR:

1
2
3
int ibv_post_send(struct ibv_qp *qp,
struct ibv_send_wr *wr,
struct ibv_send_wr **bad_wr);
参数 说明
qp 队列对句柄
wr Send Work Request 链表头
bad_wr 如果提交失败,返回失败的 WR 指针

数据结构

Receive Work Request

1
2
3
4
5
6
struct ibv_recv_wr {
uint64_t wr_id; // 工作请求标识符
struct ibv_recv_wr *next; // 下一个 WR(可组成链表)
struct ibv_sge *sg_list; // Scatter/Gather 元素数组
int num_sge; // SGE 数量
};

Send Work Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct ibv_send_wr {
uint64_t wr_id; // 工作请求标识符
struct ibv_send_wr *next; // 下一个 WR
struct ibv_sge *sg_list; // Scatter/Gather 元素数组
int num_sge; // SGE 数量
enum ibv_wr_opcode opcode; // 操作类型
int send_flags; // 发送标志
union {
struct {
uint64_t remote_addr; // 远程地址(RDMA 操作使用)
uint32_t rkey; // 远程密钥(RDMA 操作使用)
} rdma;
// ... 其他操作类型
} wr;
uint32_t imm_data; // 立即数据(可选)
};

Scatter/Gather Element

1
2
3
4
5
struct ibv_sge {
uint64_t addr; // 缓冲区地址
uint32_t length; // 缓冲区长度
uint32_t lkey; // 本地内存区域密钥
};

操作码与标志位

操作码 说明 使用场景
IBV_WR_SEND Send/Recv 模式 需要接收端确认的消息传递
IBV_WR_RDMA_WRITE RDMA Write 将本地数据写入远程内存
IBV_WR_RDMA_WRITE_WITH_IMM RDMA Write with Immediate 写入数据并通知对端
IBV_WR_RDMA_READ RDMA Read 从远程内存读取数据
标志位 说明 使用场景
IBV_SEND_FENCE 栅栏标志,确保顺序 需要保证操作顺序
IBV_SEND_SIGNALED 请求完成通知 需要知道操作完成
IBV_SEND_SOLICITED 请求立即通知(用于 Send with Immediate) Send with Immediate
IBV_SEND_INLINE 内联发送(小数据直接放在 WR 中) 小数据快速发送

Send/Recv 代码示例

接收端代码

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
// 1. 准备接收缓冲区
char *recv_buffer = malloc(BUFFER_SIZE);
struct ibv_mr *recv_mr = ibv_reg_mr(pd, recv_buffer, BUFFER_SIZE,
IBV_ACCESS_LOCAL_WRITE);

// 2. 准备 Scatter/Gather Element
struct ibv_sge recv_sge;
recv_sge.addr = (uintptr_t)recv_buffer;
recv_sge.length = BUFFER_SIZE;
recv_sge.lkey = recv_mr->lkey;

// 3. 准备 Receive Work Request
struct ibv_recv_wr recv_wr, *bad_recv_wr;
memset(&recv_wr, 0, sizeof(recv_wr));
recv_wr.wr_id = (uintptr_t)recv_buffer;
recv_wr.sg_list = &recv_sge;
recv_wr.num_sge = 1;

// 4. 提交 Receive WR(必须预先提交)
if (ibv_post_recv(qp, &recv_wr, &bad_recv_wr)) {
fprintf(stderr, "Failed to post receive WR\n");
return -1;
}

// 5. 等待接收完成
struct ibv_wc wc;
int ne;
do {
ne = ibv_poll_cq(cq, 1, &wc);
} while (ne == 0);

if (wc.status != IBV_WC_SUCCESS) {
fprintf(stderr, "Receive failed with status: %s\n",
ibv_wc_status_str(wc.status));
return -1;
}

发送端代码

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
// 1. 准备发送缓冲区
char *send_buffer = malloc(BUFFER_SIZE);
memcpy(send_buffer, data_to_send, data_size);
struct ibv_mr *send_mr = ibv_reg_mr(pd, send_buffer, BUFFER_SIZE,
IBV_ACCESS_LOCAL_WRITE);

// 2. 准备 Scatter/Gather Element
struct ibv_sge send_sge;
send_sge.addr = (uintptr_t)send_buffer;
send_sge.length = data_size;
send_sge.lkey = send_mr->lkey;

// 3. 准备 Send Work Request
struct ibv_send_wr send_wr, *bad_send_wr;
memset(&send_wr, 0, sizeof(send_wr));
send_wr.wr_id = (uintptr_t)send_buffer;
send_wr.opcode = IBV_WR_SEND; // Send/Recv 模式
send_wr.send_flags = IBV_SEND_SIGNALED; // 请求完成通知
send_wr.sg_list = &send_sge;
send_wr.num_sge = 1;

// 4. 提交 Send WR
if (ibv_post_send(qp, &send_wr, &bad_send_wr)) {
fprintf(stderr, "Failed to post send WR\n");
return -1;
}

// 5. 等待发送完成
struct ibv_wc wc;
int ne;
do {
ne = ibv_poll_cq(cq, 1, &wc);
} while (ne == 0);

Read/Write 代码示例

RDMA Write 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
// 发起端代码
struct ibv_send_wr write_wr, *bad_wr;
memset(&write_wr, 0, sizeof(write_wr));

write_wr.wr_id = (uintptr_t)local_buffer;
write_wr.opcode = IBV_WR_RDMA_WRITE; // RDMA Write
write_wr.send_flags = IBV_SEND_SIGNALED;
write_wr.sg_list = &sge;
write_wr.num_sge = 1;
write_wr.wr.rdma.remote_addr = remote_addr; // 远程内存地址
write_wr.wr.rdma.rkey = remote_rkey; // 远程内存密钥

ibv_post_send(qp, &write_wr, &bad_wr);

RDMA Read 操作

1
2
3
4
5
6
7
8
9
10
11
12
struct ibv_send_wr read_wr, *bad_wr;
memset(&read_wr, 0, sizeof(read_wr));

read_wr.wr_id = (uintptr_t)local_buffer;
read_wr.opcode = IBV_WR_RDMA_READ; // RDMA Read
read_wr.send_flags = IBV_SEND_SIGNALED;
read_wr.sg_list = &sge;
read_wr.num_sge = 1;
read_wr.wr.rdma.remote_addr = remote_addr; // 远程内存地址
read_wr.wr.rdma.rkey = remote_rkey; // 远程内存密钥

ibv_post_send(qp, &read_wr, &bad_wr);

内存注册要求

1
2
3
4
5
6
7
8
9
// 对端注册内存(允许远程访问)
struct ibv_mr *remote_mr = ibv_reg_mr(pd, remote_buffer, BUFFER_SIZE,
IBV_ACCESS_LOCAL_WRITE | // 本地写权限
IBV_ACCESS_REMOTE_READ | // 允许远程读
IBV_ACCESS_REMOTE_WRITE); // 允许远程写

// 将 R_Key 和地址传递给发起端
// remote_addr = (uint64_t)remote_buffer;
// remote_rkey = remote_mr->rkey;

同步机制

Send/Recv 同步机制

关键问题:ibv_post_recv 必须在 ibv_post_send 之前吗?

答案:不是必须在 ibv_post_send 之前调用,但必须在数据到达之前 post recv。

调用顺序说明

场景 说明 是否可行
预先 post recv 在发送端 post send 之前,接收端预先 post recv ✅ 推荐做法
同时调用 接收端和发送端同时调用(不同线程/进程) ✅ 可行,但需要确保 recv 先完成
延迟 post recv 发送端先 post send,接收端后 post recv ⚠️ 可能导致 RNR 错误

工作原理

  1. 接收队列(RQ)机制

    • 接收端调用 ibv_post_recv() 将 Receive WR 提交到接收队列(RQ)
    • 当数据包到达时,HCA 硬件从 RQ 中取出一个 Receive WR
    • 如果 RQ 中没有 Receive WR,HCA 返回 RNR(Receiver Not Ready)NACK
  2. 时序要求

    • 不是要求 ibv_post_recv() 必须在 ibv_post_send() 之前调用
    • 而是要求数据包到达时,RQ 中必须有可用的 Receive WR
    • 由于网络延迟,通常可以预先 post recv

代码示例

场景 1:预先批量 post recv(推荐)

1
2
3
4
5
6
7
8
9
10
// 接收端:预先批量提交多个 Receive WR
for (int i = 0; i < BATCH_SIZE; i++) {
struct ibv_recv_wr recv_wr, *bad_wr;
// ... 准备 recv_wr
ibv_post_recv(qp, &recv_wr, &bad_wr);
}

// 此时发送端可以随时 post send
// 发送端:随时发送数据
ibv_post_send(qp, &send_wr, &bad_send_wr);

场景 2:RNR 错误处理

1
2
3
4
5
6
7
8
9
10
11
// 发送端处理 RNR 错误
struct ibv_wc wc;
ibv_poll_cq(cq, 1, &wc);

if (wc.status == IBV_WC_RNR_RETRY_EXC_ERR) {
// RNR 重试次数超限
fprintf(stderr, "RNR retry exceeded\n");
} else if (wc.status == IBV_WC_REM_OP_ERR) {
// 可能是 RNR 错误
fprintf(stderr, "Remote operation error, possibly RNR\n");
}

最佳实践

实践 说明 原因
预先批量 post recv 在连接建立后立即批量提交多个 Receive WR 避免 RNR 错误,提高性能
保持 RQ 中有足够的 WR 接收完成后立即重新 post recv 确保持续接收能力
使用应用层同步 如果必须延迟 post recv,使用同步机制 避免 RNR 错误
合理设置 RNR 参数 设置合适的 rnr_retrymin_rnr_timer 给接收端时间 post recv

Read/Write 同步机制

由于 Read/Write 操作对端无感知,需要额外的同步机制来保证数据一致性和操作顺序。

同步问题

问题 说明 影响
数据写入完成通知 对端不知道数据何时写入完成 可能读取到未完成的数据
数据读取时机 对端不知道何时被读取 可能在修改时被读取,导致数据不一致
操作顺序保证 多个 RDMA 操作的顺序 可能乱序执行,导致逻辑错误
并发访问 本地 CPU 和远程 RDMA 同时访问 可能导致数据竞争

同步机制对比

机制 说明 适用场景
RDMA Write with Immediate 写入数据的同时发送立即数据通知 需要通知对端数据已写入
Send/Recv 通知 通过 Send/Recv 发送完成通知 需要确认和流控
内存屏障(Fence) 使用 IBV_SEND_FENCE 保证顺序 需要保证操作顺序
原子操作 使用原子操作作为同步点 需要细粒度同步
版本号/双缓冲 使用版本号或双缓冲机制 需要检测数据变化或无锁读取

RDMA Write with Immediate

在写入数据的同时,发送立即数据通知对端:

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
// 发起端:写入数据并通知
struct ibv_send_wr write_wr, *bad_wr;
memset(&write_wr, 0, sizeof(write_wr));

write_wr.wr_id = (uintptr_t)local_buffer;
write_wr.opcode = IBV_WR_RDMA_WRITE_WITH_IMM; // 带立即数据的 Write
write_wr.send_flags = IBV_SEND_SIGNALED;
write_wr.sg_list = &sge;
write_wr.num_sge = 1;
write_wr.wr.rdma.remote_addr = remote_addr;
write_wr.wr.rdma.rkey = remote_rkey;
write_wr.imm_data = htonl(NOTIFICATION_FLAG); // 立即数据(网络字节序)

ibv_post_send(qp, &write_wr, &bad_wr);

// 对端:接收立即数据通知(需要预先 post recv)
struct ibv_recv_wr recv_wr, *bad_recv_wr;
// ... 准备 Receive WR
ibv_post_recv(qp, &recv_wr, &bad_recv_wr);

// 轮询 CQ,接收立即数据通知
struct ibv_wc wc;
ibv_poll_cq(cq, 1, &wc);
if (wc.opcode == IBV_WC_RECV_RDMA_WITH_IMM) {
uint32_t imm_data = ntohl(wc.imm_data);
// 知道数据已经写入,可以安全读取
}

内存屏障(Fence)机制

使用 IBV_SEND_FENCE 保证操作顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 场景:需要保证多个 RDMA Write 的顺序
struct ibv_send_wr wr[3], *bad_wr;

// WR 1: 写入元数据
wr[0].opcode = IBV_WR_RDMA_WRITE;
wr[0].send_flags = 0;
wr[0].next = &wr[1];

// WR 2: Fence,确保前面的操作完成
wr[1].opcode = IBV_WR_RDMA_WRITE;
wr[1].send_flags = IBV_SEND_FENCE; // 栅栏标志
wr[1].next = &wr[2];

// WR 3: 写入标志位(表示数据已准备好)
wr[2].opcode = IBV_WR_RDMA_WRITE;
wr[2].send_flags = IBV_SEND_SIGNALED;
wr[2].next = NULL;

ibv_post_send(qp, &wr[0], &bad_wr);
// 保证:元数据写入 → Fence → 标志位写入(顺序执行)

Fence 的作用

  • 确保 Fence 之前的所有操作在 Fence 之后的操作之前完成
  • 保证操作的全局顺序(跨多个 QP)
  • 适用于需要严格顺序的场景

原子操作同步

使用原子操作作为同步点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 对端:准备数据后,使用原子操作设置标志
struct ibv_send_wr atomic_wr, *bad_wr;
memset(&atomic_wr, 0, sizeof(atomic_wr));

// 关键操作码:Compare and Swap 原子操作
// 作用:原子地比较远程内存的值,如果等于期望值则交换为新值
// 特点:这是硬件保证的原子操作,不会被其他操作打断,用于实现同步原语
atomic_wr.opcode = IBV_WR_ATOMIC_CMP_AND_SWP;

// 关键标志位:请求完成通知
// 作用:操作完成后在 CQ 中生成完成事件,用于确认原子操作是否成功
// 注意:原子操作的成功/失败通过 Work Completion 的状态字段判断
atomic_wr.send_flags = IBV_SEND_SIGNALED;
atomic_wr.sg_list = &sge; // 本地缓冲区,用于存储旧值
atomic_wr.num_sge = 1;
atomic_wr.wr.atomic.remote_addr = remote_flag_addr; // 远程标志位地址
atomic_wr.wr.atomic.rkey = remote_rkey;
atomic_wr.wr.atomic.compare_add = 0; // 期望值:0(未准备好)
atomic_wr.wr.atomic.swap = 1; // 新值:1(已准备好)

ibv_post_send(qp, &atomic_wr, &bad_wr);
操作码 说明 用途
IBV_WR_ATOMIC_CMP_AND_SWP Compare and Swap 条件更新标志位
IBV_WR_ATOMIC_FETCH_AND_ADD Fetch and Add 计数器操作

Read 时的并发安全

问题:如果 Read 时对端 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
25
26
27
28
// 数据结构
struct data_with_version {
uint64_t version; // 版本号
char data[BUFFER_SIZE]; // 实际数据
};

// 对端:修改数据时增加版本号
void update_data(struct data_with_version *buf) {
prepare_new_data(buf->data);
__sync_synchronize(); // 内存屏障
__sync_add_and_fetch(&buf->version, 1); // 原子增加版本号
}

// 发起端:读取时检查版本号
uint64_t old_version = 0;
do {
uint64_t version_before = read_version();
__sync_synchronize();
read_data(buffer);
__sync_synchronize();
uint64_t version_after = read_version();

// 如果版本号相同,说明读取期间数据未变化
if (version_before == version_after && version_before != old_version) {
break; // 数据一致,可以使用
}
old_version = version_before;
} while (1);

双缓冲机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 对端:使用两个缓冲区交替
struct double_buffer {
char buffer[2][BUFFER_SIZE];
volatile int active_index; // 当前活跃缓冲区索引
};

// 对端:修改数据
void update_data(struct double_buffer *db) {
int write_index = 1 - db->active_index; // 写入非活跃缓冲区
prepare_data(db->buffer[write_index]);
__sync_synchronize();
__sync_lock_test_and_set(&db->active_index, write_index); // 切换缓冲区
}

// 发起端:读取数据
void read_data(struct double_buffer *db) {
int read_index = db->active_index; // 读取当前活跃缓冲区
rdma_read(db->buffer[read_index]); // RDMA Read
// 即使对端切换缓冲区,读取的也是完整的数据
}

同步机制选择建议

场景 推荐机制 原因
需要通知对端数据已写入 RDMA Write with Immediate 高性能 + 通知
需要严格顺序 Fence + Send/Recv 保证操作顺序
需要检测数据变化 版本号机制 可以检测并发修改
需要无锁读取 双缓冲机制 避免锁竞争
需要细粒度控制 原子标志位 精确控制读写时机

应用与实践

应用场景对比

场景类型 Send/Recv Read/Write
请求-响应模式 ✅ 适合(RPC、数据库查询) ❌ 不适合
需要流控 ✅ 适合(接收端处理能力有限) ❌ 不适合
需要确认 ✅ 适合(事务提交、状态同步) ❌ 不适合
小数据频繁交互 ✅ 适合 ❌ 不适合
协议实现 ✅ 适合(分布式一致性协议) ❌ 不适合
高性能计算(HPC) ❌ 不适合 ✅ 适合(科学计算、数值模拟)
AI 训练 ❌ 不适合 ✅ 适合(参数同步,对端 CPU 繁忙)
存储系统 ❌ 不适合 ✅ 适合(块存储、文件系统)
批量数据传输 ❌ 不适合 ✅ 适合(大数据传输)
零拷贝需求 ❌ 不适合 ✅ 适合(避免 CPU 参与)

混合使用策略

在实际应用中,可以混合使用两种模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 示例:使用 Send/Recv 进行控制,使用 Write 进行数据传输

// 1. 通过 Send/Recv 交换元数据
struct metadata {
uint64_t data_addr;
uint32_t data_rkey;
size_t data_size;
} meta;
send_metadata(&meta);

// 2. 通过 RDMA Write 传输实际数据
rdma_write_data(meta.data_addr, meta.data_rkey, data_buffer, meta.data_size);

// 3. 通过 Send/Recv 发送完成通知
send_completion_notification();
用途 使用的模式 原因
元数据交换 Send/Recv 需要确认和流控
数据传输 Read/Write 需要最高性能
完成通知 Send/Recv 需要确认

性能优化

优化策略 Send/Recv Read/Write
批量操作 批量提交 Receive WR 批量提交 Read/Write WR
Signaling 策略 周期性 Signaling(每 N 个请求) 避免频繁 Signaling
内存对齐 建议对齐 强烈建议对齐(64 字节)
队列大小 合理设置 max_recv_wr 合理设置 max_send_wr
RNR 参数 合理设置 rnr_retry 和 min_rnr_timer 不适用

Send/Recv 性能优化代码

1
2
3
4
5
6
7
8
9
// 1. 批量提交 Receive WR
for (int i = 0; i < BATCH_SIZE; i++) {
post_recv_wr();
}

// 2. 周期性 Signaling
if (count % 64 == 0) {
send_wr.send_flags |= IBV_SEND_SIGNALED;
}

Read/Write 性能优化代码

1
2
3
4
5
6
7
8
9
// 1. 批量操作
struct ibv_send_wr *wr_list = build_wr_list();
ibv_post_send(qp, wr_list, &bad_wr);

// 2. 避免频繁 Signaling
write_wr.send_flags = 0; // 不请求完成通知

// 3. 内存对齐
posix_memalign((void**)&buffer, 64, size); // 64 字节对齐

注意事项

注意事项 Send/Recv Read/Write
必须预先 post recv ✅ 必须 ❌ 不需要
及时处理 CQ 事件 ✅ 必须(避免 CQ 溢出) ✅ 必须(仅发起端)
合理设置队列大小 ✅ 重要 ✅ 重要
内存访问权限 ✅ 需要 ✅ 需要(对端需设置 REMOTE 权限)
R_Key 安全 ❌ 不需要 ✅ 重要(需要安全传递)
同步机制 ✅ 通过 CQ 事件 ✅ 需要额外机制
内存边界检查 ✅ 需要 ✅ 需要(避免越界访问)
并发安全 ✅ 需要应用层同步 ✅ 需要应用层同步

总结

Send/Recv 和 Read/Write 代表了 InfiniBand/RDMA 的两种不同设计哲学:

模式 设计哲学 核心特点
Send/Recv 协作和流控 接收端参与,有流控机制,适合需要确认的场景
Read/Write 极致性能和 CPU 卸载 对端 CPU 不参与,无流控,适合高性能场景

在实际应用中,应根据具体场景的需求,灵活选择或组合使用这两种模式,充分发挥 InfiniBand/RDMA 的性能优势。

概述

InfiniBand 是一种高性能计算机网络通信标准,具有极高的吞吐量和极低的延迟。本文介绍 InfiniBand/RDMA 编程中的关键概念及其相互关系。

核心概念

1. CA (Channel Adapter) - 通道适配器

CA 是 InfiniBand 网络接口卡(NIC),是硬件层面的概念。每个 CA 都有一个或多个端口(Port),用于连接到 InfiniBand 网络。

2. PD (Protection Domain) - 保护域

保护域是一个安全边界,用于将 QP(队列对)和 MR(内存区域)组织在一起。只有属于同一个 PD 的 QP 和 MR 才能相互操作,这提供了内存保护机制。

3. MR (Memory Region) - 内存区域

内存区域是一块经过注册的内存,网卡可以直接访问。每个 MR 包含:

  • L_Key (Local Key): 本地访问密钥,用于本地 QP 访问本地 MR
  • R_Key (Remote Key): 远程访问密钥,用于远程 QP 访问此 MR(通过 RDMA 操作)

4. QP (Queue Pair) - 队列对

队列对是 InfiniBand 通信的基本单位,由两个队列组成:

  • SQ (Send Queue): 发送队列,用于发送数据
  • RQ (Receive Queue): 接收队列,用于接收数据

每个 QP 必须属于一个 PD,并且可以关联多个 CQ。

5. CQ (Completion Queue) - 完成队列

完成队列用于接收工作请求(WR)的完成通知。当 WR 执行完成后,会在对应的 CQ 中生成一个完成事件(Completion Event)。

6. WR (Work Request) - 工作请求

工作请求是提交到 QP 的操作指令,包括:

  • Send WR: 发送请求
  • Receive WR: 接收请求
  • RDMA Write WR: RDMA 写请求
  • RDMA Read WR: RDMA 读请求

7. SGE (Scatter/Gather Elements) - 分散/聚集元素

SGE 描述了一个内存缓冲区的位置和大小,包含:

  • 地址(Address)
  • 长度(Length)
  • L_Key(用于验证访问权限)

一个 WR 可以包含多个 SGE,实现分散/聚集 I/O。

注: SGE 专门用于描述本地散布的缓冲区, 即散布读写(Scatter read/write), 类似于 Linux 的 writev / readv 函数.

8. LID (Local Identifier) - 本地标识符

LID 是 InfiniBand 网络中每个端口的唯一标识符,用于路由数据包。

9. AH (Address Handle) - 地址句柄

地址句柄用于 UD (Unreliable Datagram) 传输类型,包含目标地址信息。每个 AH 属于一个 PD,用于在 UD QP 发送数据时指定目标地址。AH 包含:

  • 目标 LID (Local Identifier)
  • 服务级别 (Service Level)
  • 路径位 (Path Bits)
  • 全局路由头 (GRH) 信息(如果使用)

10. CM (Connection Manager) - 连接管理器

连接管理器负责建立和管理 QP 之间的连接,处理连接建立、断开等事件。

保护域(PD)资源组织结构图

以下 ASCII 图详细说明了 PD(保护域)内资源的结构和关系:

重要说明:图中 CQ 显示在 PD 内是为了展示逻辑关联关系。实际上:

  • CQ 通过 Context 创建(ibv_create_cq()),不属于任何 PD
  • CQ 是 Context 级别的资源,可以被不同 PD 的 QP 共享
  • 多个 QP(即使属于不同的 PD)可以关联到同一个 CQ
  • 例如:PD 1 的 QP 1、QP 2 和 PD 2 的 QP 3、QP 4 可以共享同一个 CQ
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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
┌─────────────────────────────────────────────────────────────────────┐
│ Application Process │
└─────────────────────────────────────────────────────────────────────┘

│ ibv_open_device()

┌─────────────────────────────────────────────────────────────────────┐
│ Context │
└─────────────────────────────────────────────────────────────────────┘

│ ibv_alloc_pd()

┌─────────────────────────────────────────────────────────┐
│ │
▼ ▼
┌──────────────────────────────────────┐ ┌──────────────────────────────────────┐
│ PD 1 (Protection Domain) │ │ PD 2 (Protection Domain) │
│ │ │ │
│ Resources in PD 1: │ │ Resources in PD 2: │
│ │ │ │
│ ┌────────────────────────────────┐ │ │ ┌────────────────────────────────┐ │
│ │ MR 1 │ │ │ │ MR 3 │ │
│ │ L_Key: 0x01 R_Key: 0x81 │ │ │ │ L_Key: 0x05 R_Key: 0x85 │ │
│ └────────────────────────────────┘ │ │ └────────────────────────────────┘ │
│ │ │ │
│ ┌────────────────────────────────┐ │ │ ┌────────────────────────────────┐ │
│ │ MR 2 │ │ │ │ MR 4 │ │
│ │ L_Key: 0x02 R_Key: 0x82 │ │ │ │ L_Key: 0x06 R_Key: 0x86 │ │
│ └────────────────────────────────┘ │ │ └────────────────────────────────┘ │
│ │ │ │
│ ┌────────────────────────────────┐ │ │ ┌────────────────────────────────┐ │
│ │ AH 1 │ │ │ │ AH 3 │ │
│ │ Target LID, Service Level │ │ │ │ Target LID, Service Level │ │
│ └────────────────────────────────┘ │ │ └────────────────────────────────┘ │
│ │ │ │
│ ┌────────────────────────────────┐ │ │ ┌────────────────────────────────┐ │
│ │ AH 2 │ │ │ │ AH 4 │ │
│ │ Target LID, Service Level │ │ │ │ Target LID, Service Level │ │
│ └────────────────────────────────┘ │ │ └────────────────────────────────┘ │
│ │ │ │
│ ┌────────────────────────────────┐ │ │ ┌────────────────────────────────┐ │
│ │ QP 1 │ │ │ │ QP 3 │ │
│ │ Uses: MR 1/2, AH 1/2, CQ 1 │ │ │ │ Uses: MR 3/4, AH 3/4, CQ 1/2 │ │
│ └────────────────────────────────┘ │ │ └────────────────────────────────┘ │
│ │ │ │
│ ┌────────────────────────────────┐ │ │ ┌────────────────────────────────┐ │
│ │ QP 2 │ │ │ │ QP 4 │ │
│ │ Uses: MR 1/2, AH 1/2, CQ 1 │ │ │ │ Uses: MR 3/4, AH 3/4, CQ 1/2 │ │
│ └────────────────────────────────┘ │ │ └────────────────────────────────┘ │
│ │ │ │
└──────────────────────────────────────┘ └──────────────────────────────────────┘
│ │
│ Security Boundary │ Security Boundary
└─────────────────────────────────────────────────────────┘



┌───────────────────────────────────────────────────────────────────┐
│ CQ (Created via Context, NOT belonging to any PD) │
│ CQ can be shared by QP from different PDs! │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ CQ 1 (Shared across PDs) │ │
│ │ Receives completions from: │ │
│ │ - QP 1 (PD 1) │ │
│ │ - QP 2 (PD 1) │ │
│ │ - QP 3 (PD 2) ← Cross-PD sharing │ │
│ │ - QP 4 (PD 2) ← Cross-PD sharing │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ CQ 2 (Alternative: separate CQ for PD 2) │ │
│ │ Receives completions from: │ │
│ │ - QP 3 (PD 2) │ │
│ │ - QP 4 (PD 2) │ │
│ └──────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘

│ Hardware Access

┌──────────────────┐
│ CA (NIC) │
└──────────────────┘

资源使用关系:
─────────────────────────────
QP 1 (PD 1) ──使用──> MR 1, MR 2, AH 1, AH 2
QP 1 (PD 1) ──发送完成事件到──> CQ 1

QP 2 (PD 1) ──使用──> MR 1, MR 2, AH 1, AH 2
QP 2 (PD 1) ──发送完成事件到──> CQ 1

QP 3 (PD 2) ──使用──> MR 3, MR 4, AH 3, AH 4
QP 3 (PD 2) ──发送完成事件到──> CQ 1 (共享) 或 CQ 2

QP 4 (PD 2) ──使用──> MR 3, MR 4, AH 3, AH 4
QP 4 (PD 2) ──发送完成事件到──> CQ 1 (共享) 或 CQ 2

关键点:CQ 可以被不同 PD 的 QP 共享!

关键关系说明:
═══════════════════════════════════════════════════════════════════

1. PD 作为安全边界
─────────────────
• PD 1 和 PD 2 相互隔离,互不干扰
• PD 直接管理的资源:MR、QP、AH
• CQ 通过 Context 创建,通过 QP 间接关联到 PD(QP 创建时指定 CQ)
• 只有属于同一个 PD 的 QP 和 MR 才能相互操作
• PD 提供了内存和队列的访问控制机制

2. MR (Memory Region) - 内存区域
──────────────────────────────
• MR 1 和 MR 2 属于 PD 1
• MR 3 和 MR 4 属于 PD 2
• 每个 MR 都有唯一的 L_Key(本地密钥)和 R_Key(远程密钥)
• MR 是 QP 可以访问的内存区域

3. QP (Queue Pair) - 队列对
──────────────────────────
• QP 1 和 QP 2 属于 PD 1
• QP 3 和 QP 4 属于 PD 2
• 每个 QP 包含:
- SQ (Send Queue): 发送队列,用于发送数据
- RQ (Receive Queue): 接收队列,用于接收数据
• QP 只能访问同一 PD 内的 MR

4. CQ (Completion Queue) - 完成队列
──────────────────────────────────
• CQ 通过 Context 创建(`ibv_create_cq()`),不属于任何 PD
• CQ 是 Context 级别的资源,可以被不同 PD 的 QP 共享
• 多个 QP(即使属于不同的 PD)可以关联到同一个 CQ
• 示例:
- CQ 1 可以同时接收 PD 1 的 QP 1、QP 2 和 PD 2 的 QP 3、QP 4 的完成事件
- 这种设计提供了灵活性,允许跨 PD 共享完成队列
• 注意:虽然图中 CQ 显示在 PD 内,但这是逻辑关联,CQ 本身不属于 PD

QP 如何将完成事件发送到 CQ:
────────────────────────────────
1. QP 创建时关联 CQ:
- 创建 QP 时,在 `ibv_qp_init_attr` 中指定 `send_cq` 和 `recv_cq`
- 每个 QP 可以有不同的 send_cq 和 recv_cq,也可以使用同一个 CQ

2. WR 标志控制完成事件生成:
- Send WR:设置 `IBV_SEND_SIGNALED` 标志,完成后在 send_cq 中生成 CQE
- Receive WR:总是生成完成事件(在 recv_cq 中生成 CQE)
- 未设置 SIGNALED 的 Send WR 不会生成完成事件

3. 硬件自动生成完成事件:
- 当 WR 执行完成后,硬件(CA)自动在对应的 CQ 中生成 CQE(Completion Queue Entry)
- CQE 包含:状态码、操作类型、WR ID、字节数等信息
- 应用程序通过 `ibv_poll_cq()` 轮询 CQ 获取完成事件

4. 完成事件流程:
Application → ibv_post_send/recv() → QP (SQ/RQ)

WR 执行完成

硬件生成 CQE

写入到 CQ

Application ← ibv_poll_cq()

5. AH (Address Handle) - 地址句柄
────────────────────────────────
• AH 1 和 AH 2 属于 PD 1
• AH 3 和 AH 4 属于 PD 2
• AH 主要用于 UD (Unreliable Datagram) 传输类型
• UD QP 发送数据时,WR 必须包含 AH 来指定目标地址
• AH 包含目标 LID、服务级别等路由信息

6. WR (Work Request) - 工作请求
──────────────────────────────
• WR 提交到 QP 的 SQ 或 RQ
• WR 包含一个或多个 SGE(Scatter/Gather Elements)
• SGE 引用 MR,使用 L_Key 验证访问权限
• UD QP 的 Send WR 必须包含 AH 来指定目标地址

7. 访问规则
─────────
✅ 允许:QP 1 → MR 1 (同一 PD)
✅ 允许:QP 1 → MR 2 (同一 PD)
✅ 允许:QP 2 → MR 1 (同一 PD)
✅ 允许:QP 2 → MR 2 (同一 PD)
✅ 允许:QP 3 → MR 3 (同一 PD)
✅ 允许:QP 3 → MR 4 (同一 PD)
✅ 允许:QP 4 → MR 3 (同一 PD)
✅ 允许:QP 4 → MR 4 (同一 PD)
✅ 允许:QP 1 → CQ 1 (CQ 可以被不同 PD 共享)
✅ 允许:QP 2 → CQ 1 (CQ 可以被不同 PD 共享)
✅ 允许:QP 3 → CQ 1 (CQ 可以被不同 PD 共享,跨 PD)
✅ 允许:QP 3 → CQ 2 (CQ 可以被不同 PD 共享)
✅ 允许:QP 4 → CQ 1 (CQ 可以被不同 PD 共享,跨 PD)
✅ 允许:QP 4 → CQ 2 (CQ 可以被不同 PD 共享)
✅ 允许:QP 1 → AH 1 (同一 PD,UD QP)
✅ 允许:QP 1 → AH 2 (同一 PD,UD QP)
✅ 允许:QP 2 → AH 1 (同一 PD,UD QP)
✅ 允许:QP 2 → AH 2 (同一 PD,UD QP)
✅ 允许:QP 3 → AH 3 (同一 PD,UD QP)
✅ 允许:QP 3 → AH 4 (同一 PD,UD QP)
✅ 允许:QP 4 → AH 3 (同一 PD,UD QP)
✅ 允许:QP 4 → AH 4 (同一 PD,UD QP)
❌ 禁止:QP 1 → MR 3 (不同 PD)
❌ 禁止:QP 1 → MR 4 (不同 PD)
❌ 禁止:QP 3 → MR 1 (不同 PD)
❌ 禁止:QP 3 → MR 2 (不同 PD)
❌ 禁止:QP 1 → AH 3 (不同 PD)
❌ 禁止:QP 1 → AH 4 (不同 PD)
❌ 禁止:QP 3 → AH 1 (不同 PD)
❌ 禁止:QP 3 → AH 2 (不同 PD)

8. 内存保护机制
──────────────
• L_Key: 用于本地 QP 访问本地 MR
- QP 1 使用 L_Key 0x01 访问 MR 1
- QP 1 使用 L_Key 0x02 访问 MR 2
- SGE 中必须包含正确的 L_Key 才能访问 MR

• R_Key: 用于远程 RDMA 操作
- 远程 QP 使用 R_Key 0x81 进行 RDMA Write/Read 到 MR 1
- 远程 QP 使用 R_Key 0x82 进行 RDMA Write/Read 到 MR 2
- RDMA 操作时,远程端必须提供正确的 R_Key

9. 资源创建顺序
──────────────
1. 创建 Context (ibv_open_device)
2. 创建 PD (ibv_alloc_pd)
3. 注册 MR (ibv_reg_mr) - 需要 PD
4. 创建 CQ (ibv_create_cq) - 需要 Context
5. 创建 AH (ibv_create_ah) - 需要 PD(仅 UD QP 需要)
6. 创建 QP (ibv_create_qp) - 需要 PD 和 CQ
7. 提交 WR (ibv_post_send/recv) - 需要 QP 和 MR(UD QP 还需要 AH)

10. 实际应用场景
──────────────
• 多租户隔离:不同应用使用不同 PD,确保安全隔离
• 资源管理:同一应用的不同模块可以使用不同 PD
• 权限控制:通过 PD 限制哪些 QP 可以访问哪些 MR
• 性能优化:合理组织 PD 内的资源,减少跨 PD 访问开销

概念关系图

以下 Mermaid 图表展示了 InfiniBand 关键概念之间的关系:

graph TB
    subgraph Hardware["硬件层"]
        CA[CA
Channel Adapter
通道适配器] Port[Port
端口] end subgraph Context["上下文层"] Context_Obj[Context
上下文] PD[PD
Protection Domain
保护域] end subgraph Memory["内存管理"] MR[MR
Memory Region
内存区域] LKey[L_Key
本地密钥] RKey[R_Key
远程密钥] Buffer[Buffer
缓冲区] end subgraph Queue["队列层"] QP[QP
Queue Pair
队列对] SQ[SQ
Send Queue
发送队列] RQ[RQ
Receive Queue
接收队列] CQ[CQ
Completion Queue
完成队列] end subgraph Operation["操作层"] WR[WR
Work Request
工作请求] SGE[SGE
Scatter/Gather Elements
分散/聚集元素] SendWR[Send WR] RecvWR[Receive WR] RDMAWriteWR[RDMA Write WR] RDMAReadWR[RDMA Read WR] end subgraph Network["网络层"] LID[LID
Local Identifier
本地标识符] CM[CM
Connection Manager
连接管理器] end %% Hardware relationships CA --> Port %% Context relationships CA --> Context_Obj Context_Obj --> PD %% Memory relationships PD --> MR MR --> LKey MR --> RKey MR --> Buffer %% Queue relationships PD --> QP QP --> SQ QP --> RQ QP --> CQ CQ --> QP %% Operation relationships SQ --> WR RQ --> WR WR --> SGE SGE --> MR SGE --> LKey WR --> SendWR WR --> RecvWR WR --> RDMAWriteWR WR --> RDMAReadWR RDMAWriteWR --> RKey RDMAReadWR --> RKey %% Network relationships Port --> LID CM --> QP %% Completion flow WR -->|完成通知| CQ style CA fill:#e1f5ff style PD fill:#fff4e1 style MR fill:#e8f5e9 style QP fill:#f3e5f5 style CQ fill:#fce4ec style WR fill:#fff9c4

数据流关系图

以下图表展示了数据在 InfiniBand 系统中的流动路径:

sequenceDiagram
    participant App as 应用程序
    participant QP as Queue Pair
    participant SQ as Send Queue
    participant RQ as Receive Queue
    participant CQ as Completion Queue
    participant MR as Memory Region
    participant CA as Channel Adapter
    participant Network as InfiniBand网络
    
    Note over App,Network: 发送数据流程
    App->>MR: 注册内存区域
    App->>SQ: 提交 Send WR (包含 SGE)
    SQ->>CA: 处理工作请求
    CA->>Network: 发送数据包
    Network->>CA: 确认/完成
    CA->>CQ: 生成完成事件
    CQ->>App: 通知应用完成
    
    Note over App,Network: 接收数据流程
    App->>MR: 注册内存区域
    App->>RQ: 提交 Receive WR (包含 SGE)
    Network->>CA: 接收数据包
    CA->>RQ: 匹配 Receive WR
    CA->>MR: 写入数据到内存
    CA->>CQ: 生成完成事件
    CQ->>App: 通知应用完成
    
    Note over App,Network: RDMA Write 流程
    App->>MR: 注册内存区域(获取R_Key)
    App->>SQ: 提交 RDMA Write WR (包含R_Key)
    SQ->>CA: 处理 RDMA Write
    CA->>Network: 发送 RDMA Write 请求
    Network->>CA: 远程CA接收请求
    CA->>MR: 直接写入远程内存(无需CPU参与)
    CA->>CQ: 生成完成事件
    CQ->>App: 通知应用完成

层次结构图

以下图表展示了 InfiniBand 编程模型的层次结构:

graph TD
    subgraph Level1["应用层"]
        App[应用程序]
    end
    
    subgraph Level2["Verbs API层"]
        Verbs[ibVerbs API]
    end
    
    subgraph Level3["资源管理层"]
        PD_Res[PD: 保护域]
        MR_Res[MR: 内存区域]
        QP_Res[QP: 队列对]
        CQ_Res[CQ: 完成队列]
    end
    
    subgraph Level4["操作层"]
        WR_Op[WR: 工作请求]
        SGE_Op[SGE: 分散/聚集元素]
    end
    
    subgraph Level5["硬件层"]
        CA_HW[CA: 通道适配器]
        Port_HW[Port: 端口]
    end
    
    App --> Verbs
    Verbs --> PD_Res
    Verbs --> MR_Res
    Verbs --> QP_Res
    Verbs --> CQ_Res
    QP_Res --> WR_Op
    WR_Op --> SGE_Op
    SGE_Op --> MR_Res
    WR_Op --> CQ_Res
    QP_Res --> CA_HW
    CA_HW --> Port_HW
    
    style Level1 fill:#e3f2fd
    style Level2 fill:#f1f8e9
    style Level3 fill:#fff3e0
    style Level4 fill:#fce4ec
    style Level5 fill:#e0f2f1

关键概念总结表

概念 英文全称 作用 关联对象
CA Channel Adapter 硬件网卡 Port
PD Protection Domain 安全边界 QP, MR, AH (CQ通过QP间接关联)
MR Memory Region 注册的内存区域 PD, L_Key, R_Key
QP Queue Pair 通信端点 PD, SQ, RQ, CQ
SQ Send Queue 发送队列 QP
RQ Receive Queue 接收队列 QP
CQ Completion Queue 完成队列 QP, WR
AH Address Handle 地址句柄 PD, UD QP
WR Work Request 工作请求 QP, SGE, AH
SGE Scatter/Gather Elements 内存描述符 MR, L_Key
L_Key Local Key 本地访问密钥 MR
R_Key Remote Key 远程访问密钥 MR
LID Local Identifier 本地标识符 Port
CM Connection Manager 连接管理器 QP

编程流程

典型的 InfiniBand 编程流程:

  1. 打开设备: ibv_open_device() - 获取 Context
  2. 分配保护域: ibv_alloc_pd() - 创建 PD
  3. 注册内存: ibv_reg_mr() - 创建 MR,获得 L_Key 和 R_Key
  4. 创建完成队列: ibv_create_cq() - 创建 CQ
  5. 创建地址句柄: ibv_create_ah() - 创建 AH(仅 UD QP 需要)
  6. 创建队列对: ibv_create_qp() - 创建 QP,关联 CQ
  7. 建立连接: 使用 CM 或手动配置 QP 状态
  8. 提交工作请求: ibv_post_send(), ibv_post_recv() - 提交 WR(UD QP 的 Send WR 需要包含 AH)
  9. 轮询完成: ibv_poll_cq() - 检查完成事件
  10. 清理资源: 销毁 QP, AH, CQ, MR, PD,关闭设备

参考资料

概述

动态插桩(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 指不好的预测,尤其是分支预测。