Skip to content

Parser asymptotic study: Nornic vs ANTLR ​

Machine: darwin/arm64, 14 CPUs, go1.27.1. Sizes n = [8, 16, 32, 64, 128, 256, 512].

Each family grows one query dimension n. Cost is fit as y = c * tokens^k on a log-log scale; k is the growth exponent (1 = linear, 2 = quadratic) and R² is fit quality. Fits use the largest 4 sizes, where per-call constant overhead no longer flattens the slope. Allocation measurements describe this build and workload; timing also depends on the machine. Tokens come from the ANTLR lexer, used as a common input-size measure for both parsers.

Growth exponents, mode = parse ​

familyparserallocs kbytes ktime kallocs growth
return_itemsnornic0.98 (R²=1.000)0.99 (R²=1.000)0.98 (R²=1.000)~O(n)
return_itemsantlr0.81 (R²=0.997)0.83 (R²=0.998)0.81 (R²=0.997)~O(n)
where_andnornic0.00 (R²=1.000)0.85 (R²=0.994)0.90 (R²=1.000)O(1)
where_andantlr0.98 (R²=1.000)0.99 (R²=1.000)0.98 (R²=1.000)~O(n)
arith_chainnornic0.09 (R²=0.980)0.91 (R²=0.991)1.03 (R²=0.999)O(1)
arith_chainantlr0.89 (R²=0.999)0.90 (R²=0.999)1.11 (R²=0.992)~O(n)
list_literalnornic1.02 (R²=0.975)1.04 (R²=1.000)1.05 (R²=0.999)~O(n)
list_literalantlr0.65 (R²=0.989)0.66 (R²=0.988)0.66 (R²=0.992)~O(n)
map_literalnornic1.13 (R²=0.996)1.05 (R²=0.999)1.05 (R²=1.000)~O(n)
map_literalantlr0.71 (R²=0.993)0.72 (R²=0.993)0.72 (R²=0.993)~O(n)
match_hopsnornic0.90 (R²=0.999)1.03 (R²=1.000)1.24 (R²=1.000)~O(n)
match_hopsantlr0.79 (R²=0.996)0.81 (R²=0.995)0.78 (R²=0.996)~O(n)
with_chainnornic0.99 (R²=1.000)1.02 (R²=1.000)1.07 (R²=0.999)~O(n)
with_chainantlr0.80 (R²=0.997)0.82 (R²=0.997)0.82 (R²=0.997)~O(n)
case_branchesnornic0.14 (R²=0.992)1.17 (R²=0.995)1.03 (R²=0.999)O(1)
case_branchesantlr0.83 (R²=0.998)0.83 (R²=0.998)0.83 (R²=0.996)~O(n)
nested_parensnornic0.07 (R²=0.999)0.68 (R²=0.998)0.85 (R²=0.997)O(1)
nested_parensantlr0.63 (R²=0.989)0.63 (R²=0.986)0.64 (R²=0.986)~O(n)

Growth exponents, mode = validate ​

familyparserallocs kbytes ktime kallocs growth
return_itemsnornic0 allocs (constant)0 allocs (constant)1.00 (R²=0.999)O(1)
return_itemsantlr0.99 (R²=1.000)1.02 (R²=1.000)1.00 (R²=1.000)~O(n)
where_andnornic0 allocs (constant)0 allocs (constant)1.08 (R²=0.999)O(1)
where_andantlr0.99 (R²=1.000)1.01 (R²=1.000)0.99 (R²=1.000)~O(n)
arith_chainnornic0 allocs (constant)0 allocs (constant)1.05 (R²=1.000)O(1)
arith_chainantlr0.98 (R²=1.000)1.01 (R²=1.000)1.68 (R²=0.997)~O(n)
list_literalnornic0 allocs (constant)0 allocs (constant)1.04 (R²=0.999)O(1)
list_literalantlr0.99 (R²=1.000)1.00 (R²=1.000)1.03 (R²=0.999)~O(n)
map_literalnornic0 allocs (constant)0 allocs (constant)1.04 (R²=1.000)O(1)
map_literalantlr0.99 (R²=1.000)1.00 (R²=1.000)0.99 (R²=1.000)~O(n)
match_hopsnornic0 allocs (constant)0 allocs (constant)0.99 (R²=1.000)O(1)
match_hopsantlr0.99 (R²=1.000)1.02 (R²=0.999)0.99 (R²=1.000)~O(n)
with_chainnornic0 allocs (constant)0 allocs (constant)1.02 (R²=1.000)O(1)
with_chainantlr0.99 (R²=1.000)1.02 (R²=1.000)1.01 (R²=1.000)~O(n)
case_branchesnornic0 allocs (constant)0 allocs (constant)1.02 (R²=1.000)O(1)
case_branchesantlr1.00 (R²=1.000)1.00 (R²=1.000)1.03 (R²=0.999)~O(n)
nested_parensnornic0 allocs (constant)0 allocs (constant)0.92 (R²=0.999)O(1)
nested_parensantlr0.99 (R²=1.000)1.01 (R²=1.000)1.01 (R²=0.999)~O(n)

