Repository navigation
Conversation
For "last read" in parse errors, the lexer keeps the text of the token it reads. The token start moves only at strings and numbers, so everything after a string or number belongs to it until the next one starts. Input that cannot be read again copies that text while parsing (the istream and FILE* adapters in their carry buffer, the lexer in token_string for iterators), so valid input of 1 followed by 100,000,000 newlines peaked at about 225 MB, and a long string was kept a second time. Replace the carry buffer and token_string by token_text_buffer, which keeps the first 4096 and at least the last 4112 characters of a token and drops the characters in between where it would otherwise grow, so appending stays as cheap as before. get_token_string() shows a token of more than 8192 characters as its first and last 4096 characters around "...", the same for every kind of input; contiguous input copies only these parts. The example input now peaks at 1.8 MB. Instruction counts of parsing from std::string, std::istream and FILE* are unchanged (clang 21 and GCC 15); parsing from std::list<char> needs 1 to 17 % fewer. Fixes #6120. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
| if (n != 0) | ||
| { | ||
| std::memcpy(m_end, first, n); | ||
| std::memcpy(m_end, first, n * sizeof(CharType)); |
| { | ||
| const std::size_t piece = n < tail() ? n : tail(); | ||
| make_room(piece); | ||
| std::memcpy(m_end, first, piece * sizeof(CharType)); |
| std::unique_ptr<CharType[]> data(new CharType[new_capacity]); // NOLINT(cppcoreguidelines-avoid-c-arrays,hicpp-avoid-c-arrays,modernize-avoid-c-arrays) | ||
| if (size != 0) | ||
| { | ||
| std::memcpy(data.get(), m_data.get(), size * sizeof(CharType)); |
| } | ||
| else if (kind == 2) | ||
| { | ||
| FILE* const file = std::tmpfile(); // NOLINT(cppcoreguidelines-owning-memory) |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6120.
Problem
For error messages (
last read: '...'), the lexer keeps the text of the token it is reading. The token start moves only at strings and numbers, so after a string or number everything that follows (whitespace, structural characters, literals) still belongs to that token. Input that cannot be read again copies it while parsing: thestd::istreamandFILE*adapters in their carry buffer on every buffer refill, and the lexer intoken_stringfor iterator and other input. Valid input such as1followed by 100,000,000 newlines therefore peaked at about 225 MB forstd::istream,FILE*and input iterators (3.12.0: the same), and the raw text of a long string was kept a second time next to the parsed value.Change
The text is only used for error messages, so it does not need to be kept in full:
token_text_buffer<CharType>(ininput_adapters.hpp, outside theJSON_NO_IOguard) replaces the char-onlytoken_carry_buffer(now an alias oftoken_text_buffer<char>) and the lexer'sstd::vector<char_type> token_string. It keeps the first 4096 and at least the last 4112 characters of a token. When it is full and holds more than 12,320 characters, it drops the characters in between instead of growing. That happens inmake_room(), the out-of-line path that previously reallocated, so appending a character is still one comparison and a store. The capacity never exceeds 12,320 characters.get_token_string()shows a token of more than 8192 characters as its first 4096 characters,..., and its last 4096 characters. For contiguous input, which rebuilds the token from positions, it copies only these two parts instead of the whole range.exceptions.md(parse_error.101) describes the shortened token.Tokens of up to 8192 characters are shown exactly as before. Messages for longer tokens are the same for every kind of input; the test below checks this for
std::string,std::istream,FILE*andstd::list<char>.Measurements
Peak resident memory for valid input of
1followed by 100,000,000 newlines (reproducer in #6120, Apple clang 21-O2):std::istreamFILE*The same input with a
?at the end (a parse error) peaks at 2.4 to 2.7 MB, and itswhat()has 65,684 bytes for all three inputs.Parsing speed, instructions retired (develop vs. this PR, 20 parses each;
std::list<char>5), Apple clang 21-O2 -DNDEBUG, arm64:std::stringstd::istreamFILE*std::list<char>The iterator path gets cheaper because the buffer's
push_backdoes less thanstd::vector::push_back. A first version that keptstd::vectorand added a capacity check before each character cost +5 % to +22 % there, so it was dropped.On Linux (GCC 15.2,
-O2 -DNDEBUG, x86-64), exact instruction counts with callgrind (3 parses each):std::stringstd::istreamFILE*std::list<char>(for
std::string,std::istreamandFILE*these are instructions out of 70 to 420 million, i.e. below 0.001 %). Wall times on the same machine varied by ±4 % in both directions between identical builds, so they are not given.Tests
unit-class_lexer.cpp:token_text_bufferdirectly, with characters added one by one and in pieces of different sizes (around the 4112-character boundary and larger than the capacity): the first 4096 and the last 4112 characters are kept, the size stays within the limit,pop_back()after dropping works, andclear()starts a new token.unit-class_parser.cpp: the message of a long string, of a long whitespace run after a number, and of a token just below the limit, each forstd::string,std::istream,FILE*(tmpfile) andstd::list<char>; valid input with a long whitespace run. 8 of its assertions fail on develop.JSON_NO_IO(C++11-O0) and with GCC 16 without warnings. clang-tidy (CI configuration) is clean.Breaking changes
No API change. The text of a parse error message changes for a token of more than 8192 characters: the middle of the token is replaced by
.... The SAXparse_error()last_tokenargument is shortened in the same way. Message texts are not part of the API.🤖 Generated with Claude Code