Benchmarks

The benchmarks are done with this script using CMake. There are 3 benchmarking scenarios:

Compilers used:

  • WINDOWS: Microsoft Visual Studio Community 2017 - Version 15.3.3+26730.12
  • WINDOWS: gcc 7.1.0 (x86_64-posix-seh-rev2, 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, 16g RAM):

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

doctest version: 1.2.2 (released on 2017.09.05)

Catch version: 2.0.0-develop.3 (released on 2017.08.30)

Compile time benchmarks

Cost of including the header

This is a benchmark that is relevant only to single header and header only frameworks - like doctest and Catch.

The script generates 201 source files and in 200 of them makes a function in the form of int f135() { return 135; } and in main.cpp it forward declares all the 200 such dummy functions and accumulates their result to return from the main() function. This is done to ensure that all source files are built and that the linker doesn't remove/optimize anything.

  • baseline - how much time the source files need for a single threaded build with msbuild/make
  • + implement - only in main.cpp the header is included with a #define before it so the test runner gets implemented:
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
  • + header everywhere - the framework header is also included in all the other source files
  • + disabled - remove everything testing-related from the binary
doctestbaseline+ implement+ header everywhere+ disabled
MSVC Debug6.778.2811.738.73
MSVC Release6.358.5712.188.28
MinGW GCC Debug10.2313.0317.6212.29
MinGW GCC Release10.3313.6817.8713.11
Linux GCC Debug5.016.2410.486.49
Linux GCC Release4.587.3011.707.41
Linux Clang Debug8.809.7014.9210.89
Linux Clang Release9.2912.0517.5111.56
Catchbaseline+ implement+ header everywhere+ disabled
MSVC Debug6.7810.00107.85115.05
MSVC Release6.3611.19102.69109.06
MinGW GCC Debug10.3641.83124.41126.70
MinGW GCC Release10.4921.9397.81105.47
Linux GCC Debug4.4012.3994.3493.68
Linux GCC Release4.5515.7594.2893.80
Linux Clang Debug9.3015.00105.84103.05
Linux Clang Release9.6822.75114.36111.32

Conclusion

doctest

  • instantiating the test runner in one source file costs ~1-3 seconds implement - baseline
  • the inclusion of doctest.h in one source file costs between 17ms - 27ms (header_everywhere - implement) / 200
  • including the library everywhere but everything disabled costs around 2 seconds disabled - baseline for 200 files

Catch

  • instantiating the test runner in one source file costs ~4-31 seconds implement - baseline
  • the inclusion of catch.hpp in one source file costs between 390ms - 490ms (header_everywhere - implement) / 200
  • using the config option to disable the library (CATCH_CONFIG_DISABLE) has no effect on the header cost

So if doctest.h costs 17ms and catch.hpp costs 490ms on MSVC - then the doctest header is >> 27 << times lighter (for MSVC)!


The results are in seconds and are in no way intended to bash Catch - the doctest framework wouldn't exist without it.

The reason the doctest header is so light on compile times is because it forward declares everything and doesn't drag any headers in the source files (except for the source file where the test runner gets implemented). This was a key design decision.

Cost of an assertion macro

The script generates 11 .cpp files and in 10 of them makes 50 test cases with 100 asserts in them (of the form CHECK(a==b) where a and b are always the same int variables) - 50k asserts! The testing framework gets implemented in main.cpp.

  • baseline - how much time a single threaded build takes with the header included everywhere - no test cases or asserts!
  • CHECK(a==b) - will add CHECK() asserts which decompose the expression with template machinery

doctest specific:

  • CHECK_EQ(a,b) - will use CHECK_EQ(a,b) instead of the expression decomposing ones
  • FAST_CHECK_EQ(a,b) - will use FAST_CHECK_EQ(a,b) instead of the expression decomposing ones
  • +faster - will add DOCTEST_CONFIG_SUPER_FAST_ASSERTS which speeds up FAST_CHECK_EQ(a,b) even more
  • +disabled - all test case and assert macros will be disabled with DOCTEST_CONFIG_DISABLE

Catch specific:

  • +faster - will add CATCH_CONFIG_FAST_COMPILE which speeds up the compilation of the normal asserts CHECK(a==b)
  • +disabled - all test case and assert macros will be disabled with CATCH_CONFIG_DISABLE
