Using pre-compiled headers in GCC/Clang using CMake and usage in Catch2
Pre-compiled headers can speed up the total compilation times. As gcc
compiles each C++ file, the pre-processor adds each and every header file into a big file and then compiles that big file. In most cases, the same file is getting compiled multiple times over and over again. If we can process and compile the header files and use the results, we can save a considerable amount of time.
Project files are located here.
Pre-compiled Headers in GCC
Suppose we have the follwing header file hello.h
and source file main.cpp
#pragma once
#include <iostream>
template<typename T>
void say(T t) {
std::cout << t << std::endl;
}
#include "hello.h"
int main() {
say("hello world");
}
Obviously, using g++ main.cpp -o main
will create the output file.
Now, let us ask g++
with -H
flag that outputs all the header files that is included using the command g++ -H main.cpp -o main
and the result is a long list of header files.
. hello.h
.. /usr/include/c++/8/iostream
... /usr/include/x86_64-linux-gnu/c++/8/bits/c++config.h
How if we issue g++ hello.h
, we see that it creates a file called hello.h.gch
. And, then again running g++ -H main.cpp -o main
results in the following output:
! hello.h.gch
main.cpp
and that it is using the pre-compiled header.
Using the pre-compiled headers reduces the compilation time of the above example from 0.38s to 0.15s. However, it does take 0.6s to build the pre-compiled header.
Updating in the header files
If I change the header to add a tab before the say
function, then the above command will only use the pre-compiled header that was previously generated and not give the right answer. If we regenerate the pre-compiled header, then it will generate the right answer.
#include <iostream>
template<typename T>
void say(T t) {
std::cout << '\t' << t << std::endl;
}
This is very dangerous situation since we have the pre-compiled header differing from the header and might lead to frustrations.
We will use CMake to fix the problem so that when hello.h
is updated, then the pre-compiled header is also re-generated.
Pre-Compiled Headers in Clang
Clang works in a similar way to g++. We first create the precompiled header file. The following command produces the hello.h.pch
file as specified.
clang++ -stdlib=libstdc++ -x c++-header hello.h -o hello.h.pch
Different from g++
, we have to add the include in the command line. We can also remove the #include "hello.h"
from the source file main.cpp
but it will just be included twice and ignored the second time from the header guard.
We build the binary using the following command line. Note the inclusion of the header file using the command line.
clang++ -stdlib=libstdc++ -include hello.h main.cpp -o main
Another thing to note is that if I modify the header and use an outdated pre-compiled header file, I get an error from clang. Whereas g++ would happily produce an outdated binary, clang will throw an error and stop the compilation.
fatal error: file 'hello.h' has been modified since the precompiled header 'hello.h.pch' was built
note: please rebuild precompiled header 'hello.h.pch'
1 error generated.
Compilation Times
Similarly as g++, clang also reduced the compilation times after using pre-compiled header. For me, to build the pre-compiled header took 0.353s. To build main with the pre-compiled header took m0.179s. Without pre-compiled headers, clang took 0.381s.
CMake Pre-Compiled Headers
CMake uses the clang
method (which would also work for g++). We use the target_precompile_headers
in CMake to use pre-compiled headers.
Thus, our CMake file for our project would be
cmake_minimum_required(VERSION 3.16)
project(pch-cmake)
add_executable(main main.cpp)
target_precompile_headers(main PRIVATE hello.h)
When we build the project, we get the following output showing that the pre-compiled header is built.
[ 33%] Building CXX object CMakeFiles/main.dir/cmake_pch.hxx.gch
[ 66%] Building CXX object CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable main
[100%] Built target main
Building by clang also works and get the same output. We can use the CC
and CXX
variables to specify the compiler.
export CC=/usr/bin/clang
export CXX=/usr/bin/clang++
cmake ..
-- The C compiler identification is Clang 8.0.0
-- The CXX compiler identification is Clang 8.0.0
-- Check for working C compiler: /usr/bin/clang
-- Check for working C compiler: /usr/bin/clang - works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/clang++
-- Check for working CXX compiler: /usr/bin/clang++ - works
...
Using clang
produces a pch
file rather than a gch
file.
[ 33%] Building CXX object CMakeFiles/main.dir/cmake_pch.hxx.pch
[ 66%] Building CXX object CMakeFiles/main.dir/main.cpp.o
[100%] Linking CXX executable main
[100%] Built target main
Pre-Compiled Catch2 Header for Faster Unit Testing
The basic catch2
test is the following (catch-test.cpp
)
#include "catch.hpp"
TEST_CASE( "Two and Two is Four", "[2+2=4]" ) {
REQUIRE( 2+2 == 5 );
}
To include a main, we have another file with the Catch2
main in the file catch-main.cpp
.
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
Note that each of the file uses catch.hpp
in different ways with a macro in front changing how it is included. With CATCH_CONFIG_MAIN
, a main is included and in the other case it is not included.
This different way of including catch.hpp
is a problem with pre-compiled headers because one pre-compilation will not work with the other.
If we use the pre-compiled header with the main
included using the CATCH_CONFIG_MAIN
macro, then the main
will be added to every file in the project and get multiple definitions of main
.
If the pre-compiled header is without the CATCH_CONFIG_MAIN
macro, then then no file can have a main
function.
To get around the problem, we put the main in a shared library and our test in a different library.
Thus, our cmake looks like the following for the shared library
add_library(catch2_main STATIC catch-main.cpp)
target_include_directories(catch2_main PUBLIC Catch2/single_include/catch2/)
To use it without using pre-compiled headers, we would use it the following way:
add_executable(catch-test2 catch-test.cpp)
target_include_directories(catch-test2 PUBLIC Catch2/single_include/catch2/)
target_link_libraries(catch-test2 catch2_main)
Using the pre-compiled header for our tests and using the shared library with the Catch2 main
add_executable(catch-test catch-test.cpp)
target_link_libraries(catch-test catch2_main)
target_compile_definitions(catch-test PRIVATE CATCH_CONFIG_FAST_COMPILE CATCH_CONFIG_DISABLE_MATCHERS)
target_precompile_headers(catch-test PRIVATE Catch2/single_include/catch2/catch.hpp)
Compilation Times
Without using the pre-compiled headers, the full compilation are the following
catch2_main
shared library : 5.04scatch-test2
not using pre-compiled headers : 1.12scatch-test2
not using pre-compiled headers - test recompilation : 1.13scatch-test
using pre-compiled headers : 1.31scatch-test
using pre-compiled headers - test recompilation : 0.69ss
The test recompilation time improved by around 0.45s which is about 40% reduction in compilation time.
Obsolete - Pre-Compiled Catch2 Header for Faster Unit Testing
This section is obsolete. It uses a slow method of compiling catch2 and not recommended. I have kept the section for didactic purposes.
Lets try to use pre-compiled headers on catch2
unit testing framework that is header only and has a long compile time.
Our project has Catch2
as a submodule from the github
repo. Thus, the single include header file is at the location Catch2/single_include/catch2/catch.hpp
.
The very basic Catch2
test is the following:
#define CATCH_CONFIG_MAIN
#define CATCH_CONFIG_FAST_COMPILE
#define CATCH_CONFIG_DISABLE_MATCHERS
#include "catch.hpp"
TEST_CASE( "Two and Two is Four", "[2+2=4]" ) {
REQUIRE( 2+2 == 5 );
}
The corresponding cmake
entry is the standard header-only usage.
add_executable(catch-test2 catch-test2.cpp)
target_include_directories(catch-test2 PUBLIC Catch2/single_include/catch2/)
Compilation and re-compilation all takes about 5.6s
To use the pre-compiled headers, we use the corresponding CMake to compile the project
add_executable(catch-test catch-test.cpp)
target_compile_definitions(catch-test PRIVATE CATCH_CONFIG_MAIN CATCH_CONFIG_FAST_COMPILE CATCH_CONFIG_DISABLE_MATCHERS)
target_precompile_headers(catch-test PRIVATE Catch2/single_include/catch2/catch.hpp)
The first compile to generate the pre-compiled headers take 6.3s and the subsequent recompilations with changes in the test take 4.7s. Compared to not using pre-compiled headers, we save about 0.9-1 seconds each time we modify the test and re-run the test.
Conclusion
Using pre-compiled headers reduces the compilation time. With the support by cmake
for pre-compiled headers, there is an easy way to integrate pre-compiled headers into our projects.