Cost at the largest size (parse mode) ​

familytokensNornic allocsANTLR allocsratioNornic BANTLR BNornic timeANTLR timeratio
return_items46164,13630,3457x319kB2.44MB139.5µs1489.4µs11x
where_and513142248,3035912x41.7kB21.2MB37.0µs10839.0µs293x
arith_chain20472489,3793724x15.4kB7.3MB22.0µs6662.6µs303x
list_literal153881022,47628x222kB1.78MB64.0µs1076.0µs17x
map_literal30741,74629,13017x210kB2.32MB125.6µs1422.8µs11x
match_hops51291,61627,80317x350kB2.21MB341.4µs1250.8µs4x
with_chain41055,68329,3395x585kB2.39MB249.9µs1551.5µs6x
case_branches615329104,8543616x72kB8.22MB90.1µs4560.1µs51x
nested_parens10272220,928951x6.42kB1.55MB7.7µs1023.4µs133x

parse allocs per op

parse bytes per op

parse ns per op

validate allocs per op

validate bytes per op

validate ns per op

Tail latency and GC (parse mode, n=32, fixed input) ​

inputparserp50 µsp95 µsp99 µsmax µsmax/p50GCsGC pause ms
return_items/n=32nornic8.018.942.816020x601.93
return_items/n=32antlr180.9402.5566.5301817x92937.82
where_and/n=32nornic3.15.010.118359x150.50
where_and/n=32antlr718.11231.31442.0961413x4353190.12
arith_chain/n=32nornic1.52.15.57753x40.20
arith_chain/n=32antlr357.7727.8840.0402511x211773.96
list_literal/n=32nornic2.98.420.810135x361.17
list_literal/n=32antlr205.3438.9577.2235011x111440.66
map_literal/n=32nornic6.613.527.413020x371.21
map_literal/n=32antlr228.8486.1665.711035x121245.03
match_hops/n=32nornic12.021.248.117815x622.00
match_hops/n=32antlr164.0362.2466.415179x90831.16
with_chain/n=32nornic11.528.967.318216x1013.33
with_chain/n=32antlr180.2384.2523.69475x92330.64
case_branches/n=32nornic5.67.011.720938x120.37
case_branches/n=32antlr510.8941.01020.2566311x3033102.30
nested_parens/n=32nornic0.81.12.34054x30.09
nested_parens/n=32antlr199.2424.4561.2196110x106737.50

How to read this, and its limits ​

  • What this proves. The exponent k and the allocation counts are properties of the algorithm and its memory behaviour, not of this CPU. A parser whose allocs/op grow as tokens^1 and whose constant is 30x lower is cheaper on any hardware. ns/op is reported but is the weakest evidence.
  • What this does not prove. Fits are empirical over n = 8..max, not a proof of asymptotic class. Confirm with a derivation from the algorithm (e.g. each token consumed once, no backtracking) before claiming O(n) in print.
  • The parsers do different amounts of work. nornic parse (ASTBuilder.Build) is a clause splitter plus string-based clause parsing, and nornic validate is a set of scanners. ANTLR runs a full grammar (ALL(*)) and, in parse mode, builds a complete parse tree. A big ratio partly reflects "less work", not only "better implementation". The fair statement is the cost of getting a usable structure out of the query, plus the exponents. Check Clauses and error rejection parity before claiming equivalence.
  • Caches. The Nornic validate path memoises per-executor; the harness clears it before each call. ANTLR's DFA cache stays warm (as in a long-running server), which favours ANTLR.
  • Scope. This is parse time only. End-to-end query latency (storage, planning, execution) is dominated by other costs, so parser results should not be presented as database speedups.
Analytics privacy