Skip to content

Commit 16fd356

Browse files
Merge pull request #2775 from jasonrandrews/review
tech review of CPU affinity
2 parents 458a740 + 28fdf4d commit 16fd356

5 files changed

Lines changed: 186 additions & 90 deletions

File tree

content/learning-paths/servers-and-cloud-computing/pinning-threads/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ cascade:
77

88
minutes_to_complete: 30
99

10-
who_is_this_for: Developers, performance engineers and system administrators looking to fine-tune the performance of their workload on many-core Arm-based systems.
10+
who_is_this_for: Developers, performance engineers and system administrators, looking to fine-tune the performance of their workload on many-core Arm-based systems.
1111

1212
learning_objectives:
1313
- Create CPU Sets and implement directly into sourcecode
Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
11
---
2-
title: Background Information
2+
title: Thread pinning fundamentals
33
weight: 2
44

55
### FIXED, DO NOT MODIFY
66
layout: learningpathall
77
---
88

9-
## Introduction
9+
## CPU affinity
1010

11+
CPU affinity is the practice of binding a process or thread to a specific CPU core or set of cores. This tells the operating system scheduler where that work is allowed to run. By default, the Linux scheduler dynamically migrates threads across cores to balance load and maximize overall throughput. Pinning overrides this behavior by constraining execution to a chosen set of cores.
1112

12-
CPU affinity is the practice of binding a process or thread to a specific CPU core or set of cores, telling the operating system scheduler where that work is allowed to run. By default the Linux scheduler dynamically migrates threads across cores to balance load and maximize overall throughput. Pinning overrides this behavior by constraining execution to a chosen set of cores.
13+
## Pinning
1314

14-
Pinning is most often used as a fine-tuning technique for workloads that aim to consume as many CPU cycles as possible while running alongside other demanding applications. Scientific computing pipelines and real time analytics frequently fall into this category. Typical applications that pin processes to specific cores are often sensitive to latency variation rather than just average throughput or have intricate memory access patterns. Pinning can reduce this noise and provide more consistent execution behavior or better memory access patterns under load.
15+
Pinning is most often used as a fine-tuning technique for workloads that aim to consume as many CPU cycles as possible while running alongside other demanding applications. Scientific computing pipelines and real-time analytics frequently fall into this category.
1516

16-
Another important motivation is memory locality. On modern systems with Non Uniform Memory Access architectures (NUMA), different cores have memory access times and characteristics depending on where the data is fetched from. For example, in a server with 2 CPU sockets, that from a programmers view appears as a single processor, would have different memory access times depending on the core. By pinning threads to cores that are close to the memory they use and allocating memory accordingly, an application can reduce memory access latency and improve bandwidth.
17+
Applications that pin processes to specific cores are often sensitive to latency variation rather than just average throughput. They may also have intricate memory access patterns. Pinning can reduce execution noise and provide more consistent behavior or better memory access patterns under load.
1718

18-
Developers can set affinity directly in source code using system calls. Many parallel frameworks expose higher level controls such as OpenMP affinity settings that manage thread placement automatically. Alternatively, at runtime system administrators can pin existing processes using utilities like `taskset` or launch applications with `NUMACTL` to control both CPU and memory placement without modifying code.
19+
## Memory locality
1920

20-
Pinning is a tradeoff. It can improve determinism and locality but it can also reduce flexibility and hurt performance if the chosen layout is suboptimal or if system load changes. Over constraining the scheduler may lead to idle cores while pinned threads contend unnecessarily. As a general rule it is best to rely on the operating system scheduler as a first pass and introduce pinning only if you are looking to fine-tune performance.
21+
Memory locality provides another important motivation for pinning. On modern systems with Non-Uniform Memory Access (NUMA) architectures, different cores have varying memory access times and characteristics. The performance depends on where the data is fetched from.
22+
23+
For example, in a server with two CPU sockets that appears as a single processor, memory access times differ depending on which core you use. By pinning threads to cores that are close to the memory they use and allocating memory accordingly, you can reduce memory access latency and improve bandwidth.
24+
25+
## Setting affinity
26+
27+
You can set affinity directly in source code using system calls. Many parallel frameworks expose higher-level controls, such as OpenMP affinity settings, that manage thread placement automatically.
28+
29+
Alternatively, at runtime, system administrators can pin existing processes using utilities like `taskset` or launch applications with `numactl` to control both CPU and memory placement without modifying code.
30+
31+
## Conclusion
32+
33+
Pinning is a tradeoff. It can improve determinism and locality, but it can also reduce flexibility and hurt performance if the chosen layout isn't optimal or if system load changes. Over-constraining the scheduler may lead to idle cores while pinned threads contend unnecessarily.
34+
35+
As a general rule, rely on the operating system scheduler initially and introduce pinning only when you're looking to fine-tune performance after measuring baseline behavior.

