Krystian's September Update
Sep 29, 2020Reviewing the review
The review period for Boost.JSON has come and gone, and we got some great feedback on the design of the library. Glancing over the results, it appears that the general mood was to accept the library. This doesn’t mean that there weren’t any problem areas – most notably the documentation, which often did contain the information people wanted, but it was difficult to find.
Other points of contention were the use of a push parser as opposed to a pull parser, the use of double
, uint64_t
, and int64_t
without allowing for users to change them, and the value conversion interface. Overall some very good points were made, and I’d like to thank everyone for participating in the review.
Customizing the build
I put a bit of work into improving our CI matrix, as it had several redundant configurations and did not test newer compiler versions (e.g. GCC 10, clang 11), nor did we have any 32-bit jobs. The most difficult thing about working on the build matrix is balancing how exhaustive it is with the turnaround time – sure, we could add 60 configurations that test x86, x86-64, and ARM on every major compiler version released since 2011, but the turnaround would be abysmal.
To alleviate this, I only added 32-bit jobs for the sanitizers that use a recent version of GCC. It’s a less common configuration in the days of 64-bit universality, and if 64 bit works then it’s highly likely that 32 bit will “just work” as well.
Here’s a table of the new Travis configurations that will be added:
Compiler | Library | C++ Standard | Variant | OS | Architecture | Job |
---|---|---|---|---|---|---|
— | — | — | Boost | Linux (Xenial) | x86-64 | Documentation |
gcc 8.4.0 | libstdc++ | 11 | Boost | Linux (Xenial) | x86-64 | Coverage |
clang 6.0.1 | libstdc++ | 11, 14 | Boost | Linux (Xenial) | x86-64 | Valgrind |
clang 11.0.0 | libstdc++ | 17 | Boost | Linux (Xenial) | x86-64 | Address Sanitizer |
clang 11.0.0 | libstdc++ | 17 | Boost | Linux (Xenial) | x86-64 | UB Sanitizer |
msvc 14.1 | MS STL | 11, 14, 17 | Boost | Windows | x86-64 | — |
msvc 14.1 | MS STL | 17, 2a | Standalone | Windows | x86-64 | — |
msvc 14.2 | MS STL | 17, 2a | Boost | Windows | x86-64 | — |
msvc 14.2 | MS STL | 17, 2a | Standalone | Windows | x86-64 | — |
icc 2021.1 | libstdc++ | 11, 14, 17 | Boost | Linux (Bionic) | x86-64 | — |
gcc 4.8.5 | libstdc++ | 11 | Boost | Linux (Trusty) | x86-64 | — |
gcc 4.9.4 | libstdc++ | 11 | Boost | Linux (Trusty) | x86-64 | — |
gcc 5.5.0 | libstdc++ | 11 | Boost | Linux (Xenial) | x86-64 | — |
gcc 6.5.0 | libstdc++ | 11, 14 | Boost | Linux (Xenial) | x86-64 | — |
gcc 7.5.0 | libstdc++ | 14, 17 | Boost | Linux (Xenial) | x86-64 | — |
gcc 8.4.0 | libstdc++ | 17, 2a | Boost | Linux (Xenial) | x86-64 | — |
gcc 9.3.0 | libstdc++ | 17, 2a | Boost | Linux (Xenial) | x86-64 | — |
gcc 9.3.0 | libstdc++ | 17, 2a | Standalone | Linux (Xenial) | x86-64 | — |
gcc 10.2.0 | libstdc++ | 17, 2a | Boost | Linux (Focal) | x86-64 | — |
gcc 10.2.0 | libstdc++ | 17, 2a | Standalone | Linux (Focal) | x86-64 | — |
gcc (trunk) | libstdc++ | 17, 2a | Boost | Linux (Focal) | x86-64 | — |
gcc (trunk) | libstdc++ | 17, 2a | Standalone | Linux (Focal) | x86-64 | — |
clang 3.8.0 | libstdc++ | 11 | Boost | Linux (Trusty) | x86-64 | — |
clang 4.0.0 | libstdc++ | 11, 14 | Boost | Linux (Xenial) | x86-64 | — |
clang 5.0.2 | libstdc++ | 11, 14 | Boost | Linux (Xenial) | x86-64 | — |
clang 6.0.1 | libstdc++ | 14, 17 | Boost | Linux (Xenial) | x86-64 | — |
clang 7.0.1 | libstdc++ | 17, 2a | Boost | Linux (Xenial) | x86-64 | — |
clang 9.0.1 | libstdc++ | 17, 2a | Boost | Linux (Xenial) | x86-64 | — |
clang 9.0.1 | libstdc++ | 17, 2a | Standalone | Linux (Xenial) | x86-64 | — |
clang 10.0.1 | libstdc++ | 17, 2a | Boost | Linux (Xenial) | x86-64 | — |
clang 10.0.1 | libstdc++ | 17, 2a | Standalone | Linux (Xenial) | x86-64 | — |
clang 11.0.0 | libstdc++ | 17, 2a | Boost | Linux (Xenial) | x86-64 | — |
clang 11.0.0 | libstdc++ | 17, 2a | Standalone | Linux (Xenial) | x86-64 | — |
clang (trunk) | libstdc++ | 17, 2a | Boost | Linux (Xenial) | x86-64 | — |
clang (trunk) | libstdc++ | 17, 2a | Standalone | Linux (Xenial) | x86-64 | — |
I think it strikes a good balance between exhaustiveness and turnaround time, and we now test the most recent compiler versions to make sure they won’t cause problems on the cutting edge.
Binary size
It doesn’t matter how good a library is if it’s too big to use within your environment. As with all things in computer science, there is a trade-off between size and speed; seldom can you have both. We have been exploring options to reduce the size of the binary, and this mostly involved removing a lot of the pre-written tables we have (such as the ever-controversial jump table), since it allows the compiler to take into account the specific options it was past and optimize for those constraints (i.e. size and speed) rather than hard-coding in a set configuration as we did with the jump tables.
Peter Dimov also helped out by transitioning our compile-time system of generating unique parse functions for each permutation of extensions to a runtime system, which drastically decreases the binary size without affecting performance too much.
I must admit I’m not the biggest fan of these changes, but it’s important to support the use of Boost.JSON in embedded environments. As Peter has said time and time again: don’t overfit for a particular use-case or configuration.
Another place with room for improvement is with string to float-point conversions. Right now we calculate a mantissa and base-10 exponent, then lookup the value in a massive table that contains pre-calculated powers of 10 from 1e-308 to 1e+308. As you can surmise, this takes up a substantial amount of space (8 bytes * 618 elements = 4.95 kb).
Here is a boiled down version of how we currently perform the conversion:
double calculate_float(
std::uint64_t mantissa,
std::uint32_t exponent,
bool sign)
{
constexpr static double table[618] =
{
1e-308, 1e-307,
...,
1e307, 1e308
};
double power;
if(exponent < -308 || exponent > 308)
power = std::pow(10.0, exponent);
else
power = table[exponent + 308]
double result = mantissa * power;
return sign ? -result : result;
}
To further reduce the size of the binary, Peter suggested that we instead calculate power
as 10^floor(exponent / 8) * 10^(exponent mod 8)
. Yes, the division operations there might look expensive, but any decent optimizing compiler will transform exponent / 8
to exponent >> 3
, and exponent mod 8
to exponent & 7
. This does introduce another multiplication instruction, but at the same time, it makes our table 8 times smaller. In theory, the slight drop in performance is worth the significant reduction in binary size.
All Posts by This Author
- 01/12/2024 Krystian's Q4 Update
- 10/31/2023 Krystian's Q3 Update
- 09/29/2020 Krystian's September Update
- 09/06/2020 Krystian's August Update
- 08/01/2020 Krystian's July Update
- 07/01/2020 Krystian's May & June Update
- 05/08/2020 Krystian's April Update
- 04/07/2020 Krystian's March Update
- 03/06/2020 Krystian's February Update
- View All Posts...