Skip to content

Expr::Function as a Box#2271

Draft
xitep wants to merge 3 commits intoapache:mainfrom
xitep:function-box
Draft

Expr::Function as a Box#2271
xitep wants to merge 3 commits intoapache:mainfrom
xitep:function-box

Conversation

@xitep
Copy link
Contributor

@xitep xitep commented Mar 6, 2026

  • The whole change is just making Expr::Function(Function) a Expr::Function(Box<Function>) thereby reducing the size of Expr from 328 down to 216 bytes.
  • This appears to be slightly improving the performance of the parser:
Details
# on branch: `box-function`
> cargo bench -- --baseline main
...
sqlparser-rs parsing benchmark/sqlparser::select
                        time:   [2.7318 µs 2.7401 µs 2.7489 µs]
                        change: [−8.0290% −7.4339% −6.8525%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 1 outliers among 100 measurements (1.00%)
  1 (1.00%) high mild
sqlparser-rs parsing benchmark/sqlparser::with_select
                        time:   [14.875 µs 14.930 µs 14.995 µs]
                        change: [−5.0708% −4.3247% −3.5655%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 3 outliers among 100 measurements (3.00%)
  3 (3.00%) high severe
sqlparser-rs parsing benchmark/parse_large_statement
                        time:   [4.7099 ms 4.7206 ms 4.7313 ms]
                        change: [−9.8526% −9.3668% −8.8572%] (p = 0.00 < 0.05)
                        Performance has improved.
sqlparser-rs parsing benchmark/format_large_statement
                        time:   [268.01 µs 268.67 µs 269.37 µs]
                        change: [−1.2684% −0.8555% −0.3824%] (p = 0.00 < 0.05)
                        Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
  2 (2.00%) high severe

word_to_ident/clone_into_ident_100x
                        time:   [1.8360 µs 1.8463 µs 1.8572 µs]
                        change: [+2.2347% +2.8474% +3.5024%] (p = 0.00 < 0.05)
                        Performance has regressed.
word_to_ident/to_ident_100x
                        time:   [1.5207 µs 1.5263 µs 1.5319 µs]
                        change: [+2.5991% +3.0039% +3.4031%] (p = 0.00 < 0.05)
                        Performance has regressed.
Found 3 outliers among 100 measurements (3.00%)
  2 (2.00%) high mild
  1 (1.00%) high severe

parse_identifiers/select_100_columns
                        time:   [71.498 µs 72.063 µs 72.674 µs]
                        change: [−6.9631% −6.2297% −5.5577%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
  6 (6.00%) high mild
  6 (6.00%) high severe
parse_identifiers/select_100_qualified_columns
                        time:   [151.78 µs 152.03 µs 152.30 µs]
                        change: [−5.3267% −4.6015% −3.4939%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 4 outliers among 100 measurements (4.00%)
  1 (1.00%) high mild
  3 (3.00%) high severe
  • I was a bit surprised, to see this change actually improve a client program (which itself i cannot share, unfortunately):
Details
# ./sqlfmt.sqlparser-main ... built against sqlparser's `main` branch (#64f4b1f)
# ./sqlfmt.sqlparser-function-box ... built ... well against the `function-box` branch proposed with this PR (#f8e9e18)


> hyperfine -N './sqlfmt.sqlparser-main /tmp/a.sql' './sqlfmt.sqlparser-function-box /tmp/a.sql'
Benchmark 1: ./sqlfmt.sqlparser-main /tmp/a.sql
  Time (mean ± σ):     373.3 ms ±   2.1 ms    [User: 278.6 ms, System: 94.4 ms]
  Range (min … max):   370.2 ms … 377.9 ms    10 runs

Benchmark 2: ./sqlfmt.sqlparser-function-box /tmp/a.sql
  Time (mean ± σ):     345.7 ms ±   1.5 ms    [User: 254.3 ms, System: 91.2 ms]
  Range (min … max):   343.2 ms … 348.4 ms    10 runs

Summary
  ./sqlfmt.sqlparser-function-box /tmp/a.sql ran
    1.08 ± 0.01 times faster than ./sqlfmt.sqlparser-main /tmp/a.sql


> hyperfine -N './sqlfmt.sqlparser-main --optimize-column-aliases /tmp/a.sql' './sqlfmt.sqlparser-function-box --optimize-column-aliases /tmp/a.sql'
Benchmark 1: ./sqlfmt.sqlparser-main --optimize-column-aliases /tmp/a.sql
  Time (mean ± σ):     411.9 ms ±   7.6 ms    [User: 311.3 ms, System: 100.3 ms]
  Range (min … max):   405.7 ms … 430.7 ms    10 runs

Benchmark 2: ./sqlfmt.sqlparser-function-box --optimize-column-aliases /tmp/a.sql
  Time (mean ± σ):     381.2 ms ±   2.8 ms    [User: 292.0 ms, System: 88.9 ms]
  Range (min … max):   377.8 ms … 386.4 ms    10 runs

Summary
  ./sqlfmt.sqlparser-function-box --optimize-column-aliases /tmp/a.sql ran
    1.08 ± 0.02 times faster than ./sqlfmt.sqlparser-main --optimize-column-aliases /tmp/a.sql


# the input file (/tmp/a.sql) is some ~50k lines of sql (approx 3.1mb) with some "real world" monster
# select (~ 800lines) from an oracle apex application; i repeated approx 100 times to make up those
# 50k lines.

# the "--optimize-column-aliases" causes the program to traverse the AST a several times
#      and allocate quite a lot of string, in other words it's inefficient, but that's the only different.
#     Notably the absolute speed up is - more or less - the same for both versions, indicating
#    a real win through the boxing in sqlparser's `Expr::Function`.
  • My theory here is that by reducing the size of the enum CPU caches get better utilised manifesting in faster performance (at least when reading the AST.)
  • Expr itself, could further practically be reduced to about 80-100 bytes, though i'm not able to tell how much improvement that could contribute. (i've marked some candidates with XXX in this pr.)
  • Other enums/structs, chiefly Statement, could be considered for this kind of change. there are really huge differences in sizes of the individual enum variants, leading Parser::parse (and related) to return needlessly large memory chunks (on average, of course.) Expr seemed very much prevalent in the AST, so i suppose this would be the biggest win / lowest hanging fruit.
  • The biggest issue with this change are tests, though; we cannot pattern match on a Box is Rust, making it a pain in the neck to (re)write them without some additional helper functions (in this PR i just went straight to make it work without a lot of thought.)
  • I would be glad if somebody could confirm or refute my theory. or maybe just run this change on some other machines. maybe @iffyio, @LucaCappelletti94, or @eyalleshem if you'd be interested?
  • I'm keeping this as a draft for now, since my primary intention is first to trigger a discussion on how the API could (or not) evolve.

@xitep xitep changed the title Expr:Function as a Box Expr::Function as a Box Mar 6, 2026
@LucaCappelletti94
Copy link
Contributor

If there isn't rush, I can come back with benchmarks in parsing with and without this change against my fuzzing corpus. I am a bit overworked right now, so I don't think I can deliver it earlier than the end of next week.

@xitep
Copy link
Contributor Author

xitep commented Mar 6, 2026

absolutely no rush, @LucaCappelletti94. (this was just an experiment which surprised me and i thought it's worth sharing.) if you'd manage to run it on some workload of yours it would be a great way to expose or exclude some error on my part.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants