Why Visual Studio C++ Builds Are Slow: PCH, /MP, Incremental Linking and Forward Declarations
Key takeaways
Four settings that cut Visual Studio C++ build times: precompiled headers, parallel compilation with /MP, incremental linking, and trimming includes with forward declarations.
Introduction
Visual Studio C++ builds get slow as a project grows, mostly because the same heavy headers (templates, inline functions, deeply nested #include chains) are parsed again for every .cpp file, and because MSVC compiles one file at a time by default. This post covers four settings that address those causes: precompiled headers, parallel compilation with /MP, incremental linking, and forward declarations to trim includes. Unity builds, C++20 modules, and ccache come up briefly in the FAQ.
Required Environment: Visual Studio 2019/2022 (Windows). PCH, /MP and incremental linking are set in the project properties or in CMake. C++20 modules and ccache are optional.
Measure before changing anything
Build time has two very different parts, and the fixes for them differ. Compile time is spread across translation units and is usually dominated by header parsing and template instantiation. Link time is a single step at the end, and in a debug build it can be the biggest part of an incremental build, when you change one file and wait for the whole executable to be relinked.
Visual Studio can tell you which is which. Tools → Options → Projects and Solutions → Build and Run → MSBuild project build output verbosity: Normal plus VC++ Project Settings → Build Timing: Yes prints the time per project and step. For detail, the /Bt+ compiler flag reports front-end and back-end time for every file, and C++ Build Insights (vcperf, viewed in Windows Performance Analyzer) shows which headers and templates consume the most time across the whole build. In my experience a single header, pulled into almost every file through a common “utilities” header, is the most frequent answer, and no compiler setting fixes that as well as removing the include does.
Use Precompiled Header (PCH)
What is PCH?
Precompiled Header (PCH) is a technique that pre-compiles frequently used but unchanging headers (e.g., <iostream>, <vector>, <boost/asio.hpp>) and reuses them in each .cpp file. Saves time parsing and compiling same headers repeatedly.
Why is it so effective?
C++ header files have many nested includes. For example, including <iostream> internally includes dozens of other headers. If project has 100 .cpp files and each includes <iostream>, same header is parsed 100 times. With PCH, parse once and reuse result.
The gain is largest when many .cpp files include the same large headers, such as Boost.Asio or Qt, because that parsing work is done once instead of per file.
Caution:
Only include rarely changed headers in PCH. Including frequently modified project headers can make it slower as PCH must be regenerated.
Setup in Visual Studio
1. Create pch.h file:
// pch.h
#ifndef PCH_H
#define PCH_H
// Frequently used standard library
#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <algorithm>
// External libraries
#include <boost/asio.hpp>
#include <fmt/core.h>
#endif
2. Create pch.cpp file:
// pch.cpp
#include "pch.h"
3. Project property settings:
- pch.cpp: Properties → C/C++ → Precompiled Headers → Create Precompiled Header (/Yc)
- All other .cpp: Properties → C/C++ → Precompiled Headers → Use Precompiled Header (/Yu), Precompiled Header File:
pch.h
4. Include at top of each .cpp file:
#include "pch.h" // ✅ Always first line
#include "myheader.h"
// ...
“First line” is a hard rule, not a style preference. With /Yu, MSVC skips everything in the file up to and including the #include "pch.h" line and replaces it with the precompiled state, so any #define or #include placed above it is silently ignored. A .cpp that does not include the PCH header at all fails with fatal error C1010: unexpected end of file while looking for precompiled header. Did you forget to add '#include "pch.h"' to your source?. Files that should not use the PCH, such as third-party .c sources added to the project, need Not Using Precompiled Headers set on the file itself. Older templates name the header stdafx.h; the name does not matter as long as the /Yc, /Yu and #include settings agree.
For CMake projects, the portable equivalent is target_precompile_headers (CMake 3.16+), which generates the PCH and force-includes it into every source of the target, so the #include "pch.h" line is not needed in each file:
target_precompile_headers(my_app PRIVATE
<vector> <string> <memory> <algorithm>
<boost/asio.hpp>
)
A PCH also hides missing includes: code that uses std::vector without including <vector> compiles because the PCH provides it, and breaks when the file is reused elsewhere or the PCH changes. Tools such as include-what-you-use, or building occasionally with the PCH disabled, catch this.
Effect
How much PCH saves depends on how much of each translation unit is shared header code. Projects built on heavy header libraries like Boost or Qt usually benefit the most; measure a full rebuild before and after to see the effect on your own project.
Enable Parallel Build (/MP)
/MP Option
By default, Visual Studio compiles one .cpp file at a time. Enabling /MP option uses multiple CPU cores for parallel compilation.
Why not default?
Two kinds of parallelism exist, and people often confuse them. MSBuild can build several projects of a solution at once (Tools → Options → Build and Run → maximum number of parallel project builds), which is on by default. /MP parallelizes the files within one project, and it is off by default partly because it is incompatible with some options, such as /Gm (Minimal Rebuild) and #import, and because each extra cl.exe process needs its own memory. For most projects on a modern multi-core machine, turning it on is a clear win.
The two levels multiply: with 8 projects building in parallel and /MP using 8 processes each, you can end up with 64 compiler processes, which exhausts RAM on many machines. If the machine starts swapping during builds, limit one of the two, for example with /MP4.
How much faster?
In theory it scales with core count, but in practice the speedup is well below linear because link stage is not parallelized and some files take much longer creating bottlenecks.
Setup Method
Project properties:
- C/C++ → General → Multi-processor Compilation → Yes (/MP)
Or command line:
msbuild MyProject.sln /p:CL_MPCount=8
CMake:
if(MSVC)
add_compile_options(/MP)
endif()
Effect
On multi-core machines, projects with many .cpp files usually build noticeably faster. Each parallel compiler process needs its own memory, though, so with too little RAM the machine starts swapping and the build can end up slower.
Enable Incremental Link (/INCREMENTAL)
What is Incremental Link?
Incremental Linking is a technique that re-links only changed object files. Link time greatly reduced by not re-linking entire program.
Concretely, the linker leaves padding around each function in the executable and calls functions through a jump table (the incremental linking table). When you change one .cpp, the linker patches the new code into the existing .exe and updates the table instead of writing the whole file from scratch. The padding and indirection make the binary larger and slightly slower, which is fine for debugging and wrong for shipping. It is already on by default in the Debug configuration of a new Visual Studio project; the setting mostly matters for CMake-generated projects and for projects whose settings were changed over the years.
Setup Method
Project properties:
- Linker → General → Enable Incremental Linking → Yes (/INCREMENTAL)
CMake:
if(MSVC)
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} /INCREMENTAL")
endif()
Caution
- Recommended only for debug builds. For release builds, better to do full link for optimization.
- Incremental linking is silently disabled by options that need the whole program, such as
/OPT:REF,/OPT:ICFand/LTCG; the linker prints warningLNK4075: ignoring '/INCREMENTAL' due to '/OPT:ICF' specification. If you see that warning in a debug configuration, one of those options leaked in. - Sometimes incremental link info gets corrupted causing strange errors. In this case, Clean Solution then rebuild.
For even faster debug links, /DEBUG:FASTLINK keeps debug information in the object files instead of merging it into the PDB, at the cost of a PDB that cannot be moved to another machine. Visual Studio 2022 has improved the default /DEBUG:FULL link times, so measure before switching.
Reduce includes with Forward Declaration
Problem Situation
Header (MyClass.h):
#include "HeavyClass.h" // ❌ Include entire HeavyClass definition
class MyClass {
HeavyClass* member; // Only using pointer, don't need full definition
};
If HeavyClass.h is heavy and many files include it, compilation time increases.
Solution: Forward Declaration
// MyClass.h
class HeavyClass; // ✅ Forward declaration
class MyClass {
HeavyClass* member; // Pointer/reference OK with just forward declaration
};
Include actual definition only in MyClass.cpp:
// MyClass.cpp
#include "MyClass.h"
#include "HeavyClass.h" // Include here
void MyClass::someMethod() {
member->doSomething(); // OK
}
The benefit is not only faster compilation of MyClass.h itself. Every file that includes MyClass.h no longer depends on HeavyClass.h, so editing HeavyClass.h rebuilds only the files that really use it. In large projects this dependency effect usually matters more than the parsing time saved.
A forward declaration is enough for pointers, references, function parameters and return types in declarations. It is not enough for a member stored by value, a base class, calling a member function, or sizeof; those need the full definition, and the error is C2027: use of undefined type 'HeavyClass'. The classic trap is std::unique_ptr<HeavyClass> as a member: the header compiles, but any file that destroys a MyClass fails with C2027 (or can't delete an incomplete type) because the implicit destructor is generated inline where HeavyClass is incomplete. Declare the destructor in the header and define it in MyClass.cpp (MyClass::~MyClass() = default;). This is the basis of the pimpl idiom, which takes the same idea further by hiding all private members behind one forward-declared pointer.
Which build-speed fix to try first
| Method | What it speeds up | Difficulty |
|---|---|---|
| PCH | Parsing of shared, rarely changed headers | Easy |
| /MP | Compile stage, across CPU cores | Very Easy |
| Incremental link | Link stage of debug builds | Easy |
| Forward declaration | Rebuilds triggered by header changes | Medium |
| Unity build | Repeated header parsing across files | Medium |
| C++20 modules | Header parsing, replaced by import | Hard |
Measure before choosing. If most of the time goes into compiling, /MP and a precompiled header are the cheapest wins; if a debug build spends a long time in the linker after a one-line change, incremental linking is the fix and the others will not help. C++ Build Insights (the vcperf tool, viewed in Windows Performance Analyzer) shows which headers are parsed most often and which files take longest, which tells you what belongs in the PCH.
The mistake to avoid is putting frequently edited project headers into the PCH. Every change to one of them rebuilds the PCH and therefore every file that uses it, which can make incremental builds slower than before. The PCH should hold standard library and third-party headers that almost never change. And when raising the /MP process count, watch memory: each compiler process holds its own copy of the parsed headers.
Frequently Asked Questions (FAQ)
Q. I enabled /MP but the build is not faster. Why?
A. /MP runs several compiler processes for the source files within one project, so a project with only a handful of .cpp files gains little, and it does nothing for link time. It is also incompatible with some options, notably /Gm (Enable Minimal Rebuild); in that case the compiler warns (D9030) and compiles serially. Check the build output for that warning and turn off /Gm, which is deprecated anyway.
Related Articles
- LNK2019 Unresolved External Symbol in C++
- C++ Forward Declarations
- The Pimpl Idiom
- C++20 Modules Basics