doctestbaselineCHECK(a==b)CHECK_EQ(a,b)FAST_CHECK_EQ(a,b)+faster+disabled
MSVC Debug3.0823.7218.158.385.672.23
MSVC Release3.6143.7524.2811.367.222.15
MinGW GCC Debug3.9085.4758.6224.4012.121.71
MinGW GCC Release4.51224.49148.8447.2518.732.40
Linux GCC Debug2.0178.3850.6117.629.871.11
Linux GCC Release3.20199.78123.4232.4719.521.97
Linux Clang Debug1.7177.3949.9717.607.571.18
Linux Clang Release3.64136.8280.1920.7212.341.45

And here is Catch which only has normal CHECK(a==b) asserts:

CatchbaselineCHECK(a==b)+faster+disabled
MSVC Debug9.5837.6925.2110.40
MSVC Release10.85260.55121.3811.56
MinGW GCC Debug36.24159.15133.9833.57
MinGW GCC Release16.15740.71562.6016.41
Linux GCC Debug12.71142.92108.0712.05
Linux GCC Release15.62825.42612.0615.51
Linux Clang Debug10.48115.1989.5910.78
Linux Clang Release18.25393.31316.9817.19

Conclusion

doctest:

  • is around 30% to 75% faster than Catch when using normal expression decomposing CHECK(a==b) asserts
  • asserts of the form CHECK_EQ(a,b) with no expression decomposition - around 25%-45% faster than CHECK(a==b)
  • fast asserts like FAST_CHECK_EQ(a,b) with no try/catch blocks - around 60-80% faster than CHECK_EQ(a,b)
  • the DOCTEST_CONFIG_SUPER_FAST_ASSERTS identifier which makes the fast assertions even faster by another 50-80%
  • using the DOCTEST_CONFIG_DISABLE identifier the assertions just disappear as if they were never written

Catch:

  • using CATCH_CONFIG_FAST_COMPILE results in 15%-55% faster build times for asserts.
  • using the CATCH_CONFIG_DISABLE identifier provides the same great benefits for assertion macros as the doctest version (DOCTEST_CONFIG_DISABLE) - unlike the case for the header cost

Runtime benchmarks

The runtime benchmarks consist of a single test case with a loop of 10 million iterations performing the task - a single normal assert (using expression decomposition) or the assert + the 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 that the assert always passes - the goal should be to optimize for the common case - lots of passing test cases and a few that maybe fail.

| doctest | assert | + info |                                 | Catch | assert | + info | |---------------------|---------|---------|-|---------------------|---------|---------| | MSVC Debug | 5.04 | 13.03 | | MSVC Debug | 101.07 | 338.41 | | MSVC Release | 0.73 | 1.67 | | MSVC Release | 1.75 | 10.99 | | MinGW GCC Debug | 2.11 | 4.50 | | MinGW GCC Debug | 4.76 | 18.22 | | MinGW GCC Release | 0.36 | 0.86 | | MinGW GCC Release | 1.24 | 7.29 | | Linux GCC Debug | 2.49 | 4.97 | | Linux GCC Debug | 5.41 | 19.01 | | Linux GCC Release | 0.29 | 0.66 | | Linux GCC Release | 1.20 | 7.88 | | Linux Clang Debug | 2.39 | 4.76 | | Linux Clang Debug | 5.12 | 17.66 | | Linux Clang Release | 0.39 | 0.70 | | Linux Clang Release | 0.99 | 7.26 |

Conclusion

doctest is significantly faster - between 2.5 and 26 times.

In these particular cases doctest makes 0 allocations when the assert doesn't fail - it uses lazy stringification (meaning it stringifies the expression or the logged loop counter only if it has to) and a small-buffer optimized string class to achieve these results.


The bar charts were generated using this google spreadsheet by pasting the data from the tables.

If you want a benchmark that is not synthetic - check out this blog post of Baptiste Wicht who tested the compile times of the asserts in the 1.1 release with his Expression Templates Library!

While reading the post - keep in mind that if a part of a process takes 50% of the time and is made 10000 times faster - the overall process would still be only roughly 50% faster.


Home