跳到主要内容

Framework Performance

Overview

Benchmarking was conducted using CMake via the this script. There are 3 benchmark scenarios:

Compilers used:

  • Windows: Microsoft Visual Studio Community 2017 - Version 15.8.1+28010.2003
  • Windows: gcc 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)
  • Linux: gcc 6.3.0 20170406 (Ubuntu 6.3.0-12ubuntu2)
  • Linux: clang 4.0.0-1 (tags/RELEASE_400/rc1) Target: x86_64-pc-linux-gnu

Environment used (Intel i7 3770k, 16GB RAM):

  • Windows 7 - on SSD
  • Ubuntu 17.04 in VirtualBox VM - on HDD

doctest version: 2.2.0 (released on December 02, 2018) Catch version: 2.3.0 (released on July 22, 2018)

Compile-Time Benchmarks

Cost of Including the Header

This benchmark is only relevant to single-header and header-only frameworks – such as doctest and Catch.

The script generates 201 source files: 200 of them create a function in the form of int f135() { return 135; }, and all 200 such dummy functions are forward-declared in main.cpp. Their results are accumulated to return from the main() function. This ensures all source files are built and the linker does not remove or optimize any content.

  • Baseline - Time taken to build source files single-threaded with msbuild/make
  • + Implement - Only in main.cpp, the header is included with a preceding #define to implement the test runner:
    #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
    #include "doctest.h"
  • + header everywhere - The framework header is also included in all other source files
  • + disabled - Remove all testing-related code from the binary
doctestbaseline+ implement+ header everywhere+ disabled
MSVC Debug4.896.218.336.39
MSVC Release4.386.398.716.02
MinGW GCC Debug8.1210.8614.7310.17
MinGW GCC Release8.2111.1115.0310.71
Linux GCC Debug4.206.239.816.24
Linux GCC Release4.296.9311.056.76
Linux Clang Debug8.7010.0214.4311.13
Linux Clang Release9.3011.6816.2011.58
Catchbaseline+ implement+ header everywhere+ disabled
MSVC Debug4.827.8388.8588.72
MSVC Release4.389.9787.1788.35
MinGW GCC Debug8.0057.28137.28132.73
MinGW GCC Release8.3822.9497.1797.22
Linux GCC Debug4.4215.5797.9497.18
Linux GCC Release4.5019.5999.48100.75
Linux Clang Debug8.7615.60107.99110.61
Linux Clang Release9.3225.75118.67117.11

Conclusion

doctest
  • Instantiating the test runner in one source file takes approximately 1–3 seconds (implement - baseline)
  • Including "doctest.h" in one source file costs 11 ms – 23 ms ((header_everywhere - implement) / 200)
  • Including the library everywhere with all features disabled costs approximately 2 seconds (disabled - baseline) for 200 files
Catch
  • Instantiating the test runner in one source file takes approximately 3–50 seconds (implement - baseline)
  • Including catch.hpp in one source file costs 380 ms – 470 ms ((header_everywhere - implement) / 200)
  • Disabling the library with the CATCH_CONFIG_DISABLE configuration option has no impact on header inclusion cost

Therefore, if "doctest.h" costs 11 ms on MSVC and "catch.hpp" costs 400 ms – the doctest header is >> 36<< times lighter (for MSVC)!

The results are shown in seconds and are by no means intended to criticize Catch – the doctest framework would not exist without it.

The doctest header has minimal compile-time overhead because it forward-declares all content and does not pull any headers into source files (except the one implementing the test runner). This is a key design decision.

Cost of an Assertion Macro

The script generates 11 .cpp files: 10 files create 50 test cases each, with 100 assertions per test case (in the form of CHECK(a==b) where a and b are the same int variable) – totaling 50k assertions! The test framework is implemented in main.cpp.

  • Baseline - Time for a single-threaded build with the header included everywhere (no test cases or assertions)
  • CHECK(a==b) - Adds CHECK() assertions that decompose expressions using template mechanisms

