mirror of
https://github.com/nlohmann/json.git
synced 2026-10-15 10:47:13 +00:00
Write CBOR integers without branching on their sign or size
The sizes of a run of integers are hard to predict, and so is the type of each element in an array that mixes negative (number_integer) and non-negative (number_unsigned) values. Writing an integer branched on the type, the sign, and the 1/2/4/8-byte size class, and the mispredictions were the larger part of the time left on integer arrays. - number_integer and number_unsigned share one case of write_cbor; the major type and the argument are selected with a sign mask. - An integer's head is assembled without branches: the size class comes from a table indexed by count_leading_zeros(), and the first byte and the argument in network order are stored as one byte and one 8-byte word, of which only the head's length is kept. Heads of strings and containers keep the branching write_cbor_head(), as their lengths are mostly predictable. - Arrays are written by write_cbor_array(), kept out of line so that the registers its loop needs are not saved on every call of write_cbor. Runs of integers are encoded straight into space reserved from the sink (the new reserve()/commit() of both sinks), so the write pointer stays in a register instead of being reloaded for every value. The output is unchanged; the new test compares integer arrays at every head-size boundary, in runs shorter and longer than one reservation and broken up by other values, with an independent encoder through each sink. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
4 files changed
+499
-50
No files matched your search
@@ -23,6 +23,7 @@
|
|||||||
#include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64
|
#include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include <nlohmann/detail/bit_ops.hpp>
|
||||||
#include <nlohmann/detail/input/binary_reader.hpp>
|
#include <nlohmann/detail/input/binary_reader.hpp>
|
||||||
#include <nlohmann/detail/input/string_scan.hpp>
|
#include <nlohmann/detail/input/string_scan.hpp>
|
||||||
#include <nlohmann/detail/macro_scope.hpp>
|
#include <nlohmann/detail/macro_scope.hpp>
|
||||||
@@ -204,24 +205,12 @@ class binary_writer
|
|||||||
}
|
}
|
||||||
|
|
||||||
case value_t::number_integer:
|
case value_t::number_integer:
|
||||||
{
|
|
||||||
if (j.m_data.m_value.number_integer >= 0)
|
|
||||||
{
|
|
||||||
// CBOR does not differentiate between positive signed
|
|
||||||
// integers and unsigned integers
|
|
||||||
write_cbor_head(0x00, static_cast<std::uint64_t>(j.m_data.m_value.number_integer));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// a negative integer n is encoded as -1 - n
|
|
||||||
write_cbor_head(0x20, static_cast<std::uint64_t>(-1 - j.m_data.m_value.number_integer));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case value_t::number_unsigned:
|
case value_t::number_unsigned:
|
||||||
{
|
{
|
||||||
write_cbor_head(0x00, j.m_data.m_value.number_unsigned);
|
// one case for both, so that an array mixing negative
|
||||||
|
// (number_integer) and non-negative (number_unsigned) values
|
||||||
|
// does not mispredict the switch
|
||||||
|
write_cbor_integer(j);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,13 +245,7 @@ class binary_writer
|
|||||||
|
|
||||||
case value_t::array:
|
case value_t::array:
|
||||||
{
|
{
|
||||||
// step 1: write control byte and the array size
|
write_cbor_array(j, depth);
|
||||||
write_cbor_head(0x80, j.m_data.m_value.array->size());
|
|
||||||
|
|
||||||
for (const auto& el : *j.m_data.m_value.array)
|
|
||||||
{
|
|
||||||
write_cbor(el, depth + 1);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1951,6 +1934,127 @@ class binary_writer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief encode the head of a CBOR integer to @a out without branching on its
|
||||||
|
size
|
||||||
|
@return the number of characters of the head; up to @ref cbor_integer_head_space
|
||||||
|
characters at @a out are overwritten
|
||||||
|
|
||||||
|
Same output as @ref write_cbor_head. The lengths of strings and containers
|
||||||
|
are mostly predictable, so their heads keep the branches; the sizes of a
|
||||||
|
run of integers are not, so an integer's head is assembled without them.
|
||||||
|
*/
|
||||||
|
std::size_t encode_cbor_integer_head(CharType* out, const std::uint8_t major_type, const std::uint64_t argument) const noexcept
|
||||||
|
{
|
||||||
|
// The size class - 0 if the argument fits the first byte, else 1 to 4
|
||||||
|
// for 1, 2, 4, or 8 following bytes - is looked up from the number of
|
||||||
|
// leading zero bits instead of found by branches: the sizes of a stream
|
||||||
|
// of numbers are hard to predict, and a mispredicted branch costs more
|
||||||
|
// than writing the head.
|
||||||
|
static constexpr std::uint8_t size_class_by_leading_zeros[64] =
|
||||||
|
{
|
||||||
|
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||||
|
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||||
|
2, 2, 2, 2, 2, 2, 2, 2,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1
|
||||||
|
};
|
||||||
|
// an argument of at most 23 is in the table's class 1 (one byte) and is
|
||||||
|
// moved to class 0 by subtracting 1; argument | 1 keeps
|
||||||
|
// count_leading_zeros() away from 0
|
||||||
|
const auto is_immediate = static_cast<unsigned>(argument <= 0x17u);
|
||||||
|
const unsigned size_class = size_class_by_leading_zeros[count_leading_zeros(argument | 1u)] - is_immediate;
|
||||||
|
const std::size_t length = (std::size_t{1} << size_class) >> 1u;
|
||||||
|
|
||||||
|
// The first byte - the argument itself if it is immediate, else
|
||||||
|
// 0x17 + size_class - selected through a mask, then the length
|
||||||
|
// low-order bytes of the argument in network order, written as one
|
||||||
|
// 8-byte word whose bytes after the argument are scratch that
|
||||||
|
// the caller drops. For an immediate argument the word is all
|
||||||
|
// scratch; it is shifted as if length were 1, avoiding a shift by 64.
|
||||||
|
const std::uint64_t immediate_mask = std::uint64_t{0} - is_immediate;
|
||||||
|
out[0] = to_char_type(static_cast<std::uint8_t>(major_type + ((argument & immediate_mask) | ((0x17u + size_class) & ~immediate_mask))));
|
||||||
|
const unsigned unused_bits = 64u - 8u * static_cast<unsigned>(length + is_immediate);
|
||||||
|
const std::uint64_t tail = is_little_endian ? byte_swap(argument) >> unused_bits : argument << unused_bits;
|
||||||
|
std::memcpy(out + 1, &tail, sizeof(tail));
|
||||||
|
return length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief write an array value (head and elements) as CBOR
|
||||||
|
|
||||||
|
Integer elements, the most common scalar, are written without a call to
|
||||||
|
@ref write_cbor. The loop is kept out of line: inlined into write_cbor, its
|
||||||
|
registers would be saved and restored on every call of write_cbor, which
|
||||||
|
is one call per value nested in an object.
|
||||||
|
*/
|
||||||
|
JSON_HEDLEY_NEVER_INLINE
|
||||||
|
void write_cbor_array(const BasicJsonType& j, const std::size_t depth)
|
||||||
|
{
|
||||||
|
write_cbor_head(0x80, j.m_data.m_value.array->size());
|
||||||
|
|
||||||
|
// Runs of integers are encoded straight into space reserved from the
|
||||||
|
// sink for a whole run: the write pointer stays a local, where it would
|
||||||
|
// otherwise be reloaded from the sink for every value, as a store of a
|
||||||
|
// character may alias it.
|
||||||
|
constexpr std::size_t max_run = max_reserve_size / cbor_integer_head_space;
|
||||||
|
auto it = j.m_data.m_value.array->begin();
|
||||||
|
const auto end = j.m_data.m_value.array->end();
|
||||||
|
while (it != end)
|
||||||
|
{
|
||||||
|
if (it->is_number_integer())
|
||||||
|
{
|
||||||
|
CharType* const start = oa.reserve(max_run * cbor_integer_head_space);
|
||||||
|
CharType* out = start;
|
||||||
|
for (std::size_t n = 0; n < max_run && it != end && it->is_number_integer(); ++n, ++it)
|
||||||
|
{
|
||||||
|
out += encode_cbor_integer(out, *it);
|
||||||
|
}
|
||||||
|
oa.commit(static_cast<std::size_t>(out - start));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
write_cbor(*it, depth + 1);
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief write a number_integer or number_unsigned value as a CBOR integer
|
||||||
|
|
||||||
|
A negative integer n has major type 1 and the argument -1 - n, which is ~n.
|
||||||
|
The sign and the type select the major type and the argument through a
|
||||||
|
mask rather than a branch, as they alternate unpredictably in an array of
|
||||||
|
mixed signs.
|
||||||
|
*/
|
||||||
|
void write_cbor_integer(const BasicJsonType& j)
|
||||||
|
{
|
||||||
|
oa.commit(encode_cbor_integer(oa.reserve(cbor_integer_head_space), j));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// encode a number_integer or number_unsigned value to @a out (see
|
||||||
|
/// @ref write_cbor_integer) and return the number of characters
|
||||||
|
std::size_t encode_cbor_integer(CharType* out, const BasicJsonType& j) const noexcept
|
||||||
|
{
|
||||||
|
const bool is_signed = j.m_data.m_type == value_t::number_integer;
|
||||||
|
const std::uint64_t raw = is_signed
|
||||||
|
? static_cast<std::uint64_t>(j.m_data.m_value.number_integer)
|
||||||
|
: static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned);
|
||||||
|
// All ones for a negative integer, else zero. Up to 64 bits, the sign is
|
||||||
|
// bit 63 of raw (a narrower integer is sign-extended); computing it
|
||||||
|
// with masks keeps GCC from branching on is_signed. A wider integer
|
||||||
|
// has to be compared.
|
||||||
|
const std::uint64_t sign_mask = sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t)
|
||||||
|
? (std::uint64_t{0} - static_cast<std::uint64_t>(is_signed)) & (std::uint64_t{0} - (raw >> 63u))
|
||||||
|
: std::uint64_t{0} - static_cast<std::uint64_t>(is_signed && j.m_data.m_value.number_integer < 0);
|
||||||
|
// ~raw is -1 - n in the 64 bits that are written
|
||||||
|
return encode_cbor_integer_head(out, static_cast<std::uint8_t>(sign_mask & 0x20u), raw ^ sign_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// characters encode_cbor_integer_head() may overwrite: the first byte and
|
||||||
|
/// one 8-byte word
|
||||||
|
static constexpr std::size_t cbor_integer_head_space = 9;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@brief write an object key as a CBOR text string
|
@brief write an object key as a CBOR text string
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm> // max
|
#include <algorithm> // max
|
||||||
|
#include <array> // array
|
||||||
#include <cstddef> // size_t
|
#include <cstddef> // size_t
|
||||||
#include <cstring> // memcpy
|
#include <cstring> // memcpy
|
||||||
#include <memory> // shared_ptr, make_shared
|
#include <memory> // shared_ptr, make_shared
|
||||||
@@ -50,8 +51,8 @@ template<typename CharType> struct output_adapter_protocol
|
|||||||
template<typename CharType>
|
template<typename CharType>
|
||||||
using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>;
|
using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>;
|
||||||
|
|
||||||
/// output_vector_sink appends writes of at least this many characters with
|
/// the largest number of characters a writer may reserve() from a sink at once;
|
||||||
/// insert() instead of copying them to the space it keeps ahead
|
/// output_vector_sink also appends writes of at least this size with insert()
|
||||||
constexpr std::size_t max_reserve_size = 256;
|
constexpr std::size_t max_reserve_size = 256;
|
||||||
|
|
||||||
/// @brief non-virtual output sink writing into a std::vector
|
/// @brief non-virtual output sink writing into a std::vector
|
||||||
@@ -132,6 +133,31 @@ class output_vector_sink
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @brief space for writing up to @a length characters directly
|
||||||
|
///
|
||||||
|
/// Returns a pointer to at least @a length (at most @ref max_reserve_size)
|
||||||
|
/// writable characters at the write position; @ref commit then keeps the
|
||||||
|
/// first ones of them. A writer can store whole words there and keep only
|
||||||
|
/// the bytes it needs, and keep its write pointer in a register across many
|
||||||
|
/// values instead of updating the sink after each.
|
||||||
|
JSON_HEDLEY_ALWAYS_INLINE
|
||||||
|
CharType* reserve(const std::size_t length)
|
||||||
|
{
|
||||||
|
JSON_ASSERT(length <= max_reserve_size);
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
|
||||||
|
{
|
||||||
|
grow(length);
|
||||||
|
}
|
||||||
|
return v->data() + position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// keep the first @a length characters written to the space from reserve()
|
||||||
|
JSON_HEDLEY_ALWAYS_INLINE
|
||||||
|
void commit(const std::size_t length) noexcept
|
||||||
|
{
|
||||||
|
position += length;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// make room for at least @a length more characters after the position;
|
/// make room for at least @a length more characters after the position;
|
||||||
/// kept out of line so that the write functions stay small enough for
|
/// kept out of line so that the write functions stay small enough for
|
||||||
@@ -276,8 +302,24 @@ class output_adapter_sink
|
|||||||
oa->write_characters(s, length);
|
oa->write_characters(s, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// space for writing up to @a length characters (see
|
||||||
|
/// output_vector_sink::reserve)
|
||||||
|
CharType* reserve(const std::size_t length) noexcept
|
||||||
|
{
|
||||||
|
JSON_ASSERT(length <= max_reserve_size);
|
||||||
|
static_cast<void>(length);
|
||||||
|
return reserved.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// write the first @a length characters written to the space from reserve()
|
||||||
|
void commit(const std::size_t length)
|
||||||
|
{
|
||||||
|
oa->write_characters(reserved.data(), length);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
output_adapter_t<CharType> oa;
|
output_adapter_t<CharType> oa;
|
||||||
|
std::array<CharType, max_reserve_size> reserved{};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits
|
/// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits
|
||||||
|
|||||||
Generated
+172
-25
@@ -21619,6 +21619,8 @@ NLOHMANN_JSON_NAMESPACE_END
|
|||||||
#include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64
|
#include <cstdlib> // _byteswap_ushort, _byteswap_ulong, _byteswap_uint64
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// #include <nlohmann/detail/bit_ops.hpp>
|
||||||
|
|
||||||
// #include <nlohmann/detail/input/binary_reader.hpp>
|
// #include <nlohmann/detail/input/binary_reader.hpp>
|
||||||
|
|
||||||
// #include <nlohmann/detail/input/string_scan.hpp>
|
// #include <nlohmann/detail/input/string_scan.hpp>
|
||||||
@@ -21639,6 +21641,7 @@ NLOHMANN_JSON_NAMESPACE_END
|
|||||||
|
|
||||||
|
|
||||||
#include <algorithm> // max
|
#include <algorithm> // max
|
||||||
|
#include <array> // array
|
||||||
#include <cstddef> // size_t
|
#include <cstddef> // size_t
|
||||||
#include <cstring> // memcpy
|
#include <cstring> // memcpy
|
||||||
#include <memory> // shared_ptr, make_shared
|
#include <memory> // shared_ptr, make_shared
|
||||||
@@ -21681,8 +21684,8 @@ template<typename CharType> struct output_adapter_protocol
|
|||||||
template<typename CharType>
|
template<typename CharType>
|
||||||
using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>;
|
using output_adapter_t = std::shared_ptr<output_adapter_protocol<CharType>>;
|
||||||
|
|
||||||
/// output_vector_sink appends writes of at least this many characters with
|
/// the largest number of characters a writer may reserve() from a sink at once;
|
||||||
/// insert() instead of copying them to the space it keeps ahead
|
/// output_vector_sink also appends writes of at least this size with insert()
|
||||||
constexpr std::size_t max_reserve_size = 256;
|
constexpr std::size_t max_reserve_size = 256;
|
||||||
|
|
||||||
/// @brief non-virtual output sink writing into a std::vector
|
/// @brief non-virtual output sink writing into a std::vector
|
||||||
@@ -21763,6 +21766,31 @@ class output_vector_sink
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// @brief space for writing up to @a length characters directly
|
||||||
|
///
|
||||||
|
/// Returns a pointer to at least @a length (at most @ref max_reserve_size)
|
||||||
|
/// writable characters at the write position; @ref commit then keeps the
|
||||||
|
/// first ones of them. A writer can store whole words there and keep only
|
||||||
|
/// the bytes it needs, and keep its write pointer in a register across many
|
||||||
|
/// values instead of updating the sink after each.
|
||||||
|
JSON_HEDLEY_ALWAYS_INLINE
|
||||||
|
CharType* reserve(const std::size_t length)
|
||||||
|
{
|
||||||
|
JSON_ASSERT(length <= max_reserve_size);
|
||||||
|
if (JSON_HEDLEY_UNLIKELY(v->size() - position < length))
|
||||||
|
{
|
||||||
|
grow(length);
|
||||||
|
}
|
||||||
|
return v->data() + position;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// keep the first @a length characters written to the space from reserve()
|
||||||
|
JSON_HEDLEY_ALWAYS_INLINE
|
||||||
|
void commit(const std::size_t length) noexcept
|
||||||
|
{
|
||||||
|
position += length;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/// make room for at least @a length more characters after the position;
|
/// make room for at least @a length more characters after the position;
|
||||||
/// kept out of line so that the write functions stay small enough for
|
/// kept out of line so that the write functions stay small enough for
|
||||||
@@ -21907,8 +21935,24 @@ class output_adapter_sink
|
|||||||
oa->write_characters(s, length);
|
oa->write_characters(s, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// space for writing up to @a length characters (see
|
||||||
|
/// output_vector_sink::reserve)
|
||||||
|
CharType* reserve(const std::size_t length) noexcept
|
||||||
|
{
|
||||||
|
JSON_ASSERT(length <= max_reserve_size);
|
||||||
|
static_cast<void>(length);
|
||||||
|
return reserved.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// write the first @a length characters written to the space from reserve()
|
||||||
|
void commit(const std::size_t length)
|
||||||
|
{
|
||||||
|
oa->write_characters(reserved.data(), length);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
output_adapter_t<CharType> oa;
|
output_adapter_t<CharType> oa;
|
||||||
|
std::array<CharType, max_reserve_size> reserved{};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits
|
/// @brief whether std::basic_string<CharType> has a non-deprecated std::char_traits
|
||||||
@@ -22193,24 +22237,12 @@ class binary_writer
|
|||||||
}
|
}
|
||||||
|
|
||||||
case value_t::number_integer:
|
case value_t::number_integer:
|
||||||
{
|
|
||||||
if (j.m_data.m_value.number_integer >= 0)
|
|
||||||
{
|
|
||||||
// CBOR does not differentiate between positive signed
|
|
||||||
// integers and unsigned integers
|
|
||||||
write_cbor_head(0x00, static_cast<std::uint64_t>(j.m_data.m_value.number_integer));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// a negative integer n is encoded as -1 - n
|
|
||||||
write_cbor_head(0x20, static_cast<std::uint64_t>(-1 - j.m_data.m_value.number_integer));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case value_t::number_unsigned:
|
case value_t::number_unsigned:
|
||||||
{
|
{
|
||||||
write_cbor_head(0x00, j.m_data.m_value.number_unsigned);
|
// one case for both, so that an array mixing negative
|
||||||
|
// (number_integer) and non-negative (number_unsigned) values
|
||||||
|
// does not mispredict the switch
|
||||||
|
write_cbor_integer(j);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22245,13 +22277,7 @@ class binary_writer
|
|||||||
|
|
||||||
case value_t::array:
|
case value_t::array:
|
||||||
{
|
{
|
||||||
// step 1: write control byte and the array size
|
write_cbor_array(j, depth);
|
||||||
write_cbor_head(0x80, j.m_data.m_value.array->size());
|
|
||||||
|
|
||||||
for (const auto& el : *j.m_data.m_value.array)
|
|
||||||
{
|
|
||||||
write_cbor(el, depth + 1);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23940,6 +23966,127 @@ class binary_writer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief encode the head of a CBOR integer to @a out without branching on its
|
||||||
|
size
|
||||||
|
@return the number of characters of the head; up to @ref cbor_integer_head_space
|
||||||
|
characters at @a out are overwritten
|
||||||
|
|
||||||
|
Same output as @ref write_cbor_head. The lengths of strings and containers
|
||||||
|
are mostly predictable, so their heads keep the branches; the sizes of a
|
||||||
|
run of integers are not, so an integer's head is assembled without them.
|
||||||
|
*/
|
||||||
|
std::size_t encode_cbor_integer_head(CharType* out, const std::uint8_t major_type, const std::uint64_t argument) const noexcept
|
||||||
|
{
|
||||||
|
// The size class - 0 if the argument fits the first byte, else 1 to 4
|
||||||
|
// for 1, 2, 4, or 8 following bytes - is looked up from the number of
|
||||||
|
// leading zero bits instead of found by branches: the sizes of a stream
|
||||||
|
// of numbers are hard to predict, and a mispredicted branch costs more
|
||||||
|
// than writing the head.
|
||||||
|
static constexpr std::uint8_t size_class_by_leading_zeros[64] =
|
||||||
|
{
|
||||||
|
4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
|
||||||
|
3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
|
||||||
|
2, 2, 2, 2, 2, 2, 2, 2,
|
||||||
|
1, 1, 1, 1, 1, 1, 1, 1
|
||||||
|
};
|
||||||
|
// an argument of at most 23 is in the table's class 1 (one byte) and is
|
||||||
|
// moved to class 0 by subtracting 1; argument | 1 keeps
|
||||||
|
// count_leading_zeros() away from 0
|
||||||
|
const auto is_immediate = static_cast<unsigned>(argument <= 0x17u);
|
||||||
|
const unsigned size_class = size_class_by_leading_zeros[count_leading_zeros(argument | 1u)] - is_immediate;
|
||||||
|
const std::size_t length = (std::size_t{1} << size_class) >> 1u;
|
||||||
|
|
||||||
|
// The first byte - the argument itself if it is immediate, else
|
||||||
|
// 0x17 + size_class - selected through a mask, then the length
|
||||||
|
// low-order bytes of the argument in network order, written as one
|
||||||
|
// 8-byte word whose bytes after the argument are scratch that
|
||||||
|
// the caller drops. For an immediate argument the word is all
|
||||||
|
// scratch; it is shifted as if length were 1, avoiding a shift by 64.
|
||||||
|
const std::uint64_t immediate_mask = std::uint64_t{0} - is_immediate;
|
||||||
|
out[0] = to_char_type(static_cast<std::uint8_t>(major_type + ((argument & immediate_mask) | ((0x17u + size_class) & ~immediate_mask))));
|
||||||
|
const unsigned unused_bits = 64u - 8u * static_cast<unsigned>(length + is_immediate);
|
||||||
|
const std::uint64_t tail = is_little_endian ? byte_swap(argument) >> unused_bits : argument << unused_bits;
|
||||||
|
std::memcpy(out + 1, &tail, sizeof(tail));
|
||||||
|
return length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief write an array value (head and elements) as CBOR
|
||||||
|
|
||||||
|
Integer elements, the most common scalar, are written without a call to
|
||||||
|
@ref write_cbor. The loop is kept out of line: inlined into write_cbor, its
|
||||||
|
registers would be saved and restored on every call of write_cbor, which
|
||||||
|
is one call per value nested in an object.
|
||||||
|
*/
|
||||||
|
JSON_HEDLEY_NEVER_INLINE
|
||||||
|
void write_cbor_array(const BasicJsonType& j, const std::size_t depth)
|
||||||
|
{
|
||||||
|
write_cbor_head(0x80, j.m_data.m_value.array->size());
|
||||||
|
|
||||||
|
// Runs of integers are encoded straight into space reserved from the
|
||||||
|
// sink for a whole run: the write pointer stays a local, where it would
|
||||||
|
// otherwise be reloaded from the sink for every value, as a store of a
|
||||||
|
// character may alias it.
|
||||||
|
constexpr std::size_t max_run = max_reserve_size / cbor_integer_head_space;
|
||||||
|
auto it = j.m_data.m_value.array->begin();
|
||||||
|
const auto end = j.m_data.m_value.array->end();
|
||||||
|
while (it != end)
|
||||||
|
{
|
||||||
|
if (it->is_number_integer())
|
||||||
|
{
|
||||||
|
CharType* const start = oa.reserve(max_run * cbor_integer_head_space);
|
||||||
|
CharType* out = start;
|
||||||
|
for (std::size_t n = 0; n < max_run && it != end && it->is_number_integer(); ++n, ++it)
|
||||||
|
{
|
||||||
|
out += encode_cbor_integer(out, *it);
|
||||||
|
}
|
||||||
|
oa.commit(static_cast<std::size_t>(out - start));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
write_cbor(*it, depth + 1);
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@brief write a number_integer or number_unsigned value as a CBOR integer
|
||||||
|
|
||||||
|
A negative integer n has major type 1 and the argument -1 - n, which is ~n.
|
||||||
|
The sign and the type select the major type and the argument through a
|
||||||
|
mask rather than a branch, as they alternate unpredictably in an array of
|
||||||
|
mixed signs.
|
||||||
|
*/
|
||||||
|
void write_cbor_integer(const BasicJsonType& j)
|
||||||
|
{
|
||||||
|
oa.commit(encode_cbor_integer(oa.reserve(cbor_integer_head_space), j));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// encode a number_integer or number_unsigned value to @a out (see
|
||||||
|
/// @ref write_cbor_integer) and return the number of characters
|
||||||
|
std::size_t encode_cbor_integer(CharType* out, const BasicJsonType& j) const noexcept
|
||||||
|
{
|
||||||
|
const bool is_signed = j.m_data.m_type == value_t::number_integer;
|
||||||
|
const std::uint64_t raw = is_signed
|
||||||
|
? static_cast<std::uint64_t>(j.m_data.m_value.number_integer)
|
||||||
|
: static_cast<std::uint64_t>(j.m_data.m_value.number_unsigned);
|
||||||
|
// All ones for a negative integer, else zero. Up to 64 bits, the sign is
|
||||||
|
// bit 63 of raw (a narrower integer is sign-extended); computing it
|
||||||
|
// with masks keeps GCC from branching on is_signed. A wider integer
|
||||||
|
// has to be compared.
|
||||||
|
const std::uint64_t sign_mask = sizeof(typename BasicJsonType::number_integer_t) <= sizeof(std::uint64_t)
|
||||||
|
? (std::uint64_t{0} - static_cast<std::uint64_t>(is_signed)) & (std::uint64_t{0} - (raw >> 63u))
|
||||||
|
: std::uint64_t{0} - static_cast<std::uint64_t>(is_signed && j.m_data.m_value.number_integer < 0);
|
||||||
|
// ~raw is -1 - n in the 64 bits that are written
|
||||||
|
return encode_cbor_integer_head(out, static_cast<std::uint8_t>(sign_mask & 0x20u), raw ^ sign_mask);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// characters encode_cbor_integer_head() may overwrite: the first byte and
|
||||||
|
/// one 8-byte word
|
||||||
|
static constexpr std::size_t cbor_integer_head_space = 9;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@brief write an object key as a CBOR text string
|
@brief write an object key as a CBOR text string
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using nlohmann::json;
|
|||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -182,6 +183,161 @@ TEST_CASE("binary writer output sinks")
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// the CBOR head of an integer, encoded independently of binary_writer
|
||||||
|
void reference_cbor_head(std::vector<std::uint8_t>& out, const std::uint8_t major_type, const std::uint64_t argument)
|
||||||
|
{
|
||||||
|
if (argument <= 23)
|
||||||
|
{
|
||||||
|
out.push_back(static_cast<std::uint8_t>(major_type + argument));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int length = 8;
|
||||||
|
std::uint8_t additional = 27;
|
||||||
|
if (argument <= 0xFF)
|
||||||
|
{
|
||||||
|
length = 1;
|
||||||
|
additional = 24;
|
||||||
|
}
|
||||||
|
else if (argument <= 0xFFFF)
|
||||||
|
{
|
||||||
|
length = 2;
|
||||||
|
additional = 25;
|
||||||
|
}
|
||||||
|
else if (argument <= 0xFFFFFFFF)
|
||||||
|
{
|
||||||
|
length = 4;
|
||||||
|
additional = 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_back(static_cast<std::uint8_t>(major_type + additional));
|
||||||
|
for (int i = length - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
out.push_back(static_cast<std::uint8_t>(argument >> (8 * i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the CBOR encoding of an integer value
|
||||||
|
void reference_cbor_integer(std::vector<std::uint8_t>& out, const json& j)
|
||||||
|
{
|
||||||
|
if (j.is_number_unsigned())
|
||||||
|
{
|
||||||
|
reference_cbor_head(out, 0x00, j.get<std::uint64_t>());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto n = j.get<std::int64_t>();
|
||||||
|
if (n >= 0)
|
||||||
|
{
|
||||||
|
reference_cbor_head(out, 0x00, static_cast<std::uint64_t>(n));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
reference_cbor_head(out, 0x20, static_cast<std::uint64_t>(-1 - n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// integers at every boundary of the CBOR head sizes, signed and unsigned
|
||||||
|
std::vector<json> boundary_integers()
|
||||||
|
{
|
||||||
|
std::vector<json> result;
|
||||||
|
const std::uint64_t boundaries[] =
|
||||||
|
{
|
||||||
|
0, 1, 22, 23, 24, 25, 254, 255, 256, 257, 65534, 65535, 65536, 65537,
|
||||||
|
4294967294ULL, 4294967295ULL, 4294967296ULL, 4294967297ULL,
|
||||||
|
9223372036854775806ULL, 9223372036854775807ULL, 9223372036854775808ULL,
|
||||||
|
18446744073709551614ULL, 18446744073709551615ULL
|
||||||
|
};
|
||||||
|
for (const std::uint64_t u : boundaries)
|
||||||
|
{
|
||||||
|
result.emplace_back(u);
|
||||||
|
if (u <= static_cast<std::uint64_t>((std::numeric_limits<std::int64_t>::max)()))
|
||||||
|
{
|
||||||
|
const auto n = static_cast<std::int64_t>(u);
|
||||||
|
result.emplace_back(static_cast<json::number_integer_t>(n));
|
||||||
|
result.emplace_back(static_cast<json::number_integer_t>(-n));
|
||||||
|
result.emplace_back(static_cast<json::number_integer_t>(-n - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
// Arrays of integers are written in runs straight into space reserved from the
|
||||||
|
// sink, with a branch-free head; check them against an independent encoder for
|
||||||
|
// runs shorter and longer than one reservation, broken up by other values, and
|
||||||
|
// through each sink.
|
||||||
|
TEST_CASE("CBOR integer arrays")
|
||||||
|
{
|
||||||
|
const std::vector<json> integers = boundary_integers();
|
||||||
|
|
||||||
|
for (const std::size_t length :
|
||||||
|
{
|
||||||
|
std::size_t{1}, std::size_t{27}, std::size_t{28}, std::size_t{29}, std::size_t{57}, std::size_t{1000}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
for (const std::size_t break_every :
|
||||||
|
{
|
||||||
|
std::size_t{0}, std::size_t{1}, std::size_t{7}, std::size_t{28}
|
||||||
|
})
|
||||||
|
{
|
||||||
|
CAPTURE(length)
|
||||||
|
CAPTURE(break_every)
|
||||||
|
|
||||||
|
json array = json::array();
|
||||||
|
std::vector<std::uint8_t> expected;
|
||||||
|
for (std::size_t i = 0; i < length; ++i)
|
||||||
|
{
|
||||||
|
if (break_every != 0 && i % (break_every + 1) == break_every)
|
||||||
|
{
|
||||||
|
array.push_back("x");
|
||||||
|
expected.push_back(0x61);
|
||||||
|
expected.push_back('x');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const json& value = integers[(i * 7) % integers.size()];
|
||||||
|
array.push_back(value);
|
||||||
|
reference_cbor_integer(expected, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> head;
|
||||||
|
reference_cbor_head(head, 0x80, length);
|
||||||
|
expected.insert(expected.begin(), head.begin(), head.end());
|
||||||
|
|
||||||
|
CHECK(json::to_cbor(array) == expected);
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> through_adapter = {0xAA};
|
||||||
|
json::to_cbor(array, through_adapter);
|
||||||
|
CHECK(std::vector<std::uint8_t>(through_adapter.begin() + 1, through_adapter.end()) == expected);
|
||||||
|
|
||||||
|
std::ostringstream stream;
|
||||||
|
json::to_cbor(array, stream);
|
||||||
|
const std::string streamed = stream.str();
|
||||||
|
CHECK(std::vector<std::uint8_t>(streamed.begin(), streamed.end()) == expected);
|
||||||
|
|
||||||
|
CHECK(json::from_cbor(expected) == array);
|
||||||
|
|
||||||
|
// the same array as an object member, and as single values
|
||||||
|
const json object = {{"a", array}, {"b", array}};
|
||||||
|
CHECK(json::from_cbor(json::to_cbor(object)) == object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& value : integers)
|
||||||
|
{
|
||||||
|
CAPTURE(value)
|
||||||
|
std::vector<std::uint8_t> expected;
|
||||||
|
reference_cbor_integer(expected, value);
|
||||||
|
CHECK(json::to_cbor(value) == expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// binary_reserve_hint() is documented as a *lower* bound on the serialized size,
|
// binary_reserve_hint() is documented as a *lower* bound on the serialized size,
|
||||||
// so that reserving it up front can never leave the returned vector holding
|
// so that reserving it up front can never leave the returned vector holding
|
||||||
// capacity beyond what the value actually needs.
|
// capacity beyond what the value actually needs.
|
||||||
|
|||||||
Reference in new issue
Block a user