-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Improve performance of JSON rendering #6380
Changes from 11 commits
463663b
a9c72e3
cb0af6b
4b0ea28
c784ff7
b9957bb
3781068
666ff1c
d2ce6d2
ca7e560
05c6008
4f222be
889e4ce
87db480
258d656
a4d1783
ee7499d
d033e3b
d871e20
bc6a4d8
ae157de
6a5b3f4
50efe45
04429d4
7430d62
03f9671
32deb11
cbc0868
f057ef7
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 |
---|---|---|
|
@@ -27,18 +27,32 @@ namespace | |
constexpr int MAX_FLOAT_STRING_LENGTH = 256; | ||
} | ||
|
||
struct Renderer | ||
template <typename Out> struct Renderer | ||
{ | ||
explicit Renderer(std::ostream &_out) : out(_out) {} | ||
explicit Renderer(Out &_out) : out(_out) {} | ||
|
||
void operator()(const String &string) const | ||
void operator()(const String &string) | ||
{ | ||
out << "\""; | ||
out << escape_JSON(string.value); | ||
out << "\""; | ||
write('"'); | ||
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. |
||
// here we assume that vast majority of strings don't need to be escaped, | ||
// so we check it first and escape only if needed | ||
auto size = SizeOfEscapedJSONString(string.value); | ||
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. Can we instead just search for the existence of a character that requires escaping? 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. Do we have a feel for how often OSRM responses do require escaping? I.e how representative are these benchmarks? 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 can, but current implementation allows to preallocate string for escaped string if we need it(because we know required size - see how else we use this
That’s the great question. I don’t have numbers, just a gut feeling… Will try to collect some statistics from real responses. Btw this idea is inspired by implementation in https://github.com/nlohmann/json 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. Actually probably might be a good idea to take huge real JSON response, convert it to our json:: objects using script and try to check some of optimizations on it. 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. Well, actually I found 2 "sources" of escaped strings in responses.
But even though they appear quite often these strings are usually relatively big and it should allow us to preallocate string buffer for them and avoid expensive reallocations during escaping(but we have to scan string twice)...
After that I updated benchmark to use real output of OSRM(huge route from Portugal to Korea) and "played" a bit with this optimisation. And my conclusion is that it seems this optimisation is still useful on real JSONs, but improvement is barely noticeable. Without this optimisation (i.e. just always call
|
||
if (size == string.value.size()) | ||
{ | ||
write(string.value); | ||
} | ||
else | ||
{ | ||
std::string escaped; | ||
escaped.reserve(size); | ||
EscapeJSONString(string.value, escaped); | ||
|
||
write(escaped); | ||
} | ||
write('"'); | ||
} | ||
|
||
void operator()(const Number &number) const | ||
void operator()(const Number &number) | ||
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 spend quite a lot on number formatting: I've tried to optimize it using 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. Yeah - I optimized this part a long time ago - if you dig into this function, you'll notice we're already using a highly optimized floating point -> string rendering implementation, rather than just doing Turning floats into strings is fairly expensive, which is why it still shows up in a profiler run. There are a lot of papers about number->string rendering performance, you can possibly find a better implementation than the one already here, but the gains over what we have will likely be small. 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. Yes, but as I can see we currently use It seemed to be low-hanging fruit to use it here(we already depend on fmtlib anyway), but 100% agree - no reason to waste time on it since possible improvement is marginal. 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. It seems we have UB in current implementation 🤔 https://github.com/Project-OSRM/osrm-backend/actions/runs/3160140786/jobs/5144255047 It seems we had no UBSAN alerts here before, because for 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. Checked on separate PR that 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. |
||
{ | ||
char buffer[MAX_FLOAT_STRING_LENGTH] = {'\0'}; | ||
ieee754::dtoa_milo(number.value, buffer); | ||
|
@@ -64,131 +78,95 @@ struct Renderer | |
} | ||
++pos; | ||
} | ||
out << buffer; | ||
write(buffer); | ||
} | ||
|
||
void operator()(const Object &object) const | ||
void operator()(const Object &object) | ||
{ | ||
out << "{"; | ||
write('{'); | ||
for (auto it = object.values.begin(), end = object.values.end(); it != end;) | ||
{ | ||
out << "\"" << it->first << "\":"; | ||
write('\"'); | ||
write(it->first); | ||
write("\":"); | ||
mapbox::util::apply_visitor(Renderer(out), it->second); | ||
if (++it != end) | ||
{ | ||
out << ","; | ||
write(','); | ||
} | ||
} | ||
out << "}"; | ||
write('}'); | ||
} | ||
|
||
void operator()(const Array &array) const | ||
void operator()(const Array &array) | ||
{ | ||
out << "["; | ||
write('['); | ||
for (auto it = array.values.cbegin(), end = array.values.cend(); it != end;) | ||
{ | ||
mapbox::util::apply_visitor(Renderer(out), *it); | ||
if (++it != end) | ||
{ | ||
out << ","; | ||
write(','); | ||
} | ||
} | ||
out << "]"; | ||
write(']'); | ||
} | ||
|
||
void operator()(const True &) const { out << "true"; } | ||
void operator()(const True &) { write("true"); } | ||
|
||
void operator()(const False &) const { out << "false"; } | ||
void operator()(const False &) { write("false"); } | ||
|
||
void operator()(const Null &) const { out << "null"; } | ||
void operator()(const Null &) { write("null"); } | ||
|
||
private: | ||
std::ostream &out; | ||
void write(const std::string &str); | ||
void write(const char *str); | ||
void write(char ch); | ||
|
||
private: | ||
Out &out; | ||
}; | ||
|
||
struct ArrayRenderer | ||
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. |
||
template <> void Renderer<std::vector<char>>::write(const std::string &str) | ||
{ | ||
explicit ArrayRenderer(std::vector<char> &_out) : out(_out) {} | ||
|
||
void operator()(const String &string) const | ||
{ | ||
out.push_back('\"'); | ||
const auto string_to_insert = escape_JSON(string.value); | ||
out.insert(std::end(out), std::begin(string_to_insert), std::end(string_to_insert)); | ||
out.push_back('\"'); | ||
} | ||
out.insert(out.end(), str.begin(), str.end()); | ||
} | ||
|
||
void operator()(const Number &number) const | ||
{ | ||
const std::string number_string = cast::to_string_with_precision(number.value); | ||
out.insert(out.end(), number_string.begin(), number_string.end()); | ||
} | ||
template <> void Renderer<std::vector<char>>::write(const char *str) | ||
{ | ||
out.insert(out.end(), str, str + strlen(str)); | ||
} | ||
|
||
void operator()(const Object &object) const | ||
{ | ||
out.push_back('{'); | ||
for (auto it = object.values.begin(), end = object.values.end(); it != end;) | ||
{ | ||
out.push_back('\"'); | ||
out.insert(out.end(), it->first.begin(), it->first.end()); | ||
out.push_back('\"'); | ||
out.push_back(':'); | ||
template <> void Renderer<std::vector<char>>::write(char ch) { out.push_back(ch); } | ||
|
||
mapbox::util::apply_visitor(ArrayRenderer(out), it->second); | ||
if (++it != end) | ||
{ | ||
out.push_back(','); | ||
} | ||
} | ||
out.push_back('}'); | ||
} | ||
template <> void Renderer<std::ostream>::write(const std::string &str) { out << str; } | ||
|
||
void operator()(const Array &array) const | ||
{ | ||
out.push_back('['); | ||
for (auto it = array.values.cbegin(), end = array.values.cend(); it != end;) | ||
{ | ||
mapbox::util::apply_visitor(ArrayRenderer(out), *it); | ||
if (++it != end) | ||
{ | ||
out.push_back(','); | ||
} | ||
} | ||
out.push_back(']'); | ||
} | ||
template <> void Renderer<std::ostream>::write(const char *str) { out << str; } | ||
|
||
void operator()(const True &) const | ||
{ | ||
const std::string temp("true"); | ||
out.insert(out.end(), temp.begin(), temp.end()); | ||
} | ||
template <> void Renderer<std::ostream>::write(char ch) { out << ch; } | ||
|
||
void operator()(const False &) const | ||
{ | ||
const std::string temp("false"); | ||
out.insert(out.end(), temp.begin(), temp.end()); | ||
} | ||
template <> void Renderer<std::string>::write(const std::string &str) { out += str; } | ||
|
||
void operator()(const Null &) const | ||
{ | ||
const std::string temp("null"); | ||
out.insert(out.end(), temp.begin(), temp.end()); | ||
} | ||
template <> void Renderer<std::string>::write(const char *str) { out += str; } | ||
|
||
private: | ||
std::vector<char> &out; | ||
}; | ||
template <> void Renderer<std::string>::write(char ch) { out += ch; } | ||
|
||
inline void render(std::ostream &out, const Object &object) | ||
{ | ||
Value value = object; | ||
mapbox::util::apply_visitor(Renderer(out), value); | ||
Renderer renderer(out); | ||
renderer(object); | ||
} | ||
|
||
inline void render(std::string &out, const Object &object) | ||
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. |
||
{ | ||
Renderer renderer(out); | ||
renderer(object); | ||
} | ||
|
||
inline void render(std::vector<char> &out, const Object &object) | ||
{ | ||
Value value = object; | ||
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. |
||
mapbox::util::apply_visitor(ArrayRenderer(out), value); | ||
Renderer renderer(out); | ||
renderer(object); | ||
} | ||
|
||
} // namespace json | ||
|
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.
0.
Baseline on my M1 Pro/32 gb ram:
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.
"string" here is
std::stringstream ss; ... = ss.str()