Skip to content

Render the begin statement once per standard option combination - #2653

Open
timofurrer wants to merge 1 commit into
jackc:masterfrom
timofurrer:begin-sql-no-alloc
Open

timofurrer wants to merge 1 commit into
jackc:masterfrom
timofurrer:begin-sql-no-alloc

Conversation

@timofurrer

Copy link
Copy Markdown
Contributor

TxOptions.beginSQL builds the begin statement with a strings.Builder on every call, so every Conn.BeginTx, Batch.BeginTx, pgxpool.Pool.BeginTx and stdlib BeginTx pays a 64 byte allocation for a statement that never changes for a given set of options. Only the default TxOptions{} avoided it, through the emptyTxOptions fast path.

What this changes

The statements for every combination of the standard isolation levels, access modes and deferrable modes (5 x 3 x 3) are rendered once at package initialization into a small array, and beginSQL looks the statement up by the position of each option value. That covers every transaction begun with the exported constants. Non-standard values, such as a custom TxIsoLevel string, still render on demand exactly as before, and BeginQuery still overrides everything.

emptyTxOptions is gone: the default options are the [0][0][0] entry.

Benchmark

BenchmarkTxOptionsBeginSQL is new in tx_internal_test.go. goos: darwin, goarch: arm64, Apple M1 Pro, -count 3:

options before after
default 8.3 ns/op, 0 allocs 7.7 ns/op, 0 allocs
isolation level 25.0 ns/op, 1 alloc (64 B) 10.0 ns/op, 0 allocs
every option 32.5 ns/op, 1 alloc (64 B) 10.6 ns/op, 0 allocs
custom isolation level 25.0 ns/op, 1 alloc (64 B) 29.5 ns/op, 1 alloc (64 B)

The custom-value path is about 4 ns slower because it scans the three option lists before falling back to rendering. Per transaction this is noise compared with the round trip, but the allocation it removes is on the path of every transaction a pgx application begins.

Testing

TestTxOptionsBeginSQL pins the rendered statement for representative options, including the BeginQuery override and non-standard values. TestTxOptionsBeginSQLEveryStandardCombination checks the lookup against rendering for all 45 combinations, so a table indexing mistake cannot pass. The root package, pgxpool and stdlib suites pass against PostgreSQL 17.

AI disclosure

Per CONTRIBUTING.md: this change was developed with AI assistance (Claude Code, Fable 5.1), including the implementation, the tests and the benchmark. I understand the change and can answer questions about it.

TxOptions.beginSQL built the statement with a strings.Builder on every
call, so every Conn.BeginTx and Batch.BeginTx paid a 64 byte allocation
for options whose statement never changes. The statements for all
combinations of the standard isolation levels, access modes and
deferrable modes are now rendered once at package initialization and
looked up by option value. Non-standard values and BeginQuery behave as
before.

goos: darwin, goarch: arm64, Apple M1 Pro:

  BeginSQL/default                 8.3ns  0 allocs  ->   7.7ns  0 allocs
  BeginSQL/isolation_level        25.0ns  1 allocs  ->  10.0ns  0 allocs
  BeginSQL/every_option           32.5ns  1 allocs  ->  10.6ns  0 allocs
  BeginSQL/custom_isolation_level 25.0ns  1 allocs  ->  29.5ns  1 allocs
@timofurrer
timofurrer marked this pull request as ready for review September 13, 2026 10:14
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.

1 participant