Skip to content

Commit d6aea81

Browse files
Merge pull request #2781 from madeline-underwood/threads
Threads_reviewed
2 parents aa7d5d9 + a524c55 commit d6aea81

5 files changed

Lines changed: 66 additions & 47 deletions

File tree

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

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,35 @@
11
---
2-
title: Getting Started with CPU Affinity
3-
4-
draft: true
5-
cascade:
6-
draft: true
2+
title: Optimize application performance with CPU affinity
73

84
minutes_to_complete: 30
95

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.
6+
who_is_this_for: This is an advanced topic for developers, performance engineers, and system administrators looking to fine-tune the performance of their workload on many-core Arm-based systems.
117

128
learning_objectives:
13-
- Create CPU Sets and implement directly into sourcecode
14-
- Understand the performance tradeoff when pinning threads with CPU affinity masks
9+
- Pin threads to specific CPU cores using taskset and source code modifications
10+
- Measure cache performance improvements from thread pinning using perf
11+
- Evaluate performance trade-offs between throughput and latency consistency
12+
- Implement CPU affinity strategies for co-located workloads
1513

1614
prerequisites:
17-
- Intermediate understanding of multi-threaded object-orientated programming in C++ and Python
18-
- Foundational understanding of build systems and computer architecture
15+
- An Arm Linux system with four or more CPU cores
16+
- Experience with multi-threaded programming in C++ and Python
17+
- Understanding of build systems and computer architecture concepts
18+
- Familiarity with Linux command-line tools
1919

2020
author: Kieran Hejmadi
2121

2222
### Tags
23-
skilllevels: Introductory
23+
skilllevels: Advanced
2424
subjects: Performance and Architecture
2525
armips:
2626
- Neoverse
2727
tools_software_languages:
2828
- C++
2929
- Python
30+
- taskset
31+
- perf
32+
- Google Benchmark
3033
operatingsystems:
3134
- Linux
3235

@@ -35,6 +38,22 @@ further_reading:
3538
title: Taskset Manual
3639
link: https://man7.org/linux/man-pages/man1/taskset.1.html
3740
type: documentation
41+
- resource:
42+
title: pthread_setaffinity_np Manual
43+
link: https://man7.org/linux/man-pages/man3/pthread_setaffinity_np.3.html
44+
type: documentation
45+
- resource:
46+
title: NUMA Deep Dive
47+
link: https://frankdenneman.nl/2016/07/07/numa-deep-dive-part-1-uma-numa/
48+
type: documentation
49+
- resource:
50+
title: Linux Scheduler Documentation
51+
link: https://www.kernel.org/doc/html/latest/scheduler/index.html
52+
type: documentation
53+
- resource:
54+
title: Get started with Arm-based cloud instances
55+
link: /learning-paths/servers-and-cloud-computing/csp/
56+
type: website
3857

3958

4059

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
11
---
2-
title: Thread pinning fundamentals
2+
title: Understand thread pinning and CPU affinity
33
weight: 2
44

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

9-
## CPU affinity
9+
## Explore CPU affinity
1010

1111
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.
1212

13-
## Pinning
13+
## Identify use cases for thread pinning
1414

1515
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.
1616

1717
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.
1818

19-
## Memory locality
19+
## Improve memory locality on NUMA systems
2020

2121
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.
2222

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.
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 reduce memory access latency and improve bandwidth.
2424

25-
## Setting affinity
25+
## Choose methods to set CPU affinity
2626

2727
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.
2828

2929
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.
3030

31-
## Conclusion
31+
## Evaluate trade-offs of thread pinning
3232

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.
33+
Thread pinning is a trade-off. 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.
3434

3535
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: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ This Learning Path works on any Arm Linux system with four or more CPU cores.
1212

1313
For example, you can use an AWS Graviton 3 `m7g.4xlarge` instance running Ubuntu 24.04 LTS, based on the Arm Neoverse V1 architecture.
1414

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/).
15+
If you're unfamiliar with creating a cloud instance, see [Get started with Arm-based cloud instances](/learning-paths/servers-and-cloud-computing/csp/).
1616

1717
The `m7g.4xlarge` instance has a uniform processor architecture, so there's no difference in memory or CPU core performance across the cores.
1818

19-
On Linux, you can check this with the following command:
19+
On Linux, check this with the following command:
2020

