Skip to content

Commit 74949f6

Browse files
committed
Add Windows instructions to C++ profile guided optimization (PGO) learning path.
1 parent adcdd70 commit 74949f6

6 files changed

Lines changed: 251 additions & 132 deletions

File tree

content/learning-paths/servers-and-cloud-computing/cpp-profile-guided-optimisation/_index.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ learning_objectives:
1111

1212
prerequisites:
1313
- Basic C++ understanding.
14-
- Access to an Arm-based Linux machine.
14+
- Access to an Arm-based machine.
1515

1616
author: Kieran Hejmadi
1717

@@ -25,12 +25,17 @@ tools_software_languages:
2525
- Runbook
2626
operatingsystems:
2727
- Linux
28+
- Windows
2829

2930
further_reading:
3031
- resource:
3132
title: G++ profile-guided optimization documentation
3233
link: https://gcc.gnu.org/onlinedocs/gcc-13.3.0/gcc/Instrumentation-Options.html
3334
type: documentation
35+
- resource:
36+
title: MSVC profile-guided optimization documentation
37+
link: https://learn.microsoft.com/en-us/cpp/build/profile-guided-optimizations?view=msvc-170
38+
type: documentation
3439
- resource:
3540
title: Google Benchmark Library
3641
link: https://github.com/google/benchmark

content/learning-paths/servers-and-cloud-computing/cpp-profile-guided-optimisation/how-to-1.md

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

99
### What is Profile-Guided Optimization (PGO) and how does it work?
1010

11-
Profile-Guided Optimization (PGO) is a compiler optimization technique that enhances program performance by utilizing real-world execution data. In GCC/G++, PGO involves a two-step process:
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:
1212

13-
- First, compile the program with the `-fprofile-generate` flag to produce an instrumented binary that collects profiling data during execution;
14-
- Second, recompile the program with the `-fprofile-use` flag, 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.
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.
1515

1616
### When should I use Profile-Guided Optimization?
1717

content/learning-paths/servers-and-cloud-computing/cpp-profile-guided-optimisation/how-to-3.md

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,31 @@ In this section, you'll learn how to use Google Benchmark and Profile-Guided Opt
1212

1313
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.
1414

15-
## What tools are needed to run a Google Benchmark example?
15+
For this example, you can use an Arm computer (Linux or Windows).
1616

17-
For this example, you can use any Arm Linux computer. For example, an AWS EC2 `c7g.xlarge` instance running Ubuntu 24.04 LTS can be used.
17+
## What tools are needed to run a Google Benchmark example on Linux?
1818

19-
Run the following commands to install the prerequisite packages:
19+
For Linux, run the following commands to install the prerequisite packages:
2020

2121
```bash
2222
sudo apt update
2323
sudo apt install gcc g++ make libbenchmark-dev -y
2424
```
2525

