Skip to content

perf(pg-protocol): decode short ascii values without buffer.toString() - #3773

Open
nigrosimone wants to merge 1 commit into
brianc:masterfrom
nigrosimone:perf-decode-short-values
Open

nigrosimone wants to merge 1 commit into
brianc:masterfrom
nigrosimone:perf-decode-short-values

Conversation

@nigrosimone

Copy link
Copy Markdown
Contributor

Every field of every row goes through BufferReader.string(), which calls buffer.toString('utf-8'). That call has a fixed cost in the C++ boundary that is bigger than the decoding itself when the value is short, and most values in a result set are short: ids, flags, small numbers, dates.

So for values up to 16 bytes that are all ASCII the string is now built with String.fromCharCode. Anything longer, or with a byte above 127, still goes to buffer.toString().

Parser only, 500 rows per message, median of 5 runs on node 26 (ms, lower is better):

5 short columns (id, fk, bool, int, 'ok'):   25.1 ->  8.3
5 columns with dates and codes:              28.4 -> 10.4
8 mixed columns (names, emails, timestamp):  39.1 -> 24.2
4 long text columns (37-80 chars):           22.2 -> 22.8
3 short non ascii columns:                   27.0 -> 27.6

End to end on localhost, SELECT of 5000 rows, 5 short columns: 9.1ms -> 6.7ms per query.

The test in inbound-parser.test.ts parses a data row with a value of every length from 0 to 19 plus accented, chinese and emoji values, and fails if the ascii check is removed.

@brianc

brianc commented Sep 18, 2026

Copy link
Copy Markdown
Owner

oh hay! interested why you closed this? It actually seems like a pretty clever & nice performance improvement - particularly on things that are pretty common (short column values) in production workflows ⁉️

@nigrosimone nigrosimone reopened this Sep 18, 2026
@nigrosimone

Copy link
Copy Markdown
Contributor Author

My benchmark was producing noisy results; I'm retrying with the CI one

@nigrosimone

Copy link
Copy Markdown
Contributor Author

Benchmark: a2b5943 against 064d69f

Scenario Base q/s Head q/s Head / base Rounds Noise
select 1 row, all types, prepared 7733 7709 1.000x (-0.0%) 0.96 to 1.13 ±7.0%
select 100 rows, all types, prepared 🏆 1141 1279 1.121x (+12.1%) 1.09 to 1.13 ±2.6%
select 500 rows, all types, prepared 🏆 276 313 1.136x (+13.6%) 1.13 to 1.15 ±2.3%
select 500 rows, simple types, prepared 🏆 1602 2131 1.335x (+33.5%) 1.31 to 1.36 ±6.5%
select 500 rows, simple types, array mode 🏆 1489 1980 1.329x (+32.9%) 1.31 to 1.33 ±4.1%
select 500 rows, simple types, binary 🏆 832 887 1.069x (+6.9%) 1.06 to 1.08 ±2.4%
select by id, parameterized 5523 5310 0.975x (-2.5%) 0.95 to 0.99 ±6.7%
insert, all types, returning 4162 4237 1.019x (+1.9%) 0.98 to 1.05 ±4.8%
update, parameterized 6107 5922 0.971x (-2.9%) 0.93 to 1.01 ±4.6%
delete, parameterized 7174 7237 1.001x (+0.1%) 0.95 to 1.04 ±6.9%
transaction, 3 queries 11972 12085 0.976x (-2.4%) 0.96 to 1.07 ±7.0%
pool query, select 1 row 6812 6902 1.024x (+2.4%) 0.94 to 1.09 ±8.5%
cursor, 500 rows in 5 reads 🏆 212 232 1.097x (+9.7%) 1.08 to 1.13 ±2.2%
query stream, 500 rows 🏆 213 236 1.105x (+10.5%) 1.10 to 1.12 ±2.4%
select 100 rows, all types, pipelined x10 🏆 1210 1341 1.107x (+10.7%) 1.10 to 1.12 ±4.9%

4 rounds of 3s per scenario, each round measuring base, head and a second process of each one after the other, in an order that changes every round. "Head / base" is the median of the per-round ratios and "Rounds" their range. "Noise" is how far base/base and head/head, the same code on both sides, got from 1 in this same run: that is what the machine did, so a row is marked only when the median moved further than that, at least 2%, and every round moved the same way: 👀 slower, 🏆 faster. Only the ratio is comparable across runs, the absolute q/s depend on the runner.

Node v26.9.0, AMD EPYC 7763 64-Core Processor, 4 cores.

@nigrosimone

Copy link
Copy Markdown
Contributor Author

@brianc It actually seems like a good result!

@nigrosimone
nigrosimone marked this pull request as ready for review September 18, 2026 17:14
@nigrosimone

Copy link
Copy Markdown
Contributor Author

Results from the CI benchmark, rebased on master. Short values are common so the row heavy scenarios gain the most, the single row ones stay inside the noise.

Benchmark: a2b5943 against 064d69f

Scenario Base q/s Head q/s Head / base Rounds Noise
select 1 row, all types, prepared 7733 7709 1.000x (-0.0%) 0.96 to 1.13 ±7.0%
select 100 rows, all types, prepared 🏆 1141 1279 1.121x (+12.1%) 1.09 to 1.13 ±2.6%
select 500 rows, all types, prepared 🏆 276 313 1.136x (+13.6%) 1.13 to 1.15 ±2.3%
select 500 rows, simple types, prepared 🏆 1602 2131 1.335x (+33.5%) 1.31 to 1.36 ±6.5%
select 500 rows, simple types, array mode 🏆 1489 1980 1.329x (+32.9%) 1.31 to 1.33 ±4.1%
select 500 rows, simple types, binary 🏆 832 887 1.069x (+6.9%) 1.06 to 1.08 ±2.4%
select by id, parameterized 5523 5310 0.975x (-2.5%) 0.95 to 0.99 ±6.7%
insert, all types, returning 4162 4237 1.019x (+1.9%) 0.98 to 1.05 ±4.8%
update, parameterized 6107 5922 0.971x (-2.9%) 0.93 to 1.01 ±4.6%
delete, parameterized 7174 7237 1.001x (+0.1%) 0.95 to 1.04 ±6.9%
transaction, 3 queries 11972 12085 0.976x (-2.4%) 0.96 to 1.07 ±7.0%
pool query, select 1 row 6812 6902 1.024x (+2.4%) 0.94 to 1.09 ±8.5%
cursor, 500 rows in 5 reads 🏆 212 232 1.097x (+9.7%) 1.08 to 1.13 ±2.2%
query stream, 500 rows 🏆 213 236 1.105x (+10.5%) 1.10 to 1.12 ±2.4%
select 100 rows, all types, pipelined x10 🏆 1210 1341 1.107x (+10.7%) 1.10 to 1.12 ±4.9%

4 rounds of 3s per scenario, each round measuring base, head and a second process of each one after the other, in an order that changes every round. "Head / base" is the median of the per-round ratios and "Rounds" their range. "Noise" is how far base/base and head/head, the same code on both sides, got from 1 in this same run: that is what the machine did, so a row is marked only when the median moved further than that, at least 2%, and every round moved the same way: 👀 slower, 🏆 faster. Only the ratio is comparable across runs, the absolute q/s depend on the runner.

Node v26.9.0, AMD EPYC 7763 64-Core Processor, 4 cores.

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