docs: re-measure the 3.13.0 post and apply the review

Measure everything again against develop with the open pull requests of
11-03 (suite, timeline, comparison on the Mac and on x86-64, compile
times, stream gap), add the suite on an Intel MacBook Pro as a check,
mark the ways that keep number text as "N" in the use-case charts, move
the loop-shape box to theme 5, and apply the style and wording fixes of
the review.

Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
Niels Lohmann committed 2026-11-03 15:31:38 +01:00
1 parent 5c595412d1
commit 5f49306727
27 files changed
+13759 -13610

No files matched your search

+56 -44
View File
@@ -201,24 +201,24 @@ def digits():
# ---------- overview: speedup per workload (Google Benchmark rows, 3.12.0 time / 3.13.0 time) ----------
# (group, label, speedup), copied from the table "The short version"
OVERVIEW = [
("Parsing", "twitter.json", 3.02),
("Parsing", "citm_catalog.json", 4.11),
("Parsing", "canada.json", 3.41),
("Parsing", "canada.json, indented", 6.59),
("Parsing", "citm, ordered_json", 6.71),
("Parsing", "twitter, std::ifstream", 2.44),
("Parsing", "citm, FILE*", 4.71),
("Parsing", "citm, accept()", 7.04),
("Writing", "dump() twitter", 6.66),
("Writing", "dump() canada", 2.93),
("Writing", "dump(4) citm", 4.63),
("Writing", "to_cbor() twitter", 4.48),
("Reading CBOR", "twitter, memory", 3.18),
("Reading CBOR", "twitter, FILE*", 6.41),
("Values", "diff() canada", 10.34),
("Values", "iterate canada", 2.71),
("Values", "copy + destroy citm", 1.11),
("Values", "== citm", 1.04),
("Parsing", "twitter.json", 3.14),
("Parsing", "citm_catalog.json", 4.25),
("Parsing", "canada.json", 3.35),
("Parsing", "canada.json, indented", 6.51),
("Parsing", "citm, ordered_json", 6.85),
("Parsing", "twitter, std::ifstream", 2.50),
("Parsing", "citm, FILE*", 4.74),
("Parsing", "citm, accept()", 7.07),
("Writing", "dump() twitter", 6.70),
("Writing", "dump() canada", 2.98),
("Writing", "dump(4) citm", 4.70),
("Writing", "to_cbor() twitter", 4.59),
("Reading CBOR", "twitter, memory", 3.39),
("Reading CBOR", "twitter, FILE*", 6.59),
("Values", "diff() canada", 10.46),
("Values", "iterate canada", 2.73),
("Values", "copy + destroy citm", 1.13),
("Values", "== citm", 1.06),
]
@@ -396,15 +396,15 @@ def timeline():
# ---------- cliffs: 3.12.0 and 3.13.0 times on a log axis ----------
# (operation, 3.12.0 ms, 3.13.0 ms), copied from the table in "Fix the algorithm first"
CLIFFS = [
("diff() 64k numbers vs []", 4140, 10.5),
("diff() 20k objects vs half", 119, 3.99),
("parse() callback, 32k objects", 1361, 12.1),
("operator< nested 24 deep", 64.0, 0.002),
("operator< citm, 10x", 278, 3.86),
("flatten() citm, ordered_json", 251, 1.39),
("to_bson() 200 deep, 100x", 19.9, 0.349),
("push_back() 100k, diagnostics", 1532, 5.04),
("at(i) 10k, diagnostics", 43.6, 0.081),
("diff() 64k numbers vs []", 4237, 10.8),
("diff() 20k objects vs half", 122, 4.13),
("parse() callback, 32k objects", 1398, 12.1),
("operator< nested 24 deep", 64.4, 0.002),
("operator< citm, 10x", 284, 3.94),
("flatten() citm, ordered_json", 271, 1.45),
("to_bson() 200 deep, 100x", 20.3, 0.352),
("push_back() 100k, diagnostics", 1589, 5.32),
("at(i) 10k, diagnostics", 45.9, 0.088),
]
@@ -495,13 +495,13 @@ def heldout():
# ---------- the price: compile time of two programs against 3.12.0 ----------
# CPU time of the compiler in ms, median of 9 interleaved rounds, C++20, M1 Max (the post's appendix G)
# CPU time of the compiler in ms, median of 5 interleaved rounds, C++20, M1 Max (the post's appendix G)
# (3.12.0, 3.13.0, 3.13.0 with a precompiled header, 3.13.0 as a module)
PRICE = [
("Apple clang 21, -O0 (debug)", [("count_numbers", 959.1, 1155.3, 679.1, 803.9), ("server_config", 1108.2, 1445.1, 763.1, 1057.0)]),
("Apple clang 21, -O2", [("count_numbers", 1268.8, 1867.7, 1364.3, 1512.9), ("server_config", 1576.9, 2353.6, 1749.4, 2141.4)]),
("GCC 16, -O0 (debug)", [("count_numbers", 1443.7, 2346.4, 1579.2, 1662.5), ("server_config", 1984.8, 2781.7, 2113.3, 2181.1)]),
("GCC 16, -O2", [("count_numbers", 2326.9, 3453.3, 2972.4, 2895.3), ("server_config", 2888.7, 4388.6, 3941.8, 3825.5)]),
("Apple clang 21, -O0 (debug)", [("count_numbers", 955.0, 1320.5, 688.5, 917.5), ("server_config", 1113.2, 1496.4, 862.6, 1135.5)]),
("Apple clang 21, -O2", [("count_numbers", 1393.2, 2081.0, 1458.4, 1684.6), ("server_config", 1753.5, 2545.6, 1925.3, 2168.6)]),
("GCC 16, -O0 (debug)", [("count_numbers", 1569.4, 2548.4, 1725.0, 1699.8), ("server_config", 2025.4, 2985.1, 2301.6, 2325.8)]),
("GCC 16, -O2", [("count_numbers", 2336.0, 3701.5, 2986.8, 2913.4), ("server_config", 3121.5, 4714.2, 4006.2, 4054.9)]),
]
PRICE_SERIES = [("3.13.0", "nj-charts-c2"), ("3.13.0, precompiled header", "nj-charts-c1"), ("3.13.0, module", "nj-charts-c3")]
@@ -579,22 +579,25 @@ def geomean(xs):
WRITING = ("edit", "minify", "write") # the use cases that write numbers
NUMBER_TEXT = ("source numbers", "raw numbers") # variants that write each number by copying its text
def normalize_usecases(data):
"""Chooses each library's best variants again with the rules of the post: variants that reuse a parser or document
across calls are measured but not chosen, and neither are variants that write each number by copying its text
instead of formatting it (json_document's number_format::source, yyjson's and RapidJSON's raw numbers in edit,
minify and write). Skipping the number conversion while reading is allowed"""
across calls are measured but not chosen. Variants that write each number by copying its text instead of formatting
it (json_document's number_format::source, yyjson's and RapidJSON's raw numbers in edit, minify and write) are not
chosen as the fully validating way, but as a way that does less, marked "N" like "U" and "P". Skipping the number
conversion while reading is allowed. Each pick has a "mark": "", or the letters of what it skips"""
data = json.loads(json.dumps(data))
rank = {"full": 0, "n/a": 0, "no UTF-8": 1, "partial": 2}
for uc, files in data.items():
banned = ("reused",) + (("source numbers", "raw numbers") if uc in WRITING else ())
numtext = lambda k: uc in WRITING and any(w in k for w in NUMBER_TEXT)
for cell in files.values():
for e in cell.values():
vs = [(v["us"], k, v["level"]) for k, v in e["variants"].items() if not any(w in k for w in banned)]
full = [v for v in vs if rank[v[2]] == 0]
pick = lambda v: {"us": v[0], "variant": v[1], "level": "full" if rank[v[2]] == 0 else v[2]}
vs = [(v["us"], k, v["level"]) for k, v in e["variants"].items() if "reused" not in k]
full = [v for v in vs if rank[v[2]] == 0 and not numtext(v[1])]
pick = lambda v: {"us": v[0], "variant": v[1], "level": "full" if rank[v[2]] == 0 else v[2],
"mark": " ".join(m for m in (LEVEL_MARK.get(v[2], ""), "N" if numtext(v[1]) else "") if m)}
e["best"] = pick(min(full)) if full else None
e["best_any"] = pick(min(vs))
return data
@@ -615,6 +618,12 @@ CHECKS_LESS = [
("sonic-cpp", "", "trusts UTF-8: sonic-cpp 1.0.2 doesn't check it"),
("serde_json", "", "trusts UTF-8 in the strings it skips (from_slice); strings it reads are checked"),
]
# what a variant that copies number text does (the "N" half of edit, minify and write)
NUMBER_TEXT_TIPS = {
"nlohmann json_document": "copies each number's text instead of formatting it (number_format::source)",
"yyjson": "copies each number's text instead of formatting it (YYJSON_READ_NUMBER_AS_RAW)",
"RapidJSON": "copies each number's text instead of formatting it (kParseNumbersAsStringsFlag)",
}
def checks_less(lib, variant):
@@ -651,7 +660,9 @@ def usecase_table(data):
alt = None
levels = {a["level"] for _, _, a in c["files"]} if alt is not None else set()
level = "partial" if "partial" in levels else "no UTF-8" if "no UTF-8" in levels else "full"
out.setdefault(lib, {})[uc] = {"full": full, "alt": alt, "level": level, "files": c["files"]}
numtext = alt is not None and any("N" in a["mark"].split() for _, _, a in c["files"])
marks = " ".join(m for m in (LEVEL_MARK[level], "N" if numtext else "") if m)
out.setdefault(lib, {})[uc] = {"full": full, "alt": alt, "level": level, "marks": marks, "files": c["files"]}
return out
@@ -691,8 +702,9 @@ def tip(lib, uc_label, c, half):
if half == "alt":
seen = []
for _, e in picks:
if e["level"] != "full":
t = checks_less(lib, e["variant"])
ts = ([checks_less(lib, e["variant"])] if e["level"] != "full" else []) + \
([NUMBER_TEXT_TIPS.get(lib, "copies each number's text instead of formatting it")] if "N" in e["mark"].split() else [])
for t in ts:
if t not in seen:
seen.append(t)
lines += seen
@@ -741,14 +753,14 @@ def heatmap(platform, title):
if half == "alt":
b.append('<text class="nj-charts-cell" x="%g" y="%g" text-anchor="middle">%s</text>' % (cx, y + 16, fr(r)))
b.append('<text class="nj-charts-cell nj-charts-cell--small" x="%g" y="%g" text-anchor="middle">%s</text>'
% (cx, y + 29, LEVEL_MARK[c["level"]]))
% (cx, y + 29, c["marks"]))
else:
b.append('<text class="nj-charts-cell" x="%g" y="%g" text-anchor="middle">%s</text>' % (cx, y + ch / 2 + 4, fr(r)))
b.append('</g>')
if c["full"] is None: # no fully validating way: the left half stays empty
pass
desc.append("%s %s: %s%s" % (UC_LIBS[lib], label, fr(c["full"]) if c["full"] else "no fully validating way",
(" (%s checking less, %s)" % (fr(c["alt"]), c["level"])) if c["alt"] else ""))
(" (%s doing less, %s)" % (fr(c["alt"]), c["marks"])) if c["alt"] else ""))
# the key, set apart from the grid and narrower than its columns, so that it doesn't read as a row of it
yb = top + len(libs) * ch + 28
b.append('<text class="nj-charts-tick nj-charts-tick--l" x="8" y="%g">time relative to the fastest fully validating library, less is better</text>' % (yb + 4))