mirror of
https://github.com/nlohmann/json.git
synced 2026-10-31 18:05:44 +00:00
json_view: skip the digits of a fraction 16 bytes at a time (SSE2)
The digits after the decimal point were skipped by skip_digits(), 16 compare-and-branch steps in a row: about 3.5 instructions per digit, and an exit branch that is mispredicted when the lengths of the fractions vary. On x86 with SSE2, the fraction is now skipped by skip_fraction_digits(): 16 bytes are compared at once and the end of the run is taken from the bit mask, which is about a third of the instructions for a fraction of 12 digits. The last bytes of the input are skipped one at a time as before. Where there is no SSE2 (AArch64, other targets, JSON_NO_SIMD) skip_fraction_digits() is skip_digits(): nothing changes there. Integer digits keep skip_digits(), whose exit is predictable. GCC unrolls the loop over the last bytes in every inlined copy of the new function, which made the parse loop 4 KiB larger and parse() of documents with hardly any fractions about 4 % slower. For GCC that loop is therefore out of line (skip_digits_tail()); Clang is faster with it inline. json_document::parse(), A/B in one binary, 6 code layouts, x86-64 Haswell, time after / before (GCC 16.2 / Clang 23): - numbers.json (10,001 floats): 0.715 / 0.680 - canada.json: 0.778 / 0.790 - mesh.json: 0.908 / 0.883 - marine_ik.json: 0.937 / 0.926 - mesh.pretty.json: 0.923 / 0.960 Files with few or no fractions and files with fractions of a fixed length (20,000 prices with two decimals, 60,000 coordinates with six): between 0.98 and 1.014 on both compilers, except citm_catalog.json with GCC (1.05: the instruction count is +0.3 %, and that row varies by +-7 % between builds of unrelated variants on this machine). New test: fractions of every length from 0 to 40 digits followed by every kind of byte that ends a number or lies next to the digits in ASCII, in several positions, and cut in every place near the end of the text, against json::parse() (the length of the number is part of what is compared), for all option combinations and with and without a terminating NUL. Checked on x86-64 with GCC 16.2, Clang 23 and Apple clang under Rosetta, and with a deliberate off-by-one at the 16th byte. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
4 files changed
+145
-2
No files matched your search
Generated
+52
-1
@@ -1218,6 +1218,57 @@ inline const unsigned char* find_block_comment_end(const unsigned char* p, const
|
||||
return p;
|
||||
}
|
||||
|
||||
/// The digits of a fraction, whose number varies from one number to the next: skip_digits() with 16 bytes compared at
|
||||
/// once and the end of the run taken from a bit mask (SSE2), instead of a compare and a branch for each digit. That
|
||||
/// is about a third of the instructions for a fraction of 12 digits, and no branch that is mispredicted when the
|
||||
/// lengths vary. Where there is no SSE2, skip_digits() is used.
|
||||
#if NLOHMANN_VIEW_SSE2
|
||||
// The last bytes of the input, one at a time. GCC unrolls this loop in every inlined copy of the function below, which
|
||||
// makes the parse loop 4 KiB larger and slows down documents that have hardly any fractions; out of line, it does
|
||||
// not. Clang does better with the loop inline (a call in the parse loop costs it more than the unrolling).
|
||||
#if defined(__GNUC__) && !defined(__clang__)
|
||||
inline NLOHMANN_VIEW_NOINLINE const unsigned char* skip_digits_tail(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
#else
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_digits_tail(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (p != e && is_digit(*p))
|
||||
{
|
||||
++p;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
#endif
|
||||
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_fraction_digits(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
while (e - p >= 16)
|
||||
{
|
||||
const __m128i in = _mm_loadu_si128(static_cast<const __m128i*>(static_cast<const void*>(p))); // NOLINT(bugprone-casting-through-void)
|
||||
// a digit is above '/' and below ':' (signed compares: a byte with the top bit set is negative, and neither)
|
||||
const __m128i digits = _mm_and_si128(_mm_cmpgt_epi8(in, _mm_set1_epi8('/')), _mm_cmplt_epi8(in, _mm_set1_epi8(':')));
|
||||
const auto not_digit = static_cast<std::uint64_t>(~static_cast<unsigned>(_mm_movemask_epi8(digits)) & 0xFFFFu);
|
||||
if (not_digit != 0)
|
||||
{
|
||||
return p + count_trailing_zeros(not_digit);
|
||||
}
|
||||
p += 16;
|
||||
}
|
||||
return skip_digits_tail(p, e);
|
||||
}
|
||||
#else
|
||||
NLOHMANN_VIEW_ALWAYS_INLINE const unsigned char* skip_fraction_digits(const unsigned char* p, const unsigned char* e) noexcept
|
||||
{
|
||||
return skip_digits(p, e);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// powers of ten up to 10^19 as integers
|
||||
inline std::uint64_t int_pow10(unsigned k) noexcept
|
||||
{
|
||||
@@ -2324,7 +2375,7 @@ indent_done:
|
||||
{
|
||||
++p;
|
||||
const unsigned char* const f0 = p;
|
||||
p = skip_digits(p, e);
|
||||
p = skip_fraction_digits(p, e);
|
||||
if (NLOHMANN_VIEW_UNLIKELY(p == f0))
|
||||
{
|
||||
return fail(error_code::number_after_dot);
|
||||
|
||||
Reference in new issue
Block a user