-
Notifications
You must be signed in to change notification settings - Fork 4.3k
GH-50944: [C++] Replace RapidJSON with simdjson in JSON chunker #50945
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,100 +22,89 @@ | |
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| #include "arrow/json/rapidjson_defs.h" | ||
| #include "rapidjson/reader.h" | ||
| #include <simdjson.h> | ||
|
|
||
| #include "arrow/buffer.h" | ||
| #include "arrow/json/options.h" | ||
| #include "arrow/util/logging_internal.h" | ||
| #include "arrow/util/simdjson_internal.h" | ||
|
|
||
| namespace arrow { | ||
|
|
||
| using std::string_view; | ||
|
|
||
| namespace json { | ||
|
|
||
| namespace rj = arrow::rapidjson; | ||
|
|
||
| static size_t ConsumeWhitespace(string_view view) { | ||
| #ifdef RAPIDJSON_SIMD | ||
| auto data = view.data(); | ||
| auto nonws_begin = rj::SkipWhitespace_SIMD(data, data + view.size()); | ||
| return nonws_begin - data; | ||
| #else | ||
| auto ws_count = view.find_first_not_of(" \t\r\n"); | ||
| if (ws_count == string_view::npos) { | ||
| static size_t ConsumeWhitespace(std::string_view view) { | ||
| const auto ws_count = view.find_first_not_of(" \t\r\n"); | ||
| if (ws_count == std::string_view::npos) { | ||
| return view.size(); | ||
| } else { | ||
| return ws_count; | ||
| } | ||
| #endif | ||
| return ws_count; | ||
| } | ||
|
|
||
| /// RapidJson custom stream for reading JSON stored in multiple buffers | ||
| /// http://rapidjson.org/md_doc_stream.html#CustomStream | ||
| class MultiStringStream { | ||
| public: | ||
| using Ch = char; | ||
| explicit MultiStringStream(std::vector<string_view> strings) | ||
| : strings_(std::move(strings)) { | ||
| std::reverse(strings_.begin(), strings_.end()); | ||
| } | ||
| explicit MultiStringStream(const BufferVector& buffers) : strings_(buffers.size()) { | ||
| for (size_t i = 0; i < buffers.size(); ++i) { | ||
| strings_[i] = string_view(*buffers[i]); | ||
| } | ||
| std::reverse(strings_.begin(), strings_.end()); | ||
| } | ||
| char Peek() const { | ||
| if (strings_.size() == 0) return '\0'; | ||
| return strings_.back()[0]; | ||
| } | ||
| char Take() { | ||
| if (strings_.size() == 0) return '\0'; | ||
| char taken = strings_.back()[0]; | ||
| if (strings_.back().size() == 1) { | ||
| strings_.pop_back(); | ||
| } else { | ||
| strings_.back() = strings_.back().substr(1); | ||
| } | ||
| ++index_; | ||
| return taken; | ||
| } | ||
| size_t Tell() { return index_; } | ||
| void Put(char) { ARROW_LOG(FATAL) << "not implemented"; } | ||
| void Flush() { ARROW_LOG(FATAL) << "not implemented"; } | ||
| char* PutBegin() { | ||
| ARROW_LOG(FATAL) << "not implemented"; | ||
| return nullptr; | ||
| static size_t ConsumeWholeObject(std::string_view input) { | ||
| if (input.empty()) { | ||
| return 0; | ||
| } | ||
| size_t PutEnd(char*) { | ||
| ARROW_LOG(FATAL) << "not implemented"; | ||
|
|
||
| const size_t start = ConsumeWhitespace(input); | ||
| if (start >= input.size()) { | ||
| return 0; | ||
| } | ||
|
|
||
| private: | ||
| size_t index_ = 0; | ||
| std::vector<string_view> strings_; | ||
| }; | ||
| int depth = 0; | ||
| bool in_string = false; | ||
| bool escape_next = false; | ||
| bool started = false; | ||
|
|
||
| for (size_t i = start; i < input.size(); ++i) { | ||
| const char c = input[i]; | ||
|
|
||
| if (escape_next) { | ||
| escape_next = false; | ||
| continue; | ||
| } | ||
|
|
||
| if (c == '\\' && in_string) { | ||
| escape_next = true; | ||
| continue; | ||
| } | ||
|
|
||
| if (c == '"') { | ||
| in_string = !in_string; | ||
| continue; | ||
| } | ||
|
|
||
| if (!in_string) { | ||
| if (c == '{' || c == '[') { | ||
| started = true; | ||
| ++depth; | ||
| } else if (c == '}' || c == ']') { | ||
| if (!started) { | ||
| return 0; | ||
| } | ||
|
|
||
| --depth; | ||
|
|
||
| if (depth == 0) { | ||
| const size_t end_pos = i + 1; | ||
| const size_t doc_len = end_pos - start; | ||
|
|
||
| template <typename Stream> | ||
| static size_t ConsumeWholeObject(Stream&& stream) { | ||
| static constexpr unsigned parse_flags = rj::kParseIterativeFlag | | ||
| rj::kParseStopWhenDoneFlag | | ||
| rj::kParseNumbersAsStringsFlag; | ||
| rj::BaseReaderHandler<rj::UTF8<>> handler; | ||
| rj::Reader reader; | ||
| // parse a single JSON object | ||
| switch (reader.Parse<parse_flags>(stream, handler).Code()) { | ||
| case rj::kParseErrorNone: | ||
| return stream.Tell(); | ||
| case rj::kParseErrorDocumentEmpty: | ||
| return 0; | ||
| default: | ||
| // rapidjson emitted an error, the most recent object was partial | ||
| return string_view::npos; | ||
| simdjson::padded_string padded(input.data() + start, doc_len); | ||
| simdjson::dom::parser parser; | ||
|
|
||
| if (!parser.parse(padded).error()) { | ||
| return end_pos; | ||
| } | ||
|
|
||
| return std::string_view::npos; | ||
| } else if (depth < 0) { | ||
| return 0; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return std::string_view::npos; | ||
| } | ||
|
|
||
| namespace { | ||
|
|
@@ -124,40 +113,76 @@ namespace { | |
| // and uses actual JSON parsing to delimit them. | ||
| class ParsingBoundaryFinder : public BoundaryFinder { | ||
| public: | ||
| Status FindFirst(string_view partial, string_view block, int64_t* out_pos) override { | ||
| auto length = ConsumeWholeObject(MultiStringStream({partial, block})); | ||
| if (length == string_view::npos) { | ||
| Status FindFirst(std::string_view partial, std::string_view block, | ||
| int64_t* out_pos) override { | ||
| std::string combined; | ||
| combined.reserve(partial.size() + block.size()); | ||
| combined.append(partial); | ||
| combined.append(block); | ||
|
Comment on lines
+118
to
+121
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should only concatenate if both substrings are non-empty. |
||
|
|
||
| const size_t start = ConsumeWhitespace(combined); | ||
| if (start < combined.size() && combined[start] != '{' && combined[start] != '[') { | ||
| return Status::Invalid("JSON chunk error: invalid data at end of document"); | ||
| } | ||
|
|
||
| const auto length = ConsumeWholeObject(combined); | ||
|
|
||
| if (length == std::string_view::npos) { | ||
| *out_pos = -1; | ||
| } else if (ARROW_PREDICT_FALSE(length < partial.size())) { | ||
| return Status::Invalid("JSON chunk error: invalid data at end of document"); | ||
| return Status::Invalid("JSON parse error: Invalid value"); | ||
|
Comment on lines
-132
to
+133
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not keep the error message? |
||
| } else { | ||
| DCHECK_LE(length, partial.size() + block.size()); | ||
| *out_pos = static_cast<int64_t>(length - partial.size()); | ||
| } | ||
|
|
||
| return Status::OK(); | ||
| } | ||
|
|
||
| Status FindLast(std::string_view block, int64_t* out_pos) override { | ||
| const size_t block_length = block.size(); | ||
| size_t consumed_length = 0; | ||
|
|
||
| if (block_length > 0) { | ||
| const size_t start = ConsumeWhitespace(block); | ||
| if (start < block.size() && block[start] != '{' && block[start] != '[') { | ||
| return Status::Invalid("JSON parse error: Invalid value"); | ||
| } | ||
| } | ||
|
|
||
| while (consumed_length < block_length) { | ||
| rj::MemoryStream ms(reinterpret_cast<const char*>(block.data()), block.size()); | ||
| using InputStream = rj::EncodedInputStream<rj::UTF8<>, rj::MemoryStream>; | ||
| auto length = ConsumeWholeObject(InputStream(ms)); | ||
| if (length == string_view::npos || length == 0) { | ||
| // found incomplete object or block is empty | ||
| const auto length = ConsumeWholeObject(block); | ||
|
|
||
| if (length == std::string_view::npos || length == 0) { | ||
| const size_t start = ConsumeWhitespace(block); | ||
|
|
||
| if (start < block.size()) { | ||
| const char first_char = block[start]; | ||
|
|
||
| if (first_char != '{' && first_char != '[') { | ||
| const size_t remaining_len = block.size() - start; | ||
|
|
||
| if (remaining_len > 1 || (first_char != '}' && first_char != ']')) { | ||
| return Status::Invalid("JSON parse error: Invalid value"); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+157
to
+169
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add comments explaining what this does and why it is necessary. |
||
|
|
||
| break; | ||
| } | ||
|
|
||
| consumed_length += length; | ||
| block = block.substr(length); | ||
| } | ||
|
|
||
| if (consumed_length == 0) { | ||
| *out_pos = -1; | ||
| } else { | ||
| consumed_length += ConsumeWhitespace(block); | ||
| DCHECK_LE(consumed_length, block_length); | ||
| *out_pos = static_cast<int64_t>(consumed_length); | ||
| } | ||
|
|
||
| return Status::OK(); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does not account for unicode escapes. Do we have to parse JSON by hand like this?