26+
## What tools are needed to run a Google Benchmark example on Windows?
27+
28+
If you are using Windows, download the [Arm GNU Toolchain](https://developer.arm.com/Tools%20and%20Software/GNU%20Toolchain) to install the prerequisite packages.
29+
30+
Next, install the static version of Google Benchmark for Arm64 via vcpkg. Run the following commands in Powershell as Administrator:
31+
32+
```bash
33+
cd C:\git
34+
git clone https://github.com/microsoft/vcpkg.git
35+
cd vcpkg
36+
.\bootstrap-vcpkg.bat
37+
.\vcpkg install benchmark:arm64-windows-static
38+
```
39+
2640
## Division example
2741

2842
Use an editor to copy and paste the C++ source code below into a file named `div_bench.cpp`.
@@ -49,20 +63,32 @@ BENCHMARK(baseDiv)->Arg(1500)->Unit(benchmark::kMicrosecond); // value of 1500 i
4963
BENCHMARK_MAIN();
5064
```
5165
52-
To compile and run the microbenchmark on this function, you need to link with the `pthreads` and `benchmark` libraries.
66+
To compile and run the microbenchmark on this function, you need to link with the correct libraries:
5367
54-
Compile with the command:
68+
**(Linux)** Compile with the command:
5569
5670
```bash
5771
g++ -O3 -std=c++17 div_bench.cpp -lbenchmark -lpthread -o div_bench.base
5872
```
5973

60-
Run the program:
74+
**(Windows)** Compile with the command:
75+
76+
```bash
77+
cl /D BENCHMARK_STATIC_DEFINE div_bench.cpp /link /LIBPATH:"$VCPKG\lib" benchmark.lib benchmark_main.lib shlwapi.lib
78+
```
79+
80+
**(Linux)** Run the program:
6181

6282
```bash
6383
./div_bench.base
6484
```
6585

86+
**(Windows)** Run the program:
87+
88+
```bash
89+
.\div_bench.exe
90+
```
91+
6692
### Example output
6793

6894
```output
@@ -80,25 +106,3 @@ Benchmark Time CPU Iterations
80106
-------------------------------------------------------
81107
baseDiv/1500 7.90 us 7.90 us 88512
82108
```
83-
84-
### Inspect assembly
85-
86-
To inspect what assembly instructions are being executed most frequently, you can use the `perf` command. This is useful for identifying bottlenecks and understanding the performance characteristics of your code.
87-
88-
Install Perf using the [install guide](https://learn.arm.com/install-guides/perf/) before proceeding.
89-
90-
{{% notice Please Note %}}
91-
You may need to set the `perf_event_paranoid` value to -1 with the `sudo sysctl kernel.perf_event_paranoid=-1` command to run the commands below.
92-
{{% /notice %}}
93-
94-
Run the following commands to record `perf` data and create a report in the terminal:
95-
96-
```bash
97-
sudo perf record -o perf-division-base ./div_bench.base
98-
sudo perf report --input=perf-division-base
99-
```
100-
101-
As the `perf report` graphic below shows, the program spends a significant amount of time in the short loops with no loop unrolling. There is also an expensive `sdiv` operation, and most of the execution time is spent storing the result of the operation.
102-
103-
![before-pgo](./before-pgo.gif)
104-

content/learning-paths/servers-and-cloud-computing/cpp-profile-guided-optimisation/how-to-4.md

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
title: Using Profile Guided Optimization
2+
title: Using Profile Guided Optimization (Linux)
33
weight: 5
44

55
### FIXED, DO NOT MODIFY
@@ -22,14 +22,35 @@ Next, run the instrumented binary to generate the profile data:
2222

2323
This execution creates profile data files (typically with a `.gcda` extension) in the same directory.
2424

25+
### Inspect assembly
26+
27+
To inspect what assembly instructions are being executed most frequently, you can use the `perf` command. This is useful for identifying bottlenecks and understanding the performance characteristics of your code.
28+
29+
Install Perf using the [install guide](https://learn.arm.com/install-guides/perf/) before proceeding.
30+
31+
{{% notice Please Note %}}
32+
You may need to set the `perf_event_paranoid` value to -1 with the `sudo sysctl kernel.perf_event_paranoid=-1` command to run the commands below.
33+
{{% /notice %}}
34+
35+
Run the following commands to record `perf` data and create a report in the terminal:
36+
37+
```bash
38+
sudo perf record -o perf-division-base ./div_bench.base
39+
sudo perf report --input=perf-division-base
40+
```
41+
42+
As the `perf report` graphic below shows, the program spends a significant amount of time in the short loops with no loop unrolling. There is also an expensive `sdiv` operation, and most of the execution time is spent storing the result of the operation.
43+
44+
![before-pgo](./before-pgo.gif)
45+
46+
### Compile and run the optimized binary
47+
2548
Now recompile the program using the `-fprofile-use` flag to apply optimizations based on the collected data:
2649

2750
```bash
2851
g++ -O3 -std=c++17 -fprofile-use div_bench.cpp -lbenchmark -lpthread -o div_bench.opt
2952
```
3053

31-
### Run the optimized binary
32-
3354
Now run the optimized binary:
3455

3556
```bash
Lines changed: 65 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,89 @@
11
---
2-
title: Incorporating PGO into a GitHub Actions workflow
2+
title: Using Profile Guided Optimization (Windows)
33
weight: 6
44

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

9-
### Build locally with make
10-
11-
PGO can be integrated into a `Makefile` and continuous integration (CI) systems using simple command-line instructions, as shown in the sample `Makefile` below.
12-
13-
{{% notice Caution %}}
14-
PGO adds additional build steps which can increase compile time - especially for large code bases. As such, PGO is not suitable for all sections of code. You should PGO only for sections of code which are heavily influenced by run-time behavior and are performance critical. Therefore, PGO might not be ideal for early-stage development or for applications with highly variable or unpredictable usage patterns.
15-
{{% /notice %}}
16-
17-
Use a text editor to create a file named `Makefile` containing the following content:
18-
19-
```makefile
20-
# Simple Makefile for building and benchmarking div_bench with and without PGO
21-
22-
# Compiler and flags
23-
CXX := g++
24-
CXXFLAGS := -O3 -std=c++17
25-
LDLIBS := -lbenchmark -lpthread
26-
27-
# Default target: build both binaries
28-
.PHONY: all clean clean-gcda clean-perf run
29-
all: div_bench.base div_bench.opt
30-
31-
# Build the baseline binary (no PGO)
32-
div_bench.base: div_bench.cpp
33-
$(CXX) $(CXXFLAGS) $< $(LDLIBS) -o $@
34-
35-
# Build the PGO-optimized binary:
36-
# Note: This target depends on the source file and cleans previous profile data first.
37-
# It runs the instrumented binary to generate new profile data before the final compilation.
38-
div_bench.opt: div_bench.cpp
39-
$(MAKE) clean-gcda # Ensure no old profile data interferes
40-
$(CXX) $(CXXFLAGS) -fprofile-generate $< $(LDLIBS) -o $@
41-
@echo "Running instrumented binary to gather profile data..."
42-
./div_bench.opt # Generate .gcda file
43-
$(CXX) $(CXXFLAGS) -fprofile-use $< $(LDLIBS) -o $@ # Compile using the generated profile
44-
$(MAKE) clean-perf # Optional: Clean perf data if generated elsewhere
45-
46-
# Remove profile data files
47-
clean-gcda:
48-
rm -f ./*.gcda
49-
50-
# Remove perf data files (if applicable)
51-
clean-perf:
52-
rm -f perf-division-*
53-
54-
# Remove all generated files including binaries and profile data
55-
clean: clean-gcda clean-perf
56-
rm -f div_bench.base div_bench.opt
57-
58-
# Run both benchmarks with informative headers
59-
run: all # Ensure binaries are built before running
60-
@echo "==================== Without Profile-Guided Optimization ===================="
61-
./div_bench.base
62-
@echo "==================== With Profile-Guided Optimization ===================="
63-
./div_bench.opt
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+
```bash
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+
```bash
22+
$VCPKG="C:\git\vcpkg\installed\arm64-windows-static"
6423
```
6524

66-
You can run the following commands in your terminal:
25+
Next, run the following command, which includes the `/GENPROFILE` flag, to build the instrumented binary:
6726

68-
* `make all` (or simply `make`): Compiles both `div_bench.base` (without PGO) and `div_bench.opt` (with PGO). This includes the steps of generating profile data for the optimized version.
69-
* `make run`: Builds both binaries (if they don't exist) and then runs them, displaying the benchmark results for comparison.
70-
* `make clean`: Removes the compiled binaries (`div_bench.base`, `div_bench.opt`) and any generated profile data files (`*.gcda`).
27+
```bash
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).
7139

72-
### Build with GitHub Actions
40+
The linker options used in this command are:
7341

74-
Alternatively, you can integrate PGO into your Continuous Integration (CI) workflow using GitHub Actions. The YAML file below provides a basic example that compiles and runs the benchmark on a GitHub-hosted Ubuntu 24.04 Arm-based runner. This setup can be extended with automated tests to check for performance regressions.
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).
7546

76-
```yaml
77-
name: PGO Benchmark
47+
Next, run the instrumented binary to generate the profile data:
7848

79-
on:
80-
push:
81-
branches: [ main ]
49+
```bash
50+
.\div_bench.exe
51+
```
8252

83-
jobs:
84-
build:
85-
runs-on: ubuntu-24.04-arm
53+
This execution creates profile data files (typically with a `.pgc` extension) in the same directory.
8654

87-
steps:
88-
- name: Check out source
89-
uses: actions/checkout@v3
55+
Now recompile the program using the `/USEPROFILE` flag to apply optimizations based on the collected data:
9056

91-
- name: Install dependencies
92-
run: |
93-
sudo apt-get update
94-
sudo apt-get install -y libbenchmark-dev g++
57+
```bash
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+
```
9560

96-
- name: Clean previous profiling data
97-
run: |
98-
rm -rf ./*gcda
99-
rm -f div_bench.base div_bench.opt
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.
10062

101-
- name: Compile base and instrumented binary
102-
run: |
103-
g++ -O3 -std=c++17 div_bench.cpp -lbenchmark -lpthread -o div_bench.base
104-
g++ -O3 -std=c++17 -fprofile-generate div_bench.cpp -lbenchmark -lpthread -o div_bench.opt
63+
### Run the optimized binary
10564

106-
- name: Generate profile data and compile with PGO
107-
run: |
108-
./div_bench.opt
109-
g++ -O3 -std=c++17 -fprofile-use div_bench.cpp -lbenchmark -lpthread -o div_bench.opt
65+
Now run the optimized binary:
11066

111-
- name: Run benchmarks
112-
run: |
113-
echo "==================== Without Profile-Guided Optimization ===================="
114-
./div_bench.base
115-
echo "==================== With Profile-Guided Optimization ===================="
116-
./div_bench.opt
117-
echo "==================== Benchmarking complete ===================="
67+
```bash
68+
.\div_bench_opt.exe
11869
```
11970

120-
To use this workflow, save the YAML content into a file named `pgo_benchmark.yml` (or any other `.yml` name) inside the `.github/workflows/` directory of your GitHub repository. Ensure your `div_bench.cpp` file is present in the repository root. When you push changes to the `main` branch, GitHub Actions will automatically detect this workflow file and execute the defined steps on an Arm-based runner, compiling both versions of the benchmark and running them.
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+
```
12188

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)