各位好,欢迎来到今天的“C++ CPU 亲和性:让你的线程找到真爱”讲座。今天我们要聊聊一个听起来高深莫测,但实际上非常实用的小技巧:CPU 亲和性。
什么是CPU亲和性?
简单来说,CPU亲和性就是让你的线程或进程“爱上”某个特定的CPU核心。默认情况下,操作系统会尽力均衡各个核心的负载,线程可能会在不同的核心之间跳来跳去。这就像一个花心的家伙,一会儿喜欢这个,一会儿喜欢那个,最终导致性能下降(因为缓存失效)。
CPU亲和性就像是给线程找了个“真爱”,告诉它:“你就待在这个核心里,别乱跑了!” 这样可以减少线程在不同核心之间迁移的次数,提高缓存命中率,从而提升性能。
为什么要设置CPU亲和性?
想象一下,你正在玩一个大型游戏。游戏需要大量的计算,而这些计算被分配到多个线程上。如果没有设置CPU亲和性,这些线程可能会在不同的CPU核心上运行。
- 缓存失效: 当线程从一个核心迁移到另一个核心时,之前核心上的缓存数据就失效了,需要重新加载。这会增加延迟,降低性能。
- NUMA问题: 在NUMA(Non-Uniform Memory Access)架构的系统中,访问本地内存比访问远程内存更快。如果线程在不同的NUMA节点之间迁移,会增加内存访问延迟。
- 资源竞争: 当多个线程竞争同一个资源时,如果它们在不同的核心上运行,可能会增加锁的竞争,导致性能下降。
设置CPU亲和性可以解决这些问题,让线程稳定地在一个核心上运行,提高缓存命中率,减少内存访问延迟,降低资源竞争。
如何设置CPU亲和性?
C++本身并没有直接设置CPU亲和性的功能,我们需要借助操作系统提供的API。不同的操作系统有不同的API,下面我们分别介绍Linux和Windows下的设置方法。
1. Linux下的CPU亲和性设置
在Linux下,我们可以使用sched_setaffinity
函数来设置线程的CPU亲和性。
-
头文件:
<sched.h>
-
函数原型:
int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);
pid
: 进程ID,如果设置为0,表示设置当前线程的亲和性。cpusetsize
:mask
的大小,通常设置为sizeof(cpu_set_t)
。mask
: 一个cpu_set_t
类型的位掩码,用于指定允许线程运行的CPU核心。
-
cpu_set_t
类型: 这是一个位掩码,每一位代表一个CPU核心。如果某一位被设置为1,表示允许线程在该核心上运行;如果设置为0,表示不允许。 -
相关宏:
CPU_SET(cpu, &cpuset)
: 将cpuset
的第cpu
位设置为1。CPU_CLR(cpu, &cpuset)
: 将cpuset
的第cpu
位设置为0。CPU_ZERO(&cpuset)
: 将cpuset
的所有位设置为0。CPU_COUNT(&cpuset)
: 返回cpuset
中被设置为1的位的数量。CPU_ISSET(cpu, &cpuset)
: 检查cpuset
的第cpu
位是否被设置为1。
示例代码:
#define _GNU_SOURCE
#include <iostream>
#include <pthread.h>
#include <sched.h>
#include <unistd.h>
#include <sys/syscall.h> // For gettid()
using namespace std;
// 获取线程ID,兼容不同系统
pid_t get_thread_id() {
#ifdef __linux__
return syscall(SYS_gettid);
#else
return pthread_self();
#endif
}
void* thread_func(void* arg) {
int cpu_id = *(int*)arg;
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
pthread_t current_thread = pthread_self();
int rc = pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset);
if (rc != 0) {
cerr << "Error setting affinity for thread " << get_thread_id() << ": " << rc << endl;
return NULL;
}
cout << "Thread " << get_thread_id() << " is running on CPU " << cpu_id << endl;
// 模拟一些工作
for (int i = 0; i < 100000000; ++i) {
// 啥也不干,就消耗CPU
}
return NULL;
}
int main() {
pthread_t threads[4];
int cpu_ids[4] = {0, 1, 2, 3};
for (int i = 0; i < 4; ++i) {
int rc = pthread_create(&threads[i], NULL, thread_func, &cpu_ids[i]);
if (rc != 0) {
cerr << "Error creating thread " << i << ": " << rc << endl;
return 1;
}
}
for (int i = 0; i < 4; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
代码解释:
-
#define _GNU_SOURCE
: 这个宏定义是必需的,因为它启用了sched.h
中的某些功能,比如pthread_setaffinity_np
。 -
pthread_setaffinity_np
: 这个函数是POSIX线程库提供的,用于设置线程的CPU亲和性。它的参数和sched_setaffinity
类似,但是它接受的是pthread_t
类型的线程ID,而不是进程ID。 -
get_thread_id()
: 这个函数用于获取当前线程的ID,在Linux下使用syscall(SYS_gettid)
,在其他系统下使用pthread_self()
。 -
cpu_set_t
: 这是一个位掩码,用于指定允许线程运行的CPU核心。我们使用CPU_ZERO
宏将其初始化为0,然后使用CPU_SET
宏将指定的CPU核心对应的位设置为1。 -
错误处理: 代码中加入了错误处理,当设置CPU亲和性失败时,会打印错误信息。
-
模拟工作: 在线程函数中,我们模拟了一些工作,让线程占用CPU一段时间,以便观察CPU亲和性的效果。
编译和运行:
g++ -o affinity affinity.cpp -pthread
./affinity
运行结果应该类似于:
Thread 140737353246720 is running on CPU 0
Thread 140737353254912 is running on CPU 1
Thread 140737353263104 is running on CPU 2
Thread 140737353271296 is running on CPU 3
你可以使用top
或htop
命令来观察CPU的使用情况,你会发现每个线程都运行在指定的CPU核心上。
2. Windows下的CPU亲和性设置
在Windows下,我们可以使用SetThreadAffinityMask
函数来设置线程的CPU亲和性。
-
头文件:
<windows.h>
-
函数原型:
DWORD_PTR SetThreadAffinityMask(HANDLE hThread, DWORD_PTR dwThreadAffinityMask);
hThread
: 线程句柄。可以使用GetCurrentThread()
函数获取当前线程的句柄。dwThreadAffinityMask
: 一个位掩码,用于指定允许线程运行的CPU核心。
-
位掩码: 和Linux类似,Windows也使用位掩码来表示允许线程运行的CPU核心。每一位代表一个CPU核心。如果某一位被设置为1,表示允许线程在该核心上运行;如果设置为0,表示不允许。
示例代码:
#include <iostream>
#include <windows.h>
#include <process.h> // For _beginthreadex
using namespace std;
unsigned int __stdcall thread_func(void* arg) {
DWORD_PTR cpu_mask = (DWORD_PTR)(1ULL << *(int*)arg); // 1ULL ensures 64-bit integer
HANDLE current_thread = GetCurrentThread();
if (SetThreadAffinityMask(current_thread, cpu_mask) == 0) {
cerr << "Error setting affinity: " << GetLastError() << endl;
return 1;
}
cout << "Thread is running on CPU " << *(int*)arg << endl;
// 模拟一些工作
for (int i = 0; i < 100000000; ++i) {
// 啥也不干,就消耗CPU
}
return 0;
}
int main() {
HANDLE threads[4];
int cpu_ids[4] = {0, 1, 2, 3};
for (int i = 0; i < 4; ++i) {
threads[i] = (HANDLE)_beginthreadex(NULL, 0, thread_func, &cpu_ids[i], 0, NULL);
if (threads[i] == NULL) {
cerr << "Error creating thread " << i << ": " << GetLastError() << endl;
return 1;
}
}
WaitForMultipleObjects(4, threads, TRUE, INFINITE);
for (int i = 0; i < 4; ++i) {
CloseHandle(threads[i]);
}
return 0;
}
代码解释:
-
1ULL << *(int*)arg
: 这行代码将CPU核心的ID转换为位掩码。1ULL
表示一个无符号长长整型常量1,然后左移*(int*)arg
位,得到一个只有指定CPU核心对应的位为1的位掩码。 确保使用64位整数,避免核心数量过多导致溢出。 -
GetCurrentThread()
: 这个函数用于获取当前线程的句柄。 -
SetThreadAffinityMask()
: 这个函数用于设置线程的CPU亲和性。 -
_beginthreadex()
: 这个函数是Windows提供的创建线程的函数。 -
WaitForMultipleObjects()
: 这个函数用于等待所有线程结束。 -
CloseHandle()
: 这个函数用于关闭线程句柄。 -
错误处理: 代码中加入了错误处理,当设置CPU亲和性失败时,会打印错误信息。
-
模拟工作: 在线程函数中,我们模拟了一些工作,让线程占用CPU一段时间,以便观察CPU亲和性的效果。
编译和运行:
使用Visual Studio编译即可。
运行结果应该类似于:
Thread is running on CPU 0
Thread is running on CPU 1
Thread is running on CPU 2
Thread is running on CPU 3
你可以使用任务管理器来观察CPU的使用情况,你会发现每个线程都运行在指定的CPU核心上。
注意事项:
-
核心数量: 在设置CPU亲和性之前,需要先确定系统的CPU核心数量。可以使用
std::thread::hardware_concurrency()
函数来获取系统的CPU核心数量。 -
超线程: 超线程技术可以将一个物理核心模拟成两个逻辑核心。在设置CPU亲和性时,需要注意区分物理核心和逻辑核心。通常情况下,应该将线程绑定到不同的物理核心上,而不是同一个物理核心的两个逻辑核心上。
-
NUMA架构: 在NUMA架构的系统中,应该将线程绑定到与线程访问的内存区域相同的NUMA节点上,以减少内存访问延迟。
-
不要过度限制: 不要过度限制线程的CPU亲和性。如果将所有线程都绑定到同一个核心上,会导致该核心负载过重,而其他核心空闲,反而会降低性能。
-
测试: 设置CPU亲和性后,一定要进行充分的测试,以确保性能得到提升,而不是下降。
案例分析:优化图像处理应用
假设我们有一个图像处理应用,需要对大量的图像进行处理。图像处理的过程可以分解为多个独立的任务,每个任务可以分配给一个线程来执行。
如果没有设置CPU亲和性,这些线程可能会在不同的CPU核心上运行,导致缓存失效和NUMA问题。为了优化性能,我们可以将每个线程绑定到一个特定的CPU核心上。
// 假设我们有8个CPU核心
const int NUM_CORES = 8;
// 线程函数,处理图像
void* process_image(void* arg) {
int image_index = *(int*)arg;
int cpu_id = image_index % NUM_CORES; // 将图像索引映射到CPU核心
// 设置CPU亲和性 (Linux)
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(cpu_id, &cpuset);
pthread_t current_thread = pthread_self();
pthread_setaffinity_np(current_thread, sizeof(cpu_set_t), &cpuset);
cout << "Thread processing image " << image_index << " is running on CPU " << cpu_id << endl;
// 实际的图像处理代码
// ...
return NULL;
}
int main() {
// 假设我们有16张图像需要处理
const int NUM_IMAGES = 16;
pthread_t threads[NUM_IMAGES];
int image_indices[NUM_IMAGES];
// 创建线程
for (int i = 0; i < NUM_IMAGES; ++i) {
image_indices[i] = i;
pthread_create(&threads[i], NULL, process_image, &image_indices[i]);
}
// 等待线程结束
for (int i = 0; i < NUM_IMAGES; ++i) {
pthread_join(threads[i], NULL);
}
return 0;
}
在这个例子中,我们将每个图像处理任务分配给一个线程,并将线程绑定到一个特定的CPU核心上。我们使用图像索引对CPU核心数量取模,将图像均匀地分配到各个核心上。
通过设置CPU亲和性,我们可以提高缓存命中率,减少内存访问延迟,从而加速图像处理过程。
总结
CPU亲和性是一个强大的工具,可以用来优化多线程应用程序的性能。但是,它并不是万能的。在设置CPU亲和性之前,需要仔细分析应用程序的特点,并进行充分的测试,以确保性能得到提升。
记住,CPU亲和性就像婚姻,找对了就是幸福,找错了就是痛苦。所以,在给你的线程找“真爱”之前,一定要慎重考虑!
进阶话题:NUMA感知的CPU亲和性设置
在NUMA架构的系统中,更精细的控制可以带来更好的性能。我们需要考虑将线程绑定到与线程访问的内存区域相同的NUMA节点上。
-
numactl
工具 (Linux): Linux下可以使用numactl
工具来运行程序,并指定程序使用的NUMA节点。这可以确保程序分配的内存位于指定的NUMA节点上。numactl --cpunodebind=0 --membind=0 ./my_program
这条命令将
my_program
绑定到NUMA节点0,并确保程序分配的所有内存都位于NUMA节点0上。 -
GetNumaNodeProcessorMask
(Windows): Windows API提供了GetNumaNodeProcessorMask
函数,可以获取指定NUMA节点的CPU核心掩码。然后可以使用SetThreadAffinityMask
函数将线程绑定到该NUMA节点的CPU核心上。
总而言之,CPU亲和性是一个值得学习和掌握的技术。希望今天的讲座能帮助大家更好地理解和使用CPU亲和性,让你的C++程序跑得更快、更稳!感谢各位的聆听!