Introduction
The goal of this project is to compare several generic container C libraries that provide some STL like capabilities of the C++ (container template) but are targeting classic C language.
The Standard Template Library STL was a software library for the C++ programming language that influenced many parts of the C++ Standard Library. It provided a set of container classes for C++ that can be used with any type. Now the parts of the C++ Standard Library directly influenced/inherited from this STL are what are commonly called "the STL".
So, a STL like library for the C is a C library providing several classic generic containers for the C language, like vector, list, sorted set, unordered_map, and so on.
To do this, the same simple programs are implemented by the libraries in the more straight-forward way possible, for different kind of containers and for different types. Then the API ergonomics of each programs can be compared each other according to the user taste.
A benchmark to compare their performance is included in the bench directory.
Objective characteristics of the libraries are directly compared in this file.
But first, let's see how we can implement generic code in C.
Implementing generic code in C
One of the main issue in the C language is how to write generic code, i.e. code that works the same for different kind of objects: you write once, and it works for several types. A kind of equivalent of the templates of the C++.
There are several ways of doing so:
voidp: Everything is a pointer to void.
Each object is handled through a pointer to void. The container store only pointers to these objects. Potentially, it may also register some callback functions to handle the contained objects for the needed specialized methods (copy, drop, ...). From a user point of view, this makes the code harder to use and to debug (as you don't have any help from the compiler) and type unsafe since a lot of cast is needed to handle it (so no formal proof of the code is usually possible). This also generally generate slower code (due to the multiple reduction, indirect callback, increase memory usage and cache miss) even if using link time optimization and full inline functions could reduce this a little (with inlining and LTO, some callback overhead can be reduced but it is really dependent on the compilers and will increase your compilation time). Properly used, it can yet be the most space efficient for the code, but can consume more for the data due to the obligation of using pointers (depending on your use case). This is however the easiest to design & code.
Pros:
- easy to develop,
- reduced code.
Cons:
- slow code,
- data memory usage,
- hard to debug for user,
- type unsafe.
Note: See synthesis for mixing macros with typed handles and void * elements and creating hybrid solutions.
macro: Everything is a macro
Macros are expanded in the user code and used to access structures in a generic way (using known named fields of a structure — typically size, capacity, etc.). The macro is fully always expanded in the user code. From a user point of view, this can create subtle bugs in the use of the library (as everything is done through macro expansion in the user defined code) and hard to understand warnings. This can be mitigated using proper macros expansion and type checking, but it increases the complexity of the solution. This can generates fully efficient code. From a library developer point of view, this can be quite limiting in what you can offer.
This technique can also enforce the type consistency by encoding the type in the macro name or by using typeof/_Generic internally to check for the right types.
A key macro pitfall is evaluating arguments multiple times, and a good library should prevent misuse of this if possible.
Pros:
- fast to develop,
- efficient code.
Cons:
- potentially type unsafe,
- error prone,
- macro Side-effect,
- can have cryptic error message if usage is incorrect
- can have incorrect code usage without any error message if usage is incorrect
Note: Not to be confused with Template macros.
OO-style object
The objects inherited from all the same base object. The base object has a virtual table that provides the callbacks to all the methods needed to handle such object. The generic code is a simple classic C code that handles only the base object: therefore it uses the provided callback in its vtable to handle the object for any operation. This is an alternative to everything is a void pointer.
Indirect calls are usually slower even if modern CPUs and branch prediction often make this cost modest (the overhead may still be negligible).
It can be a good fit for a specialized API with only custom types and custom algorithms that don't need a lot of speed.
Pros:
- reduce code size
- support different kind of objects in the same container
Cons:
- data memory usage
- slow code
- new operation requires rework of vtable
- complex development
Generic objects
Another alternative is encapsulating the methods in macros that detect the type of the argument passed as parameter using _Generics, before calling the associated method according to the given type.
The C standard mandates to have valid C expressions in all cases of the _Generic. This is not a problem for very simple use of _Generic (such as the examples provided in the standard to return a different function depending on the types). But more advanced uses cannot be implemented so simply: they need to generate complex expression. Therefore, such library usually uses a double _Generic switch to overcome this limitation (increasing the number of used _Generic a lot and making code more awkward). There is discussion to remove this limitation in future C standards.
The difficulty is how to add pure user types in this generic switch (possible, just awkward and it needs high C skill level to do it).
Using this technique means using a lot of _Generic in the code. Real examples of it show notable slower compilation. Maybe it is due to compiler naive implementation of _Generic?
It is usually not used alone but with other technics (like void pointer or container instantiations) in order to create an hybrid approach.
As they need to hide the _Generic in a macro, they are subject to the key macro pitfall in evaluating arguments multiple times (a good library should prevent misuse of this if possible).
Pros:
- uniform interface provided to the user.
Cons:
- macro Side-effect shall be protected,
- support of custom user types is difficult.
- potential slow compilation
Intrusive container
A known structure is put in an intrusive way in the type of all the objects you wan to handle. From a user point of view, he needs to modify its structure and has to perform all the allocation & deallocation code itself (which is good or bad depending on the context). This can generate efficient code (both in speed and size). From a library developer point of view, this is easy to design & code. You need internally a cast to go from a pointer to the known structure to the pointed object (a reverse of offsetof) that is generally type unsafe (except if mixed with the macro generating concept).
This is quite limited in what you can do: you usually don't want to move your objects so any container that has to move some objects is not a good fit (which means that you cannot use the most efficient container). Indeed You can move intrusive objects if you update or rebuild the intrusive links appropriately, but it will be awkward and impact performance.
Intrusive containers are a good fit when object lifetime is managed externally (e.g., kernel structures) as all allocations are usually let to the user. They are also a good fit when the same object can be within several containers.
Pros:
- reduce code size
- efficient code
- external object lifetime
Cons:
- data memory usage
- limited scope of the containers
Template header
Header files are included multiple times with different macro contexts (which act as arguments of the header) in order to generate different code for each type.
From a user point of view, this creates a new step before using the container: an instantiating stage that has to be done once per type and per compilation unit (The user is responsible to create only one instance of the container, which can be troublesome if the library doesn't handle proper prefix for its naming convention). This instantiating stage generates the functions handling the container with the correct type, ensuring type safety and control by the compiler.
This instantiating stage can generate only the external interface (extern declaration), the implementation or inline implementation in function of what the user requests (reducing code bloat for containers which are heavily used so that only one instance exists in the whole program).
The debug of the library is generally reasonable. It can generate fully specialized & efficient code. Incorrectly used, this can generate a lot of code bloat. Properly used, this can even create smaller code than the void pointer variant. The interface used to configure the library can be quite tiresome in case of a lot of specialized methods for configuring the used user type: it doesn't enable to chain the configuration from a container to another one easily.
The user code is a little bit more verbose as it uses specialized function names, and not generic ones.
Pros:
- specialized & efficient code
- type safe
- easy to use for user
- reasonable to develop and understand
Cons:
- Explicit instance needed
- can generate code bloat if incorrectly used
- can have cryptic error message at instantiation stage if incorrectly used
- method names are more verbose
- unnatural usage of headers.
Template macros
Macros are used to generate context-dependent C code enabling to generate code for different type. This is pretty much like the template headers solution but with added flexibility. From a user point of view, this creates a new step before using the container: an instantiating stage that has to be done once per type and per compilation unit (The user is responsible to create only one instance of the container, which can be troublesome if the library doesn't handle proper prefix for its naming convention).
This can generate fully specialized & efficient code with the correct type ensuring type safety and control by the compiler. Incorrectly used, this can generate a lot of code bloat. Properly used, this can even create smaller code than the void pointer variant.
This instantiating stage can generate only the external interface (extern declaration), the implementation or inline implementation in function of what the user requests (reducing code bloat for containers which are heavily used so that only one instance exists in the whole program).
From a library developer point of view, the library is harder to debug: everything being expanded in one line, you can't step in the library (there is however a solution to overcome this limitation by adding another stage to the compilation process: generating the preprocessed file (-E), cleaning it of the lines directives, and recompiling this file). But this should not impact the user if the library is properly maintained.
This can also generates cryptic error messages at user level if incorrectly used when creating an instance. You can however see the generated code by looking at the preprocessed file.
You can perform heavy context-dependent customization of the code (transforming the macro preprocessing step into its own language): you can generate a variable number of methods depending on the given arguments of your instantiation, which is not possible easily with any other methods. Also, Properly done, you can also chain the methods from a container to another one easily, enabling quick and easy expansion of the containers. Errors in user code are easy to read and natural. Code usage is a little bit more verbose as it uses specialized function names, and not generic ones.
Template macros are not subject to the key macro pitfall of evaluating arguments multiple times.
Note: not to be confused with usage of Macro for meta-programming. Template Macros are much closer to Template Headers.
Note: Some people reported "issue" of the template macros with their look due to the use of backslash at the end of the lines however it is due to their coding practices (you need to align backslash at the same column). Another one of the reported "issue" is the lack of support of syntax highlighting and autocomplete in macros but... it was more an issue in their used text editor.
Pros:
- specialized & efficient code
- type safe
- easy to use for user
- maximum flexibility in code generation
Cons:
- Explicit instance needed
- can generate code bloat if incorrectly used
- little harder to debug for the library developer
- can have cryptic error message at instantiation stage if incorrectly used
- method names are verbose
Synthesis
Even if a container is usually slow, it may be a good fit for your use case. There is no universal answer.
Some hybrid patterns of the previous solutions (e.g., _Generic + templates, opaque wrappers around void *) can also be chosen for a specific usage, in order to mitigate the Cons of both solution, increasing their added value.
For example, a common pattern is to define the public API using some types (e.g., struct my_vec *), but internally performs some cast to void * to use functions on void pointers. This gives better ergonomics and some type safety at the container level, even if element type is ultimately erased.
In practice, Template headers are often the best compromise between ease of development and what is possible with it. Template macros are a little bit harder to develop but increases the generation possibility. void pointer solutions are often the one that generate the smaller code but at the cost of performance and harder debug at user level.
C libraries Selection
The following C libraries have been selected as their aim is to provide generic containers to the C language:
- C-Macro-Collections
- COLLECTIONS-C
- CTL by glouw or by rurban
- M*LIB
- STC - Smart Template Container for C
- CC
- GLIB
- STB_DS
- KLIB
with C++ STL used as the reference baseline.
The used versions for the manual comparison are:
| COMPONENT | VERSION |
|---|---|
| C Macro Collections | v0.23.1 |
| CollectionsC | ff1be366329e2c82cd85b2c803114ef8d2115f7f |
| CTL | 3923e6776a231e5d58cf91225ca8a1d61879401b |
| M*LIB | a0818419ab959e05517336e1bea699c1854b29f3 |
| STC | 5fb5ed08250b5ad4eadd6e7a9fdc44f4519b15ff |
| CC | 2012d9d2eb8f035d7dc69f36ec03ca3199ede1bf |
| GLIB | 2.74 |
| STB_DS | 904aa67e1e2d1dec92959df63e700b166d5c1022 |
| KLIB | 97a0fcb790b43b9e5da8994f4671021fec036f19 |
More specialized C libraries which provides only one kind of container are not included in this chapter. sglib is not included due to being abandoned.
Feature Comparison
Analysis
The following characteristics are compared. The C++ STL is also included as reference. For a container of such library that encapsulates a collection of objects of basic type, the following criterions are analyzed:
- What is the license?
- What is the supported C language? (C89, C99, C11 or C23, with or without extension)
- Is it a pure C program? (no need for external preprocessor)
- Is it Header only?
- How is implemented the Generic mechanism? By using (VP) void pointer, (M) macro, (OO) Object Oriented, (GO) Generic objects, (IF) intrusive field, (TH) template header, (TM) template macro
- Is it type safe (aka. using an incompatible type produces at least a compilation warning)?
- Does it support of integer/floats as basic type?
- Does it support of struct POD data as basic type?
- Does it support of array as basic type?
- Does it support of object like data (needing custom constructor, copy, destructor...) as basic type?
- Does it support of C++ class as basic type?
- Does it support of Assignment semantics? (container uses the C Assignment operators to set object in them)
- Does it support of copy semantics? (container creates a proper copy of the object data as per the object semantic: if there is pointer in the structure, it performs a proper copy of the pointed objects)
- Does it support of move semantics? (container steals the ownership of the object given as parameter as per the object semantic, rendering the original object in a destroyed or nearly destroyed state)
- Can the container and its basic type be defined fully separately in the source code? (spatial separation: the association of the methods of the basic type to the needed operators of the container library can be defined when the basic type is defined -ensuring spatial coherency of the basic type- and not only when the container is defined)
- Does it support of an adaptation layer? (a way to transform the interface of the provided method of the basic type to the expected interface of the operator of the container without writing explicitly a wrapper)
- Does it support of basic 'emplace'?
- Does it support of multiple, enhanced 'emplace' based on the initialized arguments?
- Does it support of iterator abstraction?
- Does it support of sort algorithm?
- Does it support of sort algorithm with custom comparison?
- Does it support of single definition for the whole program? (use of declaration only for all files except one that includes the container definition)
- Does it support of full abstraction of the container type? (user shall not have to use internal fields)
- Does it support of contract violation checks? (assertions on invalid inputs, on input contract violation or error reporting)
- Does it support of natural usage of array? (using of Array subscripting on the container)
- Is the basic type stored in the container, not a pointer to it?
- Does it need an explicit instantiation of the container with the basic type before its usage?
- Are the functions properly prefixed?
- How are the memory errors handled? (return code, exception, abort, none)
- Are destructors of objects on stack properly called on exception?
- Does it support custom memory functions?
- Does it support optional per-container context for custom memory functions?
- Does it support of forward declaration of container?
- Does it support of serialization? (JSON, XML, YAML)
Synthesis
| Characteristics | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| License | NA | BSD2 | MIT | MIT | MIT | LGPL3 | MIT | LGPL2.1 | MIT | MIT | Apache |
| C language | NA | >=C99 | >=C99 | >=C99 | >=C99 | >=C99 | >=C11* or >=C23 | >=C89 | >=C99* or >=C23 | >=C99 | >=C23 |
| Pure C | NA | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| Header only | Y | Y | Y* | Y | Y | N | Y | N | Y | Y | N |
| Generic mechanism | template | TM | TH | TM | TH | VP | M+GO | VP | M | TM | VP |
| type safe | Y | Y | Y | Y | Y | N | Y* | N | N* | Y | N |
| Characteristics | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| integer/float support | Y | Y | Y | Y | Y | Y* | Y | Y* | Y | Y | Y |
| struct POD support | Y | Y | Y | Y | Y | Y* | Y | Y* | Y | Y | Y |
| array support | Y | Y | N | N | N | Y* | N | Y* | N | N | Y |
| C object support | Y | Y | Y | Y | Y | Y* | Y | Y* | N | Y | Y |
| C++ class support | Y | Y | N | N | N | N | N | N | N | N | N |
| Characteristics | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Assignment semantics | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| Copy semantics | Y | Y | Y* | N | Y* | Y | N | Y | N | N | Y* |
| Move semantics | Y | Y | N | N | N | N | N | N | N | N | N |
| spatial separation | Y | Y | N | N | N | NA | Y | NA | N | N | NA |
| Adaptor Layer | N | Y | N | N | N | N | N | N | N | N | N |
| Basic emplace support | Y | Y | Y | N | N | N | N | N | N | N | N |
| Enhanced emplace support | Y | Y | N | N | N | N | N | N | N | N | N |
| Iterator support | Y | Y | Y | N | Y | Y | Y | N | N | Y | Y |
| Sort algorithm | Y | Y | Y | N | Y | Y | N | Y | N | Y | Y |
| Enhanced Sort algorithm | Y | Y | Y | N | Y | Y | N | Y | N | N | Y |
| Characteristics | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| single linkage definition | N* | Y | Y | Y | N | Y | N | Y | Y | N | Y |
| Full abstraction | Y | Y | Y* | Y | Y* | Y | Y | N | Y | Y | N |
| Contract violation checks | Y | Y | N | N | N | N | N | N | N | N | N |
| Natural usage | Y | N | N | N | N | N | N | N | Y | N | N |
| Basic type is stored | Y | Y | Y | Y | Y | N | Y | N | Y | Y | Y? |
| Explicit instantiation | N | Y | Y | Y | Y | N | N | N | N | Y | N |
| prefixed function | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| Characteristics | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| memory error handling | except | abort, except | retcode | retcode | none | retcode | retcode | retcode | none | retcode | retcode |
| destructors on exception | Y | Y* | N | N | N | N | N | N | N | N | N |
| custom memory support | Y | Y | Y | Y | N | Y | Y | N | Y | Y | Y |
| context for memory support | N | Y | Y | N | N | N | N | N | Y | N | Y |
| Forward declaration support | N | Y* | Y | N | N | N | N | N | N | N | N |
| Serialization | N | JSON | N | N | N | N | N | N | N | N | N |
- C11*: means C11 + typeof extension
- C99*: means C99 + typeof extension
- Y*: Yes with some limitations.
- N*: even it appears to be type safe, it is not and it is easy to misuse it.
- NA: the question has no meaning for this library.
| Containers | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Singly Linked Non-Intrusive list | Y | Y | N | N | Y | Y | N | Y | N | Y | Y |
| Doubly Linked Non-Intrusive list | Y | N | N | N | Y | Y | Y | Y | N | N | N |
| Singly Linked, Dually Push Non-Intrusive list | N | Y | Y | N | N | N | N | N | N | N | N |
| Singly Linked Intrusive list | N | N | N | N | N | N | N | N | N | N | Y |
| Doubly Linked Intrusive list | N | Y | N | N | N | N | N | N | N | N | Y |
| Dynamic array | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
| Static array | Y | N | N | N | Y | N | N | N | N | N | Y |
| Containers | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| pair | Y | Y | N | N | N | N | N | N | N | N | N |
| tuple | Y | Y | N | N | N | N | N | N | N | N | N |
| optional | Y | Y | N | N | N | N | N | N | N | N | N |
| variant | Y | Y | N | N | N | N | N | Y | N | N | N |
| bitset | Y | Y | Y | Y | N | N | N | N | N | N | Y |
| Dynamic character string | Y | Y | Y | N | Y | N | N | Y | N | Y | N |
| string_view | Y | N | Y | N | N | N | N | N | N | N | N |
| deque | Y | Y | Y | Y | Y | Y | N | Y | N | N | Y |
| queue | Y | Y | Y | Y | Y | Y | N | Y | N | N | N |
| priority queue | Y | Y | Y | Y | Y | Y | N | N | N | N | Y |
| stack | Y | Y | Y | N | Y | Y | N | N | N | N | N |
| Bounded Queue | N | Y | N | N | N | N | N | Y | N | N | Y |
| Containers | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| set | Y | Y | Y | N | N | Y | N | Y | N | Y | Y |
| multiset | Y | Y | N | N | N | N | N | N | N | N | N |
| map | Y | Y | N | N | Y | Y | N | N | N | Y | N |
| multimap | Y | Y | N | N | N | N | N | N | N | N | N |
| unordered_set | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | Y |
| unordered_multiset | Y | N | N | Y | N | N | N | N | N | N | N |
| unordered_map | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | N |
| unordered_multimap | Y | N | N | Y | N | N | N | Y | N | N | N |
| flat_set | Y | N | N | Y | N | N | N | N | N | N | N |
| flat_multiset | Y | N | N | Y | N | N | N | N | N | N | N |
| flat_map | Y | N | N | Y | N | N | N | N | N | N | Y |
| flat_multimap | Y | N | N | Y | N | N | N | N | N | N | N |
| Containers | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| unique_ptr | Y | N | Y | N | N | N | N | N | N | N | N |
| shared_ptr | Y | Y | Y | N | N | N | N | N | N | N | N |
| advanced shared_ptr | N | Y | N | N | N | N | N | N | N | N | N |
| weak_ptr | Y | N | N | N | N | N | N | N | N | N | N |
| Function Object | Y | Y | N | N | N | N | N | N | N | N | N |
| Span | Y | N | Y | N | N | N | N | N | N | N | N |
| MDSpan | Y | N | Y | N | N | N | N | N | N | N | N |
| Bounded String | N | Y | N | N | N | N | N | N | N | N | N |
| Containers | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Atomic Shared Register SPSC | N | Y | N | N | N | N | N | N | N | N | N |
| Atomic Shared Register MPSC | N | Y | N | N | N | N | N | N | N | N | N |
| Atomic Shared Register SPMC | N | Y | N | N | N | N | N | N | N | N | N |
| Atomic Shared Register MPMC | N | Y | N | N | N | N | N | N | N | N | N |
| Skip List | N | N | N | Y | N | N | N | N | N | N | N |
| Sorted Bidirectional Map | N | N | N | Y | N | N | N | N | N | N | N |
| Tree | N | Y | N | N | N | N | N | N | N | N | N |
| Algorithms | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Sort/Min/Max/... | Y | Y | Y | N | Y | Y | Y | Y | N | Y | Y |
If you see any mistakes in this report, or want to include another C library, or want to include another point of comparison, do not hesitate to open a pull request.
Ergonomic comparison
Rules
The test programs shall respect the following conditions:
- it shall use a basic type (int), a non POD type (the type mpz_t of the GMP library) and a string as the primary type of the container.
- if a dynamic string container exists in the container library, it shall be used instead of a C string,
- it shall not comment the code (the code is assumed to be clear on what it does by itself) except if there is some workaround,
- it shall not produce any compilation warnings,
- it shall execute properly,
- it shall not leak any memory,
- it shall abort on error,
- it shall link dynamically with the GMP library (https://gmplib.org/) if needed,
- it shall link statically with the container library if needed,
- the optional assertions shall be turned off.
A workaround is defined as a way to implement this program which is not natural for the library. This typically includes:
- create wrapper structure,
- create wrapper functions or macros,
- accessing internal fields of the containers (typically for using the qsort function).
For example, if a container library manual requests to define some macro for its use, then it won't be considered as a workaround. Workarounds are allowed but are counted separately.
Array tests
The program shall perform the following operations:
- declare a dynamic array of int (resp. mpz_t, a string),
- initialize this array with the small unsigned integers values 17, 42 and 9 (performing a conversion from unsigned integer to mpz_t for GMP) or the constant strings "Hello", "World" and "!" for strings,
- sort this array,
- iterate the array to print the values.
Associative array tests
The program shall perform the following operations:
- declare a non-ordered associative array from int (resp. mpz_t, a string) to int (resp. mpz_t, a string),
- initialize this array with the association of signed integers values 17 to 4585, 42 to 4856 and -9 to 1452 (performing a conversion from signed integer to mpz_t for GMP) or the strings "Hello" to "LIB", "Welcome" to "Program" and "Sincerely" to "Your map" for strings,
- search for the key "Hello" and display it if successful,
- iterate the associative array to print the values.
Execution
The different programs are available in this repository. To build them, you just need to have a working C11 compiler, a make tool, git, the GMP library, and the GLIB library.
Simply run "make" to perform clones of the C libraries and generate the different executables.
Conclusion
What can be objectively compared is the size of the programs:
| Array-Int programs | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| number of characters | 236 | 373 | 558 | 1011 | 593 | 885 | 611 | 696 | 817 | 783 | 1395 |
| number of line of codes | 13 | 18 | 34 | 46 | 25 | 46 | 34 | 38 | 43 | 28 | 55 |
| number of workarounds | 0 | 0 | 0 | 2 | 2 | 2 | 0 | 0 | 1 | 2 | 0 |
| Array-Str programs | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| number of characters | 274 | 442 | 564 | 1053 | 762 | 839 | 651 | 908 | 881 | 1497 | 1478 |
| number of line of codes | 14 | 19 | 36 | 45 | 29 | 47 | 33 | 44 | 45 | 55 | 54 |
| number of workarounds | 0 | 0 | 0 | 2 | 3 | 1 | 0 | 0 | 1 | 3 | 0 |
| Array-mpz programs | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| number of characters | 261 | 505 | 1222 | 1740 | 1407 | 1337 | 1120 | 840 | 1255 | 1041 | 1585 |
| number of line of codes | 14 | 20 | 44 | 65 | 42 | 68 | 49 | 47 | 61 | 43 | 61 |
| number of workarounds | 0 | 0 | 3 | 7 | 5 | 1 | 2 | 0 | 4 | 5 | 0 |
| UMap-Int programs | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| number of characters | 359 | 462 | 777 | 1640 | 1090 | 1241 | 442 | 984 | 1035 | 774 | 2140 |
| number of line of codes | 15 | 19 | 37 | 62 | 47 | 54 | 26 | 42 | 50 | 43 | 85 |
| number of workarounds | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 3 | 0 | 0 |
| UMap-Str programs | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| number of characters | 436 | 615 | 870 | 1627 | 1756 | 929 | 522 | 1514 | 991 | 837 | 2350 |
| number of line of codes | 16 | 20 | 37 | 63 | 64 | 35 | 26 | 58 | 48 | 43 | 93 |
| number of workarounds | 0 | 0 | 0 | 1 | 4 | 0 | 0 | 0 | 2 | 0 | 0 |
| UMap-mpz programs | STL | M*LIB | STC | CMC | CTL | CollecC | CC | GLIB | STB_DS | KLIB | CCC |
|---|---|---|---|---|---|---|---|---|---|---|---|
| number of characters | 797 | 1018 | 1849 | 2387 | 1964 | 1893 | 1443 | 1667 | NA | 1754 | 3137 |
| number of line of codes | 38 | 39 | 55 | 99 | 79 | 87 | 62 | 75 | NA | 84 | 132 |
| number of workarounds | 0 | 0 | 4 | 2 | 4 | 0 | 2 | 0 | NA | 3 | 1 |
As ergonomic is a personal judgement, no conclusion will be provided. You should open the different provided programs and make your own choice based on your own ergonomic criteria:
- Readability
- Debuggability
- Maintainability
However, we can still conclude that even the best C library in this domain is more verbose than the C++.
Performance Comparison
The bench directory contains a benchmark comparing the performance of different C libraries (including some C++ ones, like STL and BOOST as references).
Time and memory usage are provided for these tests and the best run out of 3 is kept to remove external interference, which is a compromise between execution time and reliability.
Compiler flags are -O2 -march=native.
More specialized C libraries are added. The tested C libraries are:
- Bstrlib (for string)
- CC
- CCC
- CMC
- Collections C
- CTL
- GLIB
- KLIB
- liblfds (for thread communication)
- libsrt
- M*LIB
- Pottery
- Qlibc
- SDS (for string)
- STC
- TOMMY DS
- UT HASH (for hash table)
- VERSTABLE (for hash table)
- XXHASH (for hash function)
Rather than measuring the performance of each individual methods exported by the library on some dataset, it measures the time taken by some test programs implementing a defined algorithm using the methods of the containers for this (to provide more real world examples). Of course, it doesn't mean that these algorithms match with your use cases, so you should take them with a grain of salt.
Each dataset size is chosen so that the time using by the best library is around 1 second (which is a compromise between execution time and reliability of the test result).
For the hash tables, the load factor is let at the default defined by the library. Instead we measure and rank the memory consumption during the test, the ones using a low load factor being necessarily at a disadvantageous.
The performance programs are performed around the following functionalities:
- sequence container (array, list and deque) where two containers are growing at the same time (on 64 bits type),
- sorted set container,
- unordered map container (on unsigned 64 bits type, on 256 bits type and on string type) with a 50% found/un-found ratio,
- unordered set container (on 32 bits type) with a low found/un-found ratio,
- string concat,
- string replacement,
- sort algorithm,
- hash function,
- multithread communication queue container.
Exact program exact behaviors, code source and dataset size are provided in the bench directory for further analysis.
Conclusion
Results are available for i5-3210M and for AMD EPYC 7763 (the latter is generated by CI).
The results are archived in git so that you can look at the history of the different runs. Thanks to that, the best rank and worst rank of the 10 previous runs are extracted: this enables detecting external interference during the run when the ranking is not stable. For example, the sequence container bench is also dependent on the performance of the kernel to allocate pages, and the top 5 libraries are challenger for the first place depending on how the kernel serves them during the bench. Extracting the best/worst ranks enables to have a better view of the relative performance of the libraries.
The conclusion is that the best C libraries can be much faster than the STL. Such libraries are all based on template-header or template-macros paradigm. This is due to the over specification of the C++ standard which prevents the STL to achieve good performances in all cases. Even for C++, more specialized C++ libraries (like boost) are needed to achieve good performance.
Continuous Integration
This project includes all sources used for this comparison and provides continuous integration to:
- perform the run of the benchmark,
- automated validation
- regression detection