Skip to content

Commit 8b64a16

Browse files
Merge pull request #2581 from tdunkleArm/add-windows-instructions-to-cpp-pgo-path
Add Learning Path for profile guided optimization (PGO) on Windows.
2 parents ad43c72 + 9101323 commit 8b64a16

6 files changed

Lines changed: 301 additions & 0 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
title: Optimize C++ performance with Profile-Guided Optimization and Google Benchmark
3+
4+
minutes_to_complete: 15
5+
6+
who_is_this_for: Developers looking to optimize C++ performance on an Arm-based Windows device, based on runtime behavior.
7+
8+
learning_objectives:
9+
- Microbenchmark a function using Google Benchmark.
10+
- Apply profile-guided optimization to build performance-tuned binaries for Windows on Arm.
11+
12+
prerequisites:
13+
- Basic C++ understanding.
14+
- Access to an Arm-based Windows machine.
15+
16+
author: Tom Dunkle
17+
18+
### Tags
19+
skilllevels: Introductory
20+
subjects: ML
21+
armips:
22+
- Neoverse
23+
tools_software_languages:
24+
- Google Benchmark
25+
- Runbook
26+
operatingsystems:
27+
- Windows
28+
29+
further_reading:
30+
- resource:
31+
title: MSVC profile-guided optimization documentation
32+
link: https://learn.microsoft.com/en-us/cpp/build/profile-guided-optimizations?view=msvc-170
33+
type: documentation
34+
- resource:
35+
title: Google Benchmark Library
36+
link: https://github.com/google/benchmark
37+
type: documentation
38+
39+
40+
41+
### FIXED, DO NOT MODIFY
42+
# ================================================================================
43+
weight: 1 # _index.md always has weight of 1 to order correctly
44+
layout: "learningpathall" # All files under learning paths have this same wrapper
45+
learning_path_main_page: "yes" # This should be surfaced when looking for related content. Only set for _index.md of learning path content.
46+
---
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# ================================================================================
3+
# FIXED, DO NOT MODIFY THIS FILE
4+
# ================================================================================
5+
weight: 21 # Set to always be larger than the content in this path to be at the end of the navigation.
6+
title: "Next Steps" # Always the same, html page title.
7+
layout: "learningpathall" # All files under learning paths have this same wrapper for Hugo processing.
8+
---
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
title: Profile-Guided Optimization
3+
weight: 2
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
### What is Profile-Guided Optimization (PGO) and how does it work?
10+
11+
Profile-Guided Optimization (PGO) is a compiler optimization technique that enhances program performance by utilizing real-world execution data. PGO typically involves a two-step process:
12+
13+
- First, compile the program to produce an instrumented binary that collects profiling data during execution;
14+
- Second, recompile the program with an optimization profile, allowing the compiler to leverage the collected data to make informed optimization decisions. This approach identifies frequently executed paths — known as “hot” paths — and optimizes them more aggressively, while potentially reducing emphasis on less critical code paths.
15+
16+
### When should I use Profile-Guided Optimization?
17+
18+
PGO is particularly beneficial in the later stages of development when real-world workloads are available. It is especially useful for applications where performance is critical and runtime behavior is complex or data-dependent. For instance, consider optimizing “hot” functions that execute frequently. Doing so ensures that the most impactful parts of your code are optimized based on actual usage patterns.
19+
20+
### What are the limitations of Profile-Guided Optimization and when should I avoid it?
21+
22+
While PGO offers substantial performance benefits, it has limitations. The profiling data must accurately represent typical usage scenarios; otherwise, the optimizations may not deliver the desired performance improvements and could even degrade performance.
23+
24+
Additionally, the process requires extra build steps, potentially increasing compile times for large codebases. Therefore, use PGO only on performance-critical sections that are heavily influenced by actual runtime behavior. PGO might not be ideal for early-stage development or applications with highly variable or unpredictable usage patterns.
25+
26+
For further information, see the [MSVC documentation](https://learn.microsoft.com/en-us/cpp/build/profile-guided-optimizations?view=msvc-170) on enabling and using PGO.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
title: Google Benchmark
3+
weight: 3
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
## Google Benchmark
10+
11+
Google Benchmark is a C++ library specifically designed for microbenchmarking – measuring the performance of small code snippets with high accuracy. Microbenchmarking is essential for identifying bottlenecks and optimizing critical sections, especially in performance-sensitive applications.
12+
13+
Google Benchmark simplifies this process by providing a framework that manages iterations, times execution, and performs statistical analysis. This allows you to focus on the code being measured, rather than writing boilerplate or trying to prevent unwanted compiler optimizations manually.
14+
15+
To use Google Benchmark, define a function that accepts a `benchmark::State&` parameter and iterate over it to perform the benchmarking. Register the function using the `BENCHMARK` macro and include `BENCHMARK_MAIN()` to generate the benchmark's entry point.
16+
17+
Here's a basic example:
18+
19+
```cpp
20+
#include <benchmark/benchmark.h>
21+
22+
static void BM_StringCreation(benchmark::State& state) {
23+
for (auto _ : state)
24+
std::string empty_string;
25+
}
26+
BENCHMARK(BM_StringCreation);
27+
28+
BENCHMARK_MAIN();
29+
```
30+
31+
### Filtering and Preventing Compiler Optimizations
32+
33+
Google Benchmark provides tools to ensure accurate measurements by preventing unintended compiler optimizations and allowing flexible benchmark selection.
34+
35+
1. **Preventing Optimizations**: Use `benchmark::DoNotOptimize(value);` to force the compiler to read and store a variable or expression, ensuring it is not optimized away.
36+
37+
2. **Filtering Benchmarks**: To run a specific subset of benchmarks, use the `--benchmark_filter` command-line option with a regular expression. For example:
38+
39+
```bash
40+
.\benchmark_binary --benchmark_filter=BM_String.*
41+
```
42+
43+
This eliminates the need to repeatedly comment out lines of source code.
44+
45+
For more detailed information and advanced usage, refer to the [official documentation](https://github.com/google/benchmark).
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
---
2+
title: Example operation
3+
weight: 4
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
## Optimizing costly division operations with Google Benchmark and PGO
10+
11+
In this section, you'll learn how to use Google Benchmark and Profile-Guided Optimization to improve the performance of a simple division operation. This example demonstrates how even seemingly straightforward operations can benefit from optimization techniques.
12+
13+
Integer division is ideal for benchmarking because it's significantly more expensive than operations like addition, subtraction, or multiplication. On most CPU architectures, including Arm, division instructions have higher latency and lower throughput compared to other arithmetic operations. By applying Profile-Guided Optimization to code containing division operations, we can potentially achieve significant performance improvements.
14+
15+
For this example, you can use an Arm computer running Windows.
16+
17+
## What tools are needed to run a Google Benchmark example on Windows?
18+
19+
Download the [Arm GNU Toolchain](https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain) to install the prerequisite packages.
20+
21+
Next, install the static version of Google Benchmark for Arm64 via vcpkg. Run the following commands in Powershell as Administrator:
22+
23+
```console
24+
cd C:\git
25+
git clone https://github.com/microsoft/vcpkg.git
26+
cd vcpkg
27+
.\bootstrap-vcpkg.bat
28+
.\vcpkg install benchmark:arm64-windows-static
29+
```
30+
31+
## Division example
32+
33+
Use an editor to copy and paste the C++ source code below into a file named `div_bench.cpp`.
34+
35+
This trivial example takes in a vector of 4096 32-bit integers and divides each element by a number. Importantly, the use of `benchmark/benchmark.h` introduces indirection since the divisor value is unknown at compile time, although it is visible in the source code as 1500.
36+
37+
```cpp
38+
#include <benchmark/benchmark.h>
39+
#include <vector>
40+
41+
// Benchmark division instruction
42+
static void baseDiv(benchmark::State &s) {
43+
std::vector<int> v_in(4096);
44+
std::vector<int> v_out(4096);
45+
46+
for (auto _ : s) {
47+
for (size_t i = 0; i < v_in.size(); i++) v_out[i] = v_in[i] / s.range(0);
48+
// s.range(0) is unknown at compile time, cannot be reduced
49+
}
50+
}
51+
52+
BENCHMARK(baseDiv)->Arg(1500)->Unit(benchmark::kMicrosecond); // value of 1500 is passed through as an argument so strength reduction cannot be applied
53+
54+
BENCHMARK_MAIN();
55+
```
56+
57+
To compile and run the microbenchmark on this function, you need to link with the correct libraries:
58+
59+
Compile with the command:
60+
61+
```console
62+
cl /D BENCHMARK_STATIC_DEFINE div_bench.cpp /link /LIBPATH:"$VCPKG\lib" benchmark.lib benchmark_main.lib shlwapi.lib
63+
```
64+
65+
Run the program:
66+
67+
```console
68+
.\div_bench.exe
69+
```
70+
71+
### Example output
72+
73+
```output
74+
Running ./div_bench.base
75+
Run on (4 X 2100 MHz CPU s)
76+
CPU Caches:
77+
L1 Data 64 KiB (x4)
78+
L1 Instruction 64 KiB (x4)
79+
L2 Unified 1024 KiB (x4)
80+
L3 Unified 32768 KiB (x1)
81+
Load Average: 0.00, 0.00, 0.00
82+
***WARNING*** Library was built as DEBUG. Timings may be affected.
83+
-------------------------------------------------------
84+
Benchmark Time CPU Iterations
85+
-------------------------------------------------------
86+
baseDiv/1500 7.90 us 7.90 us 88512
87+
```
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
---
2+
title: Using Profile Guided Optimization (Windows)
3+
weight: 5
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
### Build with PGO
10+
11+
To generate a binary optimized using runtime profile data, first build an instrumented binary that records usage data. Before building, open the Arm dev shell so that the compiler is in your PATH:
12+
13+
```console
14+
& "C:\Program Files\Microsoft Visual Studio\18\Community\Common7\Tools\Launch-VsDevShell.ps1" -Arch arm64
15+
```
16+
17+
(**note:** you may need to change the version number in your Visual Studio path, depending on which Visual Studio version you've installed.)
18+
19+
Next, set an environment variable to refer to the installed packages directory:
20+
21+
```console
22+
$VCPKG="C:\git\vcpkg\installed\arm64-windows-static"
23+
```
24+
25+
Next, run the following command, which includes the `/GENPROFILE` flag, to build the instrumented binary:
26+
27+
```console
28+
cl /O2 /GL /D BENCHMARK_STATIC_DEFINE /I "$VCPKG\include" /Fe:div_bench.exe div_bench.cpp /link /LTCG /GENPROFILE /PGD:div_bench.pgd /LIBPATH:"$VCPKG\lib" benchmark.lib benchmark_main.lib shlwapi.lib
29+
```
30+
31+
The compiler options used in this command are:
32+
33+
* **/O2**: Creates [fast code](https://learn.microsoft.com/en-us/cpp/build/reference/o1-o2-minimize-size-maximize-speed?view=msvc-170)
34+
* **/GL**: Enables [whole program optimization](https://learn.microsoft.com/en-us/cpp/build/reference/gl-whole-program-optimization?view=msvc-170).
35+
* **/D**: Enables the Benchmark [static preprocessor definition](https://learn.microsoft.com/en-us/cpp/build/reference/d-preprocessor-definitions?view=msvc-170).
36+
* **/I**: Adds the arm64 includes to the [list of include directories](https://learn.microsoft.com/en-us/cpp/build/reference/i-additional-include-directories?view=msvc-170).
37+
* **/Fe**: Specifies a name for the [executable file output](https://learn.microsoft.com/en-us/cpp/build/reference/fe-name-exe-file?view=msvc-170).
38+
* **/link**: Specifies [options to pass to linker](https://learn.microsoft.com/en-us/cpp/build/reference/link-pass-options-to-linker?view=msvc-170).
39+
40+
The linker options used in this command are:
41+
42+
* **/LTCG**: Specifies [link time code generation](https://learn.microsoft.com/en-us/cpp/build/reference/ltcg-link-time-code-generation?view=msvc-170).
43+
* **/GENPROFILE**: Specifies [generation of a .pgd file for PGO](https://learn.microsoft.com/en-us/cpp/build/reference/genprofile-fastgenprofile-generate-profiling-instrumented-build?view=msvc-170).
44+
* **/PGD**: Specifies a [database for PGO](https://learn.microsoft.com/en-us/cpp/build/reference/pgd-specify-database-for-profile-guided-optimizations?view=msvc-170).
45+
* **/LIBPATH**: Specifies the [additional library path](https://learn.microsoft.com/en-us/cpp/build/reference/libpath-additional-libpath?view=msvc-170).
46+
47+
Next, run the instrumented binary to generate the profile data:
48+
49+
```console
50+
.\div_bench.exe
51+
```
52+
53+
This execution creates profile data files (typically with a `.pgc` extension) in the same directory.
54+
55+
Now recompile the program using the `/USEPROFILE` flag to apply optimizations based on the collected data:
56+
57+
```console
58+
cl /O2 /GL /D BENCHMARK_STATIC_DEFINE /I "$VCPKG\include" /Fe:div_bench_opt.exe div_bench.cpp /link /LTCG:PGOptimize /USEPROFILE /PGD:div_bench.pgd /LIBPATH:"$VCPKG\lib" benchmark.lib benchmark_main.lib shlwapi.lib
59+
```
60+
61+
In this command, the [USEPROFILE linker option](https://learn.microsoft.com/en-us/cpp/build/reference/useprofile?view=msvc-170) instructs the linker to enable PGO with the profile generated during the previous run of the executable.
62+
63+
### Run the optimized binary
64+
65+
Now run the optimized binary:
66+
67+
```console
68+
.\div_bench_opt.exe
69+
```
70+
71+
The following output shows the performance improvement:
72+
73+
```output
74+
Running ./div_bench.opt
75+
Run on (4 X 2100 MHz CPU s)
76+
CPU Caches:
77+
L1 Data 64 KiB (x4)
78+
L1 Instruction 64 KiB (x4)
79+
L2 Unified 1024 KiB (x4)
80+
L3 Unified 32768 KiB (x1)
81+
Load Average: 0.10, 0.03, 0.01
82+
***WARNING*** Library was built as DEBUG. Timings may be affected.
83+
-------------------------------------------------------
84+
Benchmark Time CPU Iterations
85+
-------------------------------------------------------
86+
baseDiv/1500 2.86 us 2.86 us 244429
87+
```
88+
89+
As the terminal output above shows, the average execution time is reduced from 7.90 to 2.86 microseconds. This improvement occurs because the profile data informed the compiler that the input divisor was consistently 1500 during the profiled runs, allowing it to apply specific optimizations.

0 commit comments

Comments
 (0)