2121
```bash
2222
lscpu | grep -i numa
@@ -57,9 +57,9 @@ Finally, install the Linux perf utility for measuring performance. See the [Linu
5757

5858
## Create a CPU-intensive example program
5959

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.
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 the work is split across many threads.
6161

62-
Use an editor to create a file named `use_all_cores.cpp` with the code below:
62+
Create a file named `use_all_cores.cpp` with the code below. This program spawns multiple threads that calculate Pi using the Leibniz formula:
6363

6464
```cpp
6565
#include <vector>
@@ -75,7 +75,7 @@ double multiplethreaded_leibniz(int terms, bool use_all_cores){
7575

7676
int NUM_THREADS = 2; // use 2 cores by default
7777
if (use_all_cores){
78-
NUM_THREADS = std::thread::hardware_concurrency(); // e.g., 16 for a 16-core, single-threaded processor
78+
NUM_THREADS = std::thread::hardware_concurrency(); // for example, 16 for a 16-core, single-threaded processor
7979
}
8080
std::vector<double> partial_results(NUM_THREADS);
8181

@@ -143,7 +143,7 @@ g++ -O2 --std=c++11 use_all_cores.cpp -o prog
143143

144144
## Observe CPU utilization
145145

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:
146+
Observe how the compiled program utilizes CPU cores. In a separate terminal, use the `top` utility to view the utilization of each core:
147147

148148
```bash
149149
top -d 0.1
@@ -157,9 +157,9 @@ Then run the program in the other terminal:
157157
./prog
158158
```
159159

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")
160+
![Terminal output showing the top command displaying system resource usage. Sixteen CPU cores labeled CPU0 through CPU15 are shown with horizontal percentage bars. Most cores show near 100% utilization with full green bars. The display includes columns for %Cpu(s), memory usage, and process information in a dark theme terminal window alt-txt#center](cpu_util.jpg "CPU utilization showing all cores being used")
161161

162-
You should observe all cores on your system being periodically utilized up to 100% and then dropping to idle until the program exits.
162+
All cores on your system are periodically utilized up to 100% and then drop to idle until the program exits.
163163

164164
## What you've accomplished and what's next
165165

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ This program has two atomic variables that are aligned on different cache lines
8080
8181
## Create a program with explicit thread pinning
8282
83-
Now, copy the code below into a new file named `thread_affinity.cpp`:
83+
Create a file named `thread_affinity.cpp` with the code below. This program uses `pthread_setaffinity_np` to pin threads to specific CPU cores:
8484
8585
```cpp
8686
#include <benchmark/benchmark.h>
@@ -178,7 +178,7 @@ perf stat -e L1-dcache-loads,L1-dcache-load-misses ./thread-affinity
178178

179179
## Analyze the performance results
180180

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.
181+
The output shows the `L1-dcache-load-misses` metric reduces from approximately 7.84% to approximately 0.6% as a result of thread pinning. This metric measures how often the CPU core doesn't have an up-to-date version of data in the L1 data cache and must perform an expensive operation to fetch data from a different location.
182182

183183
This results in a significant reduction in function execution time, dropping from 10.7 ms to 3.53 ms:
184184

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

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ layout: learningpathall
88

99
## Create a single-threaded Python benchmark
1010

11-
Now that you have a program that utilizes all available CPU cores, you'll create a single-threaded program that's sensitive to execution variations. This simulates scenarios like a log ingesting process or a single-threaded consumer that needs to maintain a steady pace.
11+
Create a single-threaded program that's sensitive to execution variations. This simulates scenarios like a log ingesting process or a single-threaded consumer that needs to maintain a steady pace.
1212

1313
Check that you have Python installed:
1414

1515
```bash
1616
python --version
1717
```
1818

19-
You should see the version of Python:
19+
The output is similar to:
2020

2121
```output
2222
Python 3.12.3
2323
```
2424

25-
If Python isn't installed, use your Linux package manager to install it or refer to the [Python downloads page](https://www.python.org/downloads/).
25+
If Python isn't installed, use your Linux package manager to install it or see the [Python downloads page](https://www.python.org/downloads/).
2626

2727
Next, create a virtual environment to install packages without interfering with system packages:
2828

@@ -32,7 +32,7 @@ source venv/bin/activate
3232
pip install matplotlib
3333
```
3434

35-
Use an editor to create a file named `single_threaded_python_script.py` with the following code. This script repeatedly measures the execution time of a computational function and writes the results to `data.txt`. It then generates time-series graphs to illustrate the effects of thread pinning:
35+
Create a file named `single_threaded_python_script.py` with the following code. This script measures the execution time of a computational function, writes results to `data.txt`, and generates time-series graphs to illustrate the effects of thread pinning:
3636

3737
```python
3838
#!/usr/bin/env python3
@@ -119,7 +119,7 @@ chmod +x single_threaded_python_script.py
119119

120120
## Compare thread pinning strategies
121121

122-
You'll explore three different scenarios to understand the trade-offs of thread pinning:
122+
Explore three different scenarios to understand the trade-offs of thread pinning:
123123

124124
1. Free: The operating system allocates both programs to any of four cores
125125
2. Shared-pinned: The Python script is pinned to core 0, but `prog` can run on any core
@@ -149,9 +149,9 @@ wait
149149

150150
#### Shared script
151151

152-
The next script pins the Python script to core 0, while `prog` can use any of the first four cores:
152+
The next script pins the Python script to core 0, while `prog` can use any of the first four cores.
153153

154-
Use an editor to create a file named `shared-pinned.sh` with the following code:
154+
Create a file named `shared-pinned.sh` with the following code:
155155

156156
```bash
157157
#!/bin/bash
@@ -167,9 +167,9 @@ wait
167167

168168
#### Exclusive script
169169

170-
The last one gives the Python script exclusive access to core 0, and `prog` uses cores 1-3:
170+
The last one gives the Python script exclusive access to core 0, and `prog` uses cores 1-3.
171171

172-
Use a text editor to create a file named `exclusive.sh` with the following code:
172+
Create a file named `exclusive.sh` with the following code:
173173

174174
```bash
175175
#!/bin/bash
@@ -198,7 +198,7 @@ chmod +x free-script.sh shared-pinned.sh exclusive.sh
198198

199199
The terminal output shows the execution time for `prog` under the three scenarios. The Python script also generates three files: `Free.jpg`, `Exclusive.jpg`, and `Shared.jpg`.
200200

201-
As the terminal output below shows, the `free-script.sh` scenario (where the Linux scheduler assigns threads to cores without restriction) completes `prog` the fastest at 5.8 seconds. The slowest execution occurs when the Python script has exclusive access to CPU 0, which is expected because you've constrained `prog` to fewer cores:
201+
The terminal output shows the `free-script.sh` scenario (where the Linux scheduler assigns threads to cores without restriction) completes `prog` the fastest at 5.8 seconds. The slowest execution occurs when the Python script has exclusive access to CPU 0, which is expected because `prog` is constrained to fewer cores:
202202

203203
```output
204204
Answer = 3.14159 5 iterations took 5838 milliseconds
@@ -210,21 +210,21 @@ However, this represents a trade-off with the Python script's performance.
210210

211211
### Free scenario results
212212

213-
Looking at `Free.jpg`, you can see periodic zones of high latency (3.5 ms) that likely occur when there's contention between `prog` and the Python script:
213+
The `Free.jpg` graph shows periodic zones of high latency (3.5 ms) that likely occur when there's contention between `prog` and the Python script:
214214

215-
![Time-series graph showing execution time varying between 0.5ms and 3.5ms with periodic spikes, indicating contention between processes when both are free to run on any core](free.jpg "Free scenario: both programs can run on any of four cores")
215+
![Time-series line graph plotting execution time in milliseconds on the y-axis against sample number on the x-axis. The line fluctuates between approximately 0.5ms and 3.5ms, showing periodic spikes and zones of higher latency. The graph has a grid background and is titled 'Free'. The pattern indicates contention between processes when both can run on any core](free.jpg "Free scenario: both programs can run on any of four cores")
216216

217217
### Shared-pinned scenario results
218218

219-
When pinning the Python script to core 0 while `prog` remains free to use any cores, you observe similar behavior:
219+
When pinning the Python script to core 0 while `prog` remains free to use any cores, the behavior is similar:
220220

221-
![Time-series graph showing execution time with similar periodic spikes as the free scenario, indicating continued contention despite pinning the Python script](pinned_shared.jpg "Shared-pinned scenario: Python script pinned to core 0, prog free to run on any core")
221+
![Time-series line graph plotting execution time in milliseconds against sample number. The line shows similar behavior to the free scenario with fluctuations between approximately 0.5ms and 3.5ms and periodic spikes. The graph has a grid background and is titled 'Shared'. The pattern shows continued contention despite pinning the Python script to a specific core](pinned_shared.jpg "Shared-pinned scenario: Python script pinned to core 0, prog free to run on any core")
222222

223223
### Exclusive scenario results
224224

225-
When the Python script has exclusive access to core 0, you observe more consistent execution time around 0.49 ms because the script doesn't contend with any other demanding processes:
225+
When the Python script has exclusive access to core 0, the execution time is more consistent around 0.49 ms because the script doesn't contend with any other demanding processes:
226226

227-
![Time-series graph showing consistent execution time around 0.49ms with minimal variation, demonstrating stable performance when the Python script has exclusive core access](exclusive.jpg "Exclusive scenario: Python script has exclusive access to core 0, prog runs on cores 1-3")
227+
![Time-series line graph plotting execution time in milliseconds against sample number. The line shows consistent, stable execution time around 0.49ms with minimal variation throughout the entire sample range. The graph has a grid background and is titled 'Exclusively Pinned'. The flat, steady pattern demonstrates stable performance when the Python script has exclusive access to a dedicated core](exclusive.jpg "Exclusive scenario: Python script has exclusive access to core 0, prog runs on cores 1-3")
228228

229229
## Understanding the trade-offs
230230

@@ -238,10 +238,10 @@ Multiple factors influence this behavior, including the Linux scheduler algorith
238238

239239
## What you've accomplished and what's next
240240

241-
In this section, you've:
241+
In this section:
242242
- Created a single-threaded Python benchmark that measures execution time variations
243243
- Used `taskset` to pin processes to specific CPU cores
244244
- Compared three thread pinning strategies: free, shared-pinned, and exclusive
245245
- Analyzed the trade-offs between throughput and latency consistency
246246

247-
In the next section, you'll learn how to use source code modifications and environment variables to control thread affinity programmatically.
247+
Next, you learn how to control thread affinity programmatically using source code modifications.

0 commit comments

Comments
 (0)