TTS v4.0.0
The Tiny Test System
Loading...
Searching...
No Matches
Compile-Time Cost

Standard headers in TTS

Objectives

The library is header-only, and a test suite holds dozens or hundreds of small translation units, each including <tts/tts.hpp> on its own. No separately compiled object amortizes the parsing, so each header included by TTS is parsed once per test file and per build, in each project using the library. The CI matrix of the library alone rebuilds the suite on more than a dozen combinations of compiler, system, and standard library per pull request, so a header added to a widely included file adds its parsing time to each of those builds.

The library therefore holds small replacements for the facilities it needs, listed in TTS replacements, rather than including the standard headers.

Costly headers

The cost of a header is measured before the header is included, since the name of a header does not predict its cost. The times below are what a header adds to a minimal TTS_MAIN translation unit, with g++ 13 and clang++ 18, -std=c++20, best of eight runs.

Header g++ clang++ Replacement
<chrono> +460ms +680ms now_ns() in tools/clock.hpp
<regex> +270ms +350ms none, not used
<thread> +200ms +240ms none, not used
<memory> +170ms +210ms file_guard in tools/file.hpp
<sstream> +140ms +190ms tts::text in tools/text.hpp
<iostream> +140ms +170ms fputs on a FILE*, in place of std::cout
<functional> +100ms +100ms callable in tools/erased.hpp
<string> +90ms +110ms tts::text in tools/text.hpp
<unordered_map> +60ms +60ms none, not used
<vector> +50ms +60ms tts::buffer in tools/buffer.hpp
<map> +50ms +50ms none, not used
<array> +20ms +20ms a C array sized by a constant

Acceptable headers

A standard header is allowed once measured as free:

  • two translation units, identical but for the header, are compiled with -std=c++20 -c five to eight times each, and their best times are compared;
  • g++ and clang++ are both measured, since the same header costs them differently;
  • the pull request that adds the header gives both times.

<windows.h> is included only on the Windows branch of tools/clock.hpp, guarded by WIN32_LEAN_AND_MEAN and NOMINMAX. Measured on cl.exe, it costs nothing relative to an empty translation unit, and less than declaring by hand the three functions it provides. Platform-specific code is measured on that platform.

engine/deps.hpp, included by nearly every TTS header, already includes the acceptable headers, so using one of them costs nothing more.

TTS replacements

Each file below replaces a standard header listed in the table above, and is a model for a new replacement:

  • tools/clock.hpp: now_ns(), a portable monotonic timer in nanoseconds built on clock_gettime or QueryPerformanceCounter, in place of std::chrono.
  • tools/file.hpp: file_guard, a move-only RAII FILE* wrapper, in place of std::unique_ptr.
  • tools/text.hpp: tts::text, a minimal string type built on malloc and snprintf, in place of std::string and the string streams of <sstream>.
  • tools/buffer.hpp: tts::buffer, a minimal malloc-based dynamic array, in place of std::vector.
  • tools/erased.hpp: callable, a type-erased wrapper that stores each TTS_CASE body, in place of std::function.

Other test libraries

Each publication of this documentation builds test executables with TTS, doctest, Catch2, GoogleTest, Boost.Test, and ut, and measures the CPU time of their compilation and link with -std=c++20. Each executable runs and passes before its time is kept, and each point is the median of several builds. Each publication runs on a new CI machine, named below, and its times compare only with one another.

Measured on 2026-10-14 on AMD EPYC 7763 64-Core Processor, 4 cores, with clang++ 22.1.2, g++ 15.2.0 and -O0: the median of 3 builds per point, made in rounds that alternate the libraries, 2 at a time.

Every test is the same trivial case, three checks on two integers. Each library is set up as its documentation sets it up:

  • TTS, doctest, and Boost.Test compile their runner in the unit that defines TTS_MAIN, defines DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN, or includes boost/test/included/unit_test.hpp: the test file of a one-file executable, and a main.cpp of its own otherwise.
  • ut runs its tests from its header, so every test file holds its runner.
  • Catch2 and GoogleTest link Catch2WithMain and gtest_main, built once per build tree and left out of the executables.
  • doctest checks with its decomposing CHECK(a == b); its binary CHECK_EQ(a, b) costs less per test.

Include cost

A test file holding neither runner nor test, compiled with -c. The unit of ut holds its runner.

  • g++ Include cost with gcc
  • clang++ Include cost with clang

One test

An executable built from one file holding the runner and one test.

  • g++ One test executable with gcc
  • clang++ One test executable with clang

Tests in one file

An executable built from one file holding the runner and a growing number of tests. Each curve is fitted by least squares to a fixed cost plus a cost per test.

  • g++ Executables of one file with gcc

    Cost of an executable from one file holding \(n\) tests, fitted as a fixed part and a part per test:

    LibraryCost
    TTS\(0.34\,\mathrm{s} + 2.1\,\mathrm{ms} \cdot n\)
    Boost.Test\(6.17\,\mathrm{s} + 12.4\,\mathrm{ms} \cdot n\)
    Catch2\(1.02\,\mathrm{s} + 4.8\,\mathrm{ms} \cdot n\)
    GoogleTest\(0.92\,\mathrm{s} + 8.0\,\mathrm{ms} \cdot n\)
    doctest\(1.81\,\mathrm{s} + 5.4\,\mathrm{ms} \cdot n\)
    ut\(1.76\,\mathrm{s} + 2.8\,\mathrm{ms} \cdot n\)
  • clang++ Executables of one file with clang

    Cost of an executable from one file holding \(n\) tests, fitted as a fixed part and a part per test:

    LibraryCost
    TTS\(0.34\,\mathrm{s} + 1.2\,\mathrm{ms} \cdot n\)
    Boost.Test\(5.01\,\mathrm{s} + 9.8\,\mathrm{ms} \cdot n\)
    Catch2\(2.20\,\mathrm{s} + 5.0\,\mathrm{ms} \cdot n\)
    GoogleTest\(1.63\,\mathrm{s} + 5.5\,\mathrm{ms} \cdot n\)
    doctest\(2.05\,\mathrm{s} + 4.9\,\mathrm{ms} \cdot n\)
    ut\(2.97\,\mathrm{s} + 1.2\,\mathrm{ms} \cdot n\)

Cost of one test

A test file without runner holding 200 tests, compiled with -c. The cost of one test is its time minus that of the test file with no test, divided by the number of tests. The runner stays out: for some libraries, it varies more between two builds than 50 tests add to the curve above.

  • g++ Cost of one test with gcc
  • clang++ Cost of one test with clang

Tests in many files

An executable built from a runner and a growing number of files of one test each, linked together. TTS, doctest, and Boost.Test compile their runner once, in its own file; ut compiles it in each file.

  • g++ Executables of many files with gcc
  • clang++ Executables of many files with clang