content/learning-paths/servers-and-cloud-computing/pinning-threads/setup.md

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: Setup
2+
title: Create a CPU-intensive program
33
weight: 3
44

55
### FIXED, DO NOT MODIFY
@@ -8,60 +8,58 @@ layout: learningpathall
88

99
## Setup
1010

11-
In this example we will be using an AWS Graviton 3 `m7g.4xlarge` instance running Ubuntu 22.04 LTS, based on the Arm Neoverse V1 architecture. If you are unfamiliar with creating a cloud instance, please refer to our [getting started learning path](https://learn.arm.com/learning-paths/servers-and-cloud-computing/csp/).
11+
This Learning Path works on any Arm Linux system with four or more CPU cores.
1212

13-
This learning path is expected to work on any linux-based Arm instance with 4 or more CPU cores. The `m7g.4xlarge` instance has a uniform processor architecture so there is neglible different in memory or CPU core performance across the cores. On Linux, this can easily be checked with the following command.
13+
For example, you can use an AWS Graviton 3 `m7g.4xlarge` instance running Ubuntu 24.04 LTS, based on the Arm Neoverse V1 architecture.
14+
15+
If you're unfamiliar with creating a cloud instance, refer to [Get started with Arm-based cloud instances](/learning-paths/servers-and-cloud-computing/csp/).
16+
17+
The `m7g.4xlarge` instance has a uniform processor architecture, so there's no difference in memory or CPU core performance across the cores.
18+
19+
On Linux, you can check this with the following command:
1420

1521
```bash
1622
lscpu | grep -i numa
1723
```
1824

19-
For our `m7g.4xlarge` all 16 cores are in the same NUMA (non-uniform memory architecture) node.
25+
For the `m7g.4xlarge`, all 16 cores are in the same NUMA node:
2026

21-
```out
27+
```output
2228
NUMA node(s): 1
2329
NUMA node0 CPU(s): 0-15
2430
```
2531

26-
First we will demonstrate how we can pin threads easily using the `taskset` utility available in Linux. This is used to set or retrieve the CPU affinity of a running process or set the affinity of a process about to be launched. This does not require any modifications to the source code.
32+
You'll first learn how to pin threads using the `taskset` utility available in Linux.
2733

34+
This utility sets or retrieves the CPU affinity of a running process or sets the affinity of a process about to be launched. This approach doesn't require any modifications to the source code.
2835

29-
## Install Prerequisites
36+
## Install prerequisites
3037

31-
Run the following commands:
38+
Run the following commands to install the required packages:
3239

3340
```bash
34-
sudo apt update && sudo apt install g++ cmake python3.12-venv -y
41+
sudo apt update && sudo apt install g++ cmake python3-venv python-is-python3 -y
3542
```
3643

37-
Install Google's Microbenchmarking support library.
44+
Install Google's microbenchmarking support library:
3845

3946
```bash
40-
# Check out the library.
4147
git clone https://github.com/google/benchmark.git
42-
# Go to the library root directory
4348
cd benchmark
44-
# Make a build directory to place the build output.
4549
cmake -E make_directory "build"
46-
# Generate build system files with cmake, and download any dependencies.
4750
cmake -E chdir "build" cmake -DBENCHMARK_DOWNLOAD_DEPENDENCIES=on -DCMAKE_BUILD_TYPE=Release ../
48-
# or, starting with CMake 3.13, use a simpler form:
49-
# Build the library.
5051
sudo cmake --build "build" --config Release --target install -j $(nproc)
5152
```
52-
If you have issues building and installing, please refer to the [official installation guide](https://github.com/google/benchmark).
5353

54-
Finally, you will need to install the Linux perf utility for measuring performance. We recommend using our [install guide](https://learn.arm.com/install-guides/perf/). As you may need to build from source.
54+
If you have issues building and installing, visit the [Benchmark repository](https://github.com/google/benchmark).
5555

56-
## Example 1
56+
Finally, install the Linux perf utility for measuring performance. See the [Linux Perf install guide](/install-guides/perf/) as you may need to build from source.
5757

58-
To demonstrate a use case of CPU affinity, we will create a program that heavily utilizes all the available CPU cores. Create a file named `use_all_cores.cpp` and paste in the source code below. In this example, we are repeatedly calculating the [Leibniz equation](https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80) to compute the value of Pi. This is a computationally inefficient algorithm to calculate the value of Pi and we are splitting the work across many threads.
58+
## Create a CPU-intensive example program
5959

60-
```bash
61-
cd ~
62-
touch use_all_cores.cpp && chmod 755 use_all_cores.cpp
63-
```
60+
To demonstrate CPU affinity, you'll create a program that heavily utilizes all available CPU cores. This example repeatedly calculates the [Leibniz equation](https://en.wikipedia.org/wiki/Leibniz_formula_for_%CF%80) to compute the value of Pi. This is a computationally inefficient algorithm to calculate Pi, and you'll split the work across many threads.
6461

62+
Use an editor to create a file named `use_all_cores.cpp` with the code below:
6563

6664
```cpp
6765
#include <vector>
@@ -81,7 +79,6 @@ double multiplethreaded_leibniz(int terms, bool use_all_cores){
8179
}
8280
std::vector<double> partial_results(NUM_THREADS);
8381

84-
8582
auto calculation = [&](int thread_id){
8683
// Lambda function that does the calculation of the Leibniz equation
8784
double denominator = 0.0;
@@ -100,7 +97,6 @@ double multiplethreaded_leibniz(int terms, bool use_all_cores){
10097
}
10198
};
10299

103-
104100
std::vector<thread> threads;
105101
for (int i = 0; i < NUM_THREADS; i++){
106102
threads.push_back(std::thread(calculation, i));
@@ -139,18 +135,37 @@ int main(){
139135
}
140136
```
141137
142-
Compile the program with the following command.
138+
Compile the program with the following command:
143139
144140
```bash
145141
g++ -O2 --std=c++11 use_all_cores.cpp -o prog
146142
```
147143

148-
In a separate terminal we can use the `top` utility to quickly view the utilization of each core. For example, run the following command and press the number `1`. Then we can run the program by entering `./prog`.
144+
## Observe CPU utilization
145+
146+
Now that you've compiled the program, you can observe how it utilizes CPU cores. In a separate terminal, use the `top` utility to view the utilization of each core:
147+
148+
```bash
149+
top -d 0.1
150+
```
151+
152+
Press the number `1` to view per-core utilization.
153+
154+
Then run the program in the other terminal:
149155

150156
```bash
151-
top -d 0.1 # then press 1 to view per core utilization
157+
./prog
152158
```
153159

154-
![CPU-utilization](./CPU-util.jpg)
160+
![Screenshot of the top command showing CPU utilization with all 16 cores periodically reaching 100% usage, displayed in a dark terminal window with percentage bars for each CPU core](cpu_util.jpg "CPU utilization showing all cores being used")
161+
162+
You should observe all cores on your system being periodically utilized up to 100% and then dropping to idle until the program exits.
163+
164+
## What you've accomplished and what's next
165+
166+
In this section, you've:
167+
- Set up an AWS Graviton 3 instance and installed the required tools
168+
- Created a multi-threaded program that heavily utilizes all available CPU cores
169+
- Observed how the program distributes work across cores using the `top` utility
155170

156-
As the screenshot above shows, you should observe all cores on your system being periodically utilized up to 100% and then down to idle until the program exits. In the next section we will look at how to bind this program to specific CPU cores when running alongside a single-threaded Python script.
171+
In the next section, you'll learn how to bind this program to specific CPU cores when running alongside a single-threaded Python script.

content/learning-paths/servers-and-cloud-computing/pinning-threads/thread_affinity.md

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
---
2-
title: CPU Affinity
2+
title: Set CPU affinity in source code
33
weight: 5
44

55
### FIXED, DO NOT MODIFY
66
layout: learningpathall
77
---
88

9-
## Pinning Threads at Source-Code Level
9+
## Pin threads at the source code level
1010

11-
Another way to set CPU affinity is at the source code level, this allows developers to be more expressive as to which thread goes where at specific points during the runtime. For example, in a hot path that repeatedly updates shared state with a read-modify-write style, a pinned thread could avoids excessive cache invalidations due to other threads modifying data.
11+
Another way to set CPU affinity is at the source code level. This allows you to be more expressive about which thread goes where at specific points during runtime.
1212

13-
To demonstrate this we have an example program below. Copy and paste the code below into a new file named `default_os_scheduling.cpp`.
13+
For example, in a hot path that repeatedly updates shared state with a read-modify-write pattern, a pinned thread can avoid excessive cache invalidations caused by other threads modifying data.
14+
15+
## Create a baseline program without thread pinning
16+
17+
To demonstrate this, you'll create two example programs. The first uses the default OS scheduling without thread pinning.
18+
19+
Copy and paste the code below into a new file named `default_os_scheduling.cpp`:
1420

1521
```cpp
1622
#include <benchmark/benchmark.h>
@@ -70,9 +76,11 @@ BENCHMARK(default_os_scheduling)->UseRealTime()->Unit(benchmark::kMillisecond);
7076
BENCHMARK_MAIN();
7177
```
7278
73-
`default_os_scheduling.cpp` has 2 atomic variables that are aligned on different cache lines to avoid thrashing. We spawn 4 threads, with 2 threads performing a read-modify-wite operation on the first atomic variable, and the final 2 threads performing the same operation on the second atomic variable.
79+
This program has two atomic variables that are aligned on different cache lines to avoid thrashing. You spawn four threads: two threads perform a read-modify-write operation on the first atomic variable, and two threads perform the same operation on the second atomic variable.
7480
75-
Now, copy the code block below into a file named `thread_affinity.cpp`.
81+
## Create a program with explicit thread pinning
82+
83+
Now, copy the code below into a new file named `thread_affinity.cpp`:
7684
7785
```cpp
7886
#include <benchmark/benchmark.h>
@@ -150,25 +158,32 @@ BENCHMARK(thread_affinity)->UseRealTime()->Unit(benchmark::kMillisecond);
150158
BENCHMARK_MAIN();
151159
```
152160

153-
`Thread_affinity.cpp` uses the `pthread_set_affinity_np` function from the `pthread.h` header file to pin the 2 threads operating on atomic variable, `a`, to a specific CPU set and the other threads operating on atomic variable, `b`, to a different CPU.
161+
This program uses the `pthread_setaffinity_np` function from the `pthread.h` header file to pin threads. The two threads operating on atomic variable `a` are pinned to a specific CPU set, and the other threads operating on atomic variable `b` are pinned to a different CPU.
162+
163+
## Compile and benchmark the programs
154164

155-
Compile both programs with the following command.
165+
Compile both programs with the following commands:
156166

157167
```bash
158168
g++ default_os_scheduling.cpp -O3 -march=native -lbenchmark -lpthread -o default-os-scheduling
159169
g++ thread_affinity.cpp -O3 -march=native -lbenchmark -lpthread -o thread-affinity
160170
```
161171

162-
We will use the `perf` tool to print statistic for the program.
172+
Use Perf to print statistics for both programs:
163173

164174
```bash
165175
perf stat -e L1-dcache-loads,L1-dcache-load-misses ./default-os-scheduling
166176
perf stat -e L1-dcache-loads,L1-dcache-load-misses ./thread-affinity
167177
```
168178

169-
Inspecting the output below we see that the `L1-dcache-load-misses` which occur when the the CPU core does not have a up-to-date version of the data in the L1 Data cache and must perform an expensive operation to fetch data from a different location, reduces from ~7.84% to ~0.6% as a result of the thread pinning. This results in a huge reduction in function execution time, dropping from 10.7ms to 3.53ms.
179+
## Analyze the performance results
170180

171-
```outputRunning ./default-os-scheduling
181+
Inspecting the output below, you can see that the `L1-dcache-load-misses` metric (which occurs when the CPU core doesn't have an up-to-date version of the data in the L1 data cache and must perform an expensive operation to fetch data from a different location) reduces from approximately 7.84% to approximately 0.6% as a result of thread pinning.
182+
183+
This results in a significant reduction in function execution time, dropping from 10.7 ms to 3.53 ms:
184+
185+
```output
186+
Running ./default-os-scheduling
172187
Run on (16 X 2100 MHz CPU s)
173188
CPU Caches:
174189
L1 Data 64 KiB (x16)
@@ -217,8 +232,16 @@ thread_affinity/real_time 3.53 ms 0.343 ms 198
217232
0.169065000 seconds sys
218233
```
219234

220-
### Conclusion
235+
The results demonstrate that thread pinning can significantly improve performance when threads operate on separate data structures. By keeping threads on specific cores, you reduce cache coherency traffic and improve data locality.
236+
237+
## What you've accomplished and what's next
238+
239+
In this section, you've:
240+
- Created two programs to compare default OS scheduling against explicit thread pinning
241+
- Used the `pthread_setaffinity_np` API to control CPU affinity at the source code level
242+
- Measured cache performance using Perf to quantify the impact of thread pinning
243+
- Observed a performance improvement and a reduction in cache misses through strategic thread placement
221244

222-
In this tutorial, we introduced thread pinning (CPU affinity) through a pair of worked examples. By comparing default OS thread scheduling against explicitly pinned threads, we showed how controlling where threads run can reduce cache disruption in contention-heavy paths and improve runtime stability and performance.
245+
You've seen how controlling where threads run can reduce cache disruption in contention-heavy paths and improve runtime stability and performance. You've also learned about the trade-offs: pinning can boost locality and predictability, but it can hurt performance of other running processes, especially if the workload characteristics change or if you over-constrain the scheduler.
223246

224-
We also highlighted the tradeoffs, pinning can boost locality and predictability, but it can hurt performance of other running processes, espec. Finally, we showed how to implement affinity quickly using common system utilities for inspection and measurement, and how to be more expressive directly in code using APIs like `pthread_setaffinity_np` from `pthread.h`.
247+
Thread pinning is most effective when you have well-understood workload patterns and clear separation between data structures accessed by different threads. Use it as a fine-tuning technique after establishing baseline performance with default OS scheduling.

0 commit comments

Comments
 (0)