mirror of
https://github.com/simdjson/simdjson
synced 2026-06-08 17:27:07 +00:00
42c047a927
The dominant cost in the reflection serializer was the strict-aliasing reload pattern: every char* write through `string_builder::buffer.get()` forced the compiler to assume `string_builder::position` and `::capacity` may have been clobbered, so it reloaded both members from memory after each byte. perf annotate showed `ldr [position]` and `ldr [capacity]` taking 15-20% of CITM serialization time. This change refactors the reflection atom path to use an internal `writer` struct (buffer pointer + position + capacity + back-ref to string_builder). The writer lives as a stack-local in the top-level append() entry point, threaded through the inlined atom() chain via `writer&`. After SROA, the three fields are register-resident across the entire serialization. The `pos` is never round-tripped through memory between writes — it's a local size_t. This mirrors the pattern Glaze uses (passing `B&& b, auto&& ix` through every helper). The simdjson-specific bit is keeping the public string_builder API intact: only the internal atom() family is refactored to take `writer&`. The top-level append(string_builder&, T) constructs a writer on the stack, threads it through atom(), then syncs the local position back at the end. For string fields specifically, the escape path is also inlined through the writer — the existing `write_string_escaped(input, out)` helper already takes a destination pointer and returns bytes-written, so it composes cleanly with `w.ptr + w.pos`. Without this, sync/ reload around each string write was a real cost on Twitter (-7%). Performance =========== TRUE A/B in single docker invocation, 7 alternating rounds, byte- identical output, all static_reflection_comprehensive_tests pass. Build: clang-p2996 21.0.0git, -O3 -DNDEBUG, aarch64 Linux. CITM Catalog (496 682 bytes): baseline (master): 4356 MB/s patched: 6776 MB/s +55.5% Glaze: 4860 MB/s simdjson now 39.4% AHEAD of Glaze Twitter (81 927 bytes): baseline (master): 6437 MB/s patched: 6452 MB/s +0.2% (break-even) Twitter is string-heavy; the writer's gain over the existing member-based path is small here, but it no longer regresses. Default initial capacity is unchanged at 1024.
329 lines
11 KiB
C++
329 lines
11 KiB
C++
#ifndef SIMDJSON_GENERIC_STRING_BUILDER_H
|
|
|
|
#ifndef SIMDJSON_CONDITIONAL_INCLUDE
|
|
#define SIMDJSON_GENERIC_STRING_BUILDER_H
|
|
#include "simdjson/generic/implementation_simdjson_result_base.h"
|
|
#endif // SIMDJSON_CONDITIONAL_INCLUDE
|
|
|
|
namespace simdjson {
|
|
|
|
|
|
#if SIMDJSON_SUPPORTS_CONCEPTS
|
|
|
|
namespace SIMDJSON_IMPLEMENTATION {
|
|
namespace builder {
|
|
class string_builder;
|
|
}}
|
|
|
|
template <typename T, typename = void>
|
|
struct has_custom_serialization : std::false_type {};
|
|
|
|
inline constexpr struct serialize_tag {
|
|
template <typename T>
|
|
constexpr void operator()(SIMDJSON_IMPLEMENTATION::builder::string_builder& b, T&& obj) const{
|
|
return tag_invoke(*this, b, std::forward<T>(obj));
|
|
}
|
|
|
|
|
|
} serialize{};
|
|
template <typename T>
|
|
struct has_custom_serialization<T, std::void_t<
|
|
decltype(tag_invoke(serialize, std::declval<SIMDJSON_IMPLEMENTATION::builder::string_builder&>(), std::declval<T&>()))
|
|
>> : std::true_type {};
|
|
|
|
template <typename T>
|
|
constexpr bool require_custom_serialization = has_custom_serialization<T>::value;
|
|
#else
|
|
struct has_custom_serialization : std::false_type {};
|
|
#endif // SIMDJSON_SUPPORTS_CONCEPTS
|
|
|
|
namespace SIMDJSON_IMPLEMENTATION {
|
|
namespace builder {
|
|
/**
|
|
* A builder for JSON strings representing documents. This is a low-level
|
|
* builder that is not meant to be used directly by end-users. Though it
|
|
* supports atomic types (Booleans, strings), it does not support composed
|
|
* types (arrays and objects).
|
|
*
|
|
* Ultimately, this class can support kernel-specific optimizations. E.g.,
|
|
* it may make use of SIMD instructions to escape strings faster.
|
|
*/
|
|
class string_builder {
|
|
public:
|
|
simdjson_inline string_builder(size_t initial_capacity = DEFAULT_INITIAL_CAPACITY);
|
|
|
|
static constexpr size_t DEFAULT_INITIAL_CAPACITY = 1024;
|
|
|
|
/**
|
|
* Append number (includes Booleans). Booleans are mapped to the strings
|
|
* false and true. Numbers are converted to strings abiding by the JSON standard.
|
|
* Floating-point numbers are converted to the shortest string that 'correctly'
|
|
* represents the number.
|
|
*/
|
|
template<typename number_type,
|
|
typename = typename std::enable_if<std::is_arithmetic<number_type>::value>::type>
|
|
simdjson_inline void append(number_type v) noexcept;
|
|
|
|
/**
|
|
* Append character c.
|
|
*/
|
|
simdjson_inline void append(char c) noexcept;
|
|
|
|
/**
|
|
* Append the string 'null'.
|
|
*/
|
|
simdjson_inline void append_null() noexcept;
|
|
|
|
/**
|
|
* Clear the content.
|
|
*/
|
|
simdjson_inline void clear() noexcept;
|
|
|
|
/**
|
|
* Append the std::string_view, after escaping it.
|
|
* There is no UTF-8 validation.
|
|
*/
|
|
simdjson_inline void escape_and_append(std::string_view input) noexcept;
|
|
|
|
/**
|
|
* Append the std::string_view surrounded by double quotes, after escaping it.
|
|
* There is no UTF-8 validation.
|
|
*/
|
|
simdjson_inline void escape_and_append_with_quotes(std::string_view input) noexcept;
|
|
#if SIMDJSON_SUPPORTS_CONCEPTS
|
|
template<constevalutil::fixed_string key>
|
|
simdjson_inline void escape_and_append_with_quotes() noexcept;
|
|
#endif
|
|
/**
|
|
* Append the character surrounded by double quotes, after escaping it.
|
|
* There is no UTF-8 validation.
|
|
*/
|
|
simdjson_inline void escape_and_append_with_quotes(char input) noexcept;
|
|
|
|
/**
|
|
* Append the character surrounded by double quotes, after escaping it.
|
|
* There is no UTF-8 validation.
|
|
*/
|
|
simdjson_inline void escape_and_append_with_quotes(const char* input) noexcept;
|
|
|
|
/**
|
|
* Append the C string directly, without escaping.
|
|
* There is no UTF-8 validation.
|
|
*/
|
|
simdjson_inline void append_raw(const char *c) noexcept;
|
|
|
|
/**
|
|
* Append "{" to the buffer.
|
|
*/
|
|
simdjson_inline void start_object() noexcept;
|
|
|
|
/**
|
|
* Append "}" to the buffer.
|
|
*/
|
|
simdjson_inline void end_object() noexcept;
|
|
|
|
/**
|
|
* Append "[" to the buffer.
|
|
*/
|
|
simdjson_inline void start_array() noexcept;
|
|
|
|
/**
|
|
* Append "]" to the buffer.
|
|
*/
|
|
simdjson_inline void end_array() noexcept;
|
|
|
|
/**
|
|
* Append "," to the buffer.
|
|
*/
|
|
simdjson_inline void append_comma() noexcept;
|
|
|
|
/**
|
|
* Append ":" to the buffer.
|
|
*/
|
|
simdjson_inline void append_colon() noexcept;
|
|
|
|
/**
|
|
* Append a key-value pair to the buffer.
|
|
* The key is escaped and surrounded by double quotes.
|
|
* The value is escaped if it is a string.
|
|
*/
|
|
template<typename key_type, typename value_type>
|
|
simdjson_inline void append_key_value(key_type key, value_type value) noexcept;
|
|
#if SIMDJSON_SUPPORTS_CONCEPTS
|
|
template<constevalutil::fixed_string key, typename value_type>
|
|
simdjson_inline void append_key_value(value_type value) noexcept;
|
|
|
|
// Support for optional types (std::optional, etc.)
|
|
template <concepts::optional_type T>
|
|
requires(!require_custom_serialization<T>)
|
|
simdjson_inline void append(const T &opt);
|
|
|
|
template <typename T>
|
|
requires(require_custom_serialization<T>)
|
|
simdjson_inline void append(T &&val);
|
|
|
|
// Support for string-like types
|
|
template <typename T>
|
|
requires(std::is_convertible<T, std::string_view>::value ||
|
|
std::is_same<T, const char*>::value )
|
|
simdjson_inline void append(const T &value);
|
|
#endif
|
|
#if SIMDJSON_SUPPORTS_RANGES && SIMDJSON_SUPPORTS_CONCEPTS
|
|
// Support for range-based appending (std::ranges::view, etc.)
|
|
template <std::ranges::range R>
|
|
requires (!std::is_convertible<R, std::string_view>::value && !concepts::optional_type<R> && !require_custom_serialization<R>)
|
|
simdjson_inline void append(const R &range) noexcept;
|
|
#endif
|
|
/**
|
|
* Append the std::string_view directly, without escaping.
|
|
* There is no UTF-8 validation.
|
|
*/
|
|
simdjson_inline void append_raw(std::string_view input) noexcept;
|
|
|
|
/**
|
|
* Append len characters from str.
|
|
* There is no UTF-8 validation.
|
|
*/
|
|
simdjson_inline void append_raw(const char *str, size_t len) noexcept;
|
|
|
|
/**
|
|
* Append exactly N characters from str. The length is a template parameter
|
|
* so the compiler can fully inline the memcpy with a compile-time-constant
|
|
* size, avoiding the libc call. Used for compile-time-constant keys in the
|
|
* reflection struct atom.
|
|
*/
|
|
template <size_t N>
|
|
simdjson_inline void append_raw_n(const char *str) noexcept;
|
|
#if SIMDJSON_EXCEPTIONS
|
|
/**
|
|
* Creates an std::string from the written JSON buffer.
|
|
* Throws if memory allocation failed
|
|
*
|
|
* The result may not be valid UTF-8 if some of your content was not valid UTF-8.
|
|
* Use validate_unicode() to check the content if needed.
|
|
*/
|
|
simdjson_inline operator std::string() const noexcept(false);
|
|
|
|
/**
|
|
* Creates an std::string_view from the written JSON buffer.
|
|
* Throws if memory allocation failed.
|
|
*
|
|
* The result may not be valid UTF-8 if some of your content was not valid UTF-8.
|
|
* Use validate_unicode() to check the content if needed.
|
|
*/
|
|
simdjson_inline operator std::string_view() const noexcept(false) simdjson_lifetime_bound;
|
|
#endif
|
|
|
|
/**
|
|
* Returns a view on the written JSON buffer. Returns an error
|
|
* if memory allocation failed.
|
|
*
|
|
* The result may not be valid UTF-8 if some of your content was not valid UTF-8.
|
|
* Use validate_unicode() to check the content.
|
|
*/
|
|
simdjson_inline simdjson_result<std::string_view> view() const noexcept;
|
|
|
|
/**
|
|
* Appends the null character to the buffer and returns
|
|
* a pointer to the beginning of the written JSON buffer.
|
|
* Returns an error if memory allocation failed.
|
|
* The result is null-terminated.
|
|
*
|
|
* The result may not be valid UTF-8 if some of your content was not valid UTF-8.
|
|
* Use validate_unicode() to check the content.
|
|
*/
|
|
simdjson_inline simdjson_result<const char *> c_str() noexcept;
|
|
|
|
/**
|
|
* Return true if the content is valid UTF-8.
|
|
*/
|
|
simdjson_inline bool validate_unicode() const noexcept;
|
|
|
|
/**
|
|
* Returns the current size of the written JSON buffer.
|
|
* If an error occurred, returns 0.
|
|
*/
|
|
simdjson_inline size_t size() const noexcept;
|
|
|
|
// ============================================================
|
|
// Internal hooks for the position-as-local writer in json_builder.h.
|
|
// These exist so the reflection atom code can hold buffer pointer,
|
|
// position and capacity in registers across long write chains rather
|
|
// than reloading them after every char* write (strict aliasing
|
|
// forces those reloads when accessed via members of *this). User
|
|
// code should NOT call these directly.
|
|
// ============================================================
|
|
simdjson_inline char *unsafe_data() noexcept { return buffer.get(); }
|
|
simdjson_inline size_t unsafe_position() const noexcept { return position; }
|
|
simdjson_inline size_t unsafe_capacity() const noexcept { return capacity; }
|
|
simdjson_inline void unsafe_set_position(size_t p) noexcept { position = p; }
|
|
/// Make capacity available for at least `n` more bytes after the current
|
|
/// position. Returns false if the allocation failed.
|
|
simdjson_inline bool unsafe_grow(size_t needed_total_capacity) noexcept {
|
|
grow_buffer(needed_total_capacity);
|
|
return is_valid;
|
|
}
|
|
simdjson_inline bool unsafe_is_valid() const noexcept { return is_valid; }
|
|
|
|
private:
|
|
/**
|
|
* Returns true if we can write at least upcoming_bytes bytes.
|
|
* The underlying buffer is reallocated if needed. It is designed
|
|
* to be called before writing to the buffer. It should be fast.
|
|
*/
|
|
simdjson_inline bool capacity_check(size_t upcoming_bytes);
|
|
|
|
/**
|
|
* Grow the buffer to at least desired_capacity bytes.
|
|
* If the allocation fails, is_valid is set to false. We expect
|
|
* that this function would not be repeatedly called.
|
|
*/
|
|
inline void grow_buffer(size_t desired_capacity);
|
|
|
|
/**
|
|
* We use this helper function to make sure that is_valid is kept consistent.
|
|
*/
|
|
simdjson_inline void set_valid(bool valid) noexcept;
|
|
|
|
std::unique_ptr<char[]> buffer{};
|
|
size_t position{0};
|
|
size_t capacity{0};
|
|
bool is_valid{true};
|
|
};
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
#if !SIMDJSON_STATIC_REFLECTION
|
|
// fallback implementation until we have static reflection
|
|
template <class Z>
|
|
simdjson_warn_unused simdjson_result<std::string> to_json(const Z &z, size_t initial_capacity = simdjson::SIMDJSON_IMPLEMENTATION::builder::string_builder::DEFAULT_INITIAL_CAPACITY) {
|
|
simdjson::SIMDJSON_IMPLEMENTATION::builder::string_builder b(initial_capacity);
|
|
b.append(z);
|
|
std::string_view s;
|
|
auto e = b.view().get(s);
|
|
if(e) { return e; }
|
|
return std::string(s);
|
|
}
|
|
template <class Z>
|
|
simdjson_warn_unused error_code to_json(const Z &z, std::string &s, size_t initial_capacity = simdjson::SIMDJSON_IMPLEMENTATION::builder::string_builder::DEFAULT_INITIAL_CAPACITY) {
|
|
simdjson::SIMDJSON_IMPLEMENTATION::builder::string_builder b(initial_capacity);
|
|
b.append(z);
|
|
std::string_view sv;
|
|
auto e = b.view().get(sv);
|
|
if(e) { return e; }
|
|
s.assign(sv.data(), sv.size());
|
|
return simdjson::SUCCESS;
|
|
}
|
|
#endif
|
|
|
|
#if SIMDJSON_SUPPORTS_CONCEPTS
|
|
#endif // SIMDJSON_SUPPORTS_CONCEPTS
|
|
|
|
} // namespace simdjson
|
|
|
|
#endif // SIMDJSON_GENERIC_STRING_BUILDER_H
|