How much do forward declarations affect compile time?

Viewed 9567

I am very interested in some studies or empirical data that shows a comparison of compilation times between two c++ projects that are the same except one uses forward declarations where possible and the other uses none.

How drastically can forward declarations change compilation time as compared to full includes?

#include "myClass.h"

vs.

class myClass;

Are there any studies that examine this?

I realize that this is a vague question that greatly depends on the project. I don't expect a hard number for an answer. Rather, I'm hoping someone may be able to direct me to a study about this.

The project I'm specifically worried about has about 1200 files. Each cpp on average has 5 headers included. Each header has on average 5 headers included. This regresses about 4 levels deep. It would seem that for each cpp compiled, around 300 headers must be opened and parsed, some many times. (There are many duplicates in the include tree.) There are guards, but the files are still opened. Each cpp is separately compiled with gcc, so there's no header caching.

To be sure no one misunderstands, I certainly advocate using forward declarations where possible. My employer, however, has banned them. I'm trying to argue against that position.

Thank you for any information.

7 Answers

I made a small demo which generates artificial codebase and tests this hypothesis. It generates 200 headers. Each header has a struct with 100 fields and a comment 5000 bytes long. 500 .c files are used for benchmarking, each includes all the header files or forward declares all the classes. To make it more realistic, each header is also included into it's own .c file

The result is that using includes took me 22 seconds to compile while using forward declarations took 9 seconds.

generate.py

#!/usr/bin/env python3

import random
import string

include_template = """#ifndef FILE_{0}_{1}
#define FILE_{0}_{1}

{2}
//{3}

struct c_{0}_{1} {{
{4}}};

#endif
"""

def write_file(name, content):
    f = open("./src/" + name, "w")
    f.write(content)
    f.close()

GROUPS = 200
FILES_PER_GROUP = 0
EXTRA_SRC_FILES = 500
COMMENT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=5000))
VAR_BLOCK = "".join(["int var_{0};\n".format(k) for k in range(100)])

main_includes = ""
main_fwd = ""
for i in range(GROUPS):
    include_statements = ""
    for j in range(FILES_PER_GROUP):
        write_file("file_{0}_{1}.h".format(i,j), include_template.format(i, j, "", COMMENT, VAR_BLOCK))
        write_file("file_{0}_{1}.c".format(i,j), "#include \"file_{0}_{1}.h\"\n".format(i,j))
        include_statements += "#include \"file_{0}_{1}.h\"\n".format(i, j)
        main_includes += "#include \"file_{0}_{1}.h\"\n".format(i,j)
        main_fwd += "struct c_{0}_{1};\n".format(i,j)
    write_file("file_{0}_x.h".format(i), include_template.format(i, "x", include_statements, COMMENT, VAR_BLOCK))
    write_file("file_{0}_x.c".format(i), "#include \"file_{0}_x.h\"\n".format(i))
    main_includes += "#include \"file_{0}_x.h\"\n".format(i)
    main_fwd += "struct c_{0}_x;\n".format(i)

main_template = """
{0}

int main(void) {{ return 0; }}

"""

for i in range(EXTRA_SRC_FILES):
    write_file("extra_inc_{0}.c".format(i), main_includes)
    write_file("extra_fwd_{0}.c".format(i), main_fwd)

write_file("maininc.c", main_template.format(main_includes))
write_file("mainfwd.c", main_template.format(main_fwd))


run_test.sh

#!/bin/bash

mkdir -p src
./generate.py
ls src/ | wc -l
du -h src/
gcc -v
echo src/file_*_*.c src/extra_inc_*.c src/mainfwd.c | xargs time gcc -o fwd.out
rm -rf out/*.a
echo src/file_*_*.c src/extra_fwd_*.c src/maininc.c | xargs time gcc -o inc.out
rm -rf fwd.out inc.out src

Results

$ ./run_test.sh 
    1402
8.2M    src/
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
Apple clang version 11.0.3 (clang-1103.0.32.29)
Target: x86_64-apple-darwin19.3.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
       22.32 real        13.56 user         8.27 sys
        8.51 real         4.44 user         3.78 sys

For people using MS Visual Studio, check out a great plugin called Compile Score by Ramon Viladomat.

It pulls information from Clang or MSBuild (pdb) and shows how much time each file operation takes within the entire build run, separating front-end (pre-processor work) from back-end (actual code gen). You can even see which .cpp files included a specific .h and search for the low hanging fruit to speed up your builds. Lots of options and nifty features. Def. worth a try if you have large projects.

Related