doctest specific:

Catch specific:

  • +fast - Adds CATCH_CONFIG_FAST_COMPILE to speed up compilation of normal CHECK(a==b) assertions
  • +disabled - Disables all test cases and assertion macros with CATCH_CONFIG_DISABLE
doctestbaselineCHECK(a==b)+fast 1CHECK_EQ(a,b)+fast 2+disabled
MSVC Debug2.6927.3710.3717.174.821.91
MSVC Release3.1558.7320.7326.076.431.83
MinGW GCC Debug3.7897.2943.0559.8611.881.67
MinGW GCC Release4.09286.7095.42156.7318.162.03
Linux GCC Debug2.3991.3641.9252.2610.161.32
Linux GCC Release3.29257.4097.46128.8419.381.79
Linux Clang Debug2.4085.5243.5351.248.321.62
Linux Clang Release3.40160.6579.3481.5211.901.82

Here are the results for Catch (only supports normal CHECK(a==b) assertions):

CatchbaselineCHECK(a==b)+fast+disabled
MSVC Debug8.2031.2225.548.22
MSVC Release10.13448.68168.6710.20
MinGW GCC Debug53.54152.38131.8549.07
MinGW GCC Release19.26590.16466.6918.99
Linux GCC Debug15.05117.3095.3314.79
Linux GCC Release18.77608.94482.7318.96
Linux Clang Debug12.2794.3977.3312.11
Linux Clang Release20.75545.84506.0220.15

Conclusion

doctest:

  • Regex-decomposed CHECK(a==b) assertions are 0–8 times faster than Catch
  • CHECK_EQ(a,b) assertions (no expression decomposition) are approximately 31–63% faster than CHECK(a==b)
  • The DOCTEST_CONFIG_SUPER_FAST_ASSERTS identifier speeds up normal assertions by 57–68%
  • The DOCTEST_CONFIG_SUPER_FAST_ASSERTS identifier speeds up binary assertions by an additional 84–91%
  • The DOCTEST_CONFIG_DISABLE identifier makes assertions disappear entirely (even faster than baseline, as most implementation code is removed)

Catch:

  • CATCH_CONFIG_FAST_COMPILE speeds up assertion compilation by 10–30% (73% in one case)
  • CATCH_CONFIG_DISABLE provides the same significant benefit for assertions as DOCTEST_CONFIG_DISABLE – but does not reduce header inclusion cost

Runtime Benchmarks

Runtime benchmarks use a single test case with a loop of 10 million iterations: either a single normal assertion (with expression decomposition) or an assertion plus logging of the loop iterator i:

for(int i = 0; i < 10000000; ++i)
CHECK(i == i);

or

for(int i = 0; i < 10000000; ++i) {
INFO(i);
CHECK(i == i);
}

Note: All assertions pass – the goal is to optimize for the common case (many passing test cases, few failures).

doctestassert+ info                               Catchassert+ info
MSVC Debug4.0011.41MSVC Debug5.60213.91
MSVC Release0.401.47MSVC Release0.767.60
MinGW GCC Debug1.052.93MinGW GCC Debug1.179.54
MinGW GCC Release0.341.27MinGW GCC Release0.364.28
Linux GCC Debug1.242.34Linux GCC Debug1.449.69
Linux GCC Release0.290.52Linux GCC Release0.293.60
Linux Clang Debug1.152.38Linux Clang Debug1.219.91
Linux Clang Release0.280.50Linux Clang Release0.323.27

Conclusion

doctest assertions are approximately 20% faster than Catch's, and several times faster (over 18 times faster in one specific compiler scenario) when logging variables and context.


Bar charts were generated by pasting table data into this Google Spreadsheet.

For a non-synthetic benchmark, see this blog post by Baptiste Wicht, who tested assertion compile times in version 1.1 with his expression template library!

When reading the article: note that if a process segment takes 50% of the total time and is sped up 10,000 times, the entire process will only be about 50% faster overall.