Skip to content

Allocate nothing of our own, build errors lazily, publish the benchmarks - #12

Merged
pscheid92 merged 2 commits into
mainfrom
share-options
Nov 10, 2026
Merged

pscheid92 merged 2 commits into
mainfrom
share-options

Conversation

@pscheid92

Copy link
Copy Markdown
Owner

The package now allocates nothing of its own on the JSON path, and BENCHMARKS.md shows what a duration costs
next to the standard library.

What gets cheaper

Medians of ten runs on an Apple M2 with Go 1.27.3.

Before After
With(ISO8601) 262 ns, 13 allocations 5 ns, 0 allocations
With(ISO8601, AlsoAccept(Nano)) 344 ns, 19 allocations 13 ns with AcceptDays too, 0 allocations
Format.Format 25 to 37 ns, 2 allocations 19 to 32 ns, 1 allocation (the string)
Format.Parse 29 to 68 ns, 1 allocation 20 to 60 ns, 0 allocations up to 32 bytes of input
A rejected duration, error only checked 530 ns, 10 allocations 292 ns, 5 allocations

Writing and reading a valid duration allocated nothing of ours before either; that is now pinned by tests and
measured against an int64 in the same place.

How

  • Shared options. With, Marshalers and Unmarshalers build their options once for each set of arguments
    and return the same ones afterwards. They sit in a fixed array of atomic pointers, indexed by the written
    format, the set of other accepted formats, and AcceptDays: no hashing and no lock.
  • Options of one byte. The accepted formats are a set of bits, which Go puts into an interface without
    allocating.
  • Codecs without an interface. A codec is a struct with a switch, so the calls are static and the buffers of
    Format and Parse stay on the stack.
  • Lazy errors. The error about a rejected duration, and the one about an unknown unit inside it, keep the
    input they quote and build the message when Error is called. The format names in the message are built once
    for each decoder.

What callers can see

  • The order of the formats in AlsoAccept makes no difference any more. Error messages name the written format
    first and the others in the order of Formats(), as in iso8601 or units or nano.
  • An invalid format in AlsoAccept panics in AlsoAccept itself, with the same message as before.
  • errors.Unwrap of an error about a rejected duration returns its cause, where it returned nil. errors.Is
    matches ErrInvalidDuration and the cause as before, and the messages are unchanged.

Benchmarks

bench_test.go covers every part of the API: building the options, writing and reading one duration with kept
options, with With on every call and with a wrapper type, AlsoAccept, a thousand durations, a struct with other
fields, invalid input, and Format and Parse. Each is compared with an int64 field, the standard library's
FormatDurationAsNano, the smallest possible function option, and a hand-written type with text methods.

BENCHMARKS.md reports the results, and the README has a short section that links to it. One finding is worth
knowing: in a struct with many fields, wrapper types are faster than options, because the standard library asks
for every field whether a function applies to it.

BENCHMARKS.md says that versions up to v0.3.1 built the options on every call, so it reads correctly once this
is released.

Checks

  • just check passes, with 100% statement coverage.
  • just mutate: 215 changes, a test fails for every one.
  • go vet passes for linux/386. The 32-bit tests run here in CI.

With, Marshalers and Unmarshalers build their options once for each set of arguments and share them afterwards,
so a call takes about 5 ns and allocates nothing. Before, every call built them anew: 262 ns and 13 allocations
for With(ISO8601).

The codecs are one struct with a switch instead of an interface, so the buffers of Format and Parse stay on the
stack: Format allocates only the string it returns, and Parse nothing for input of up to 32 bytes.

An error about a rejected duration builds its message when Error is called. Checking one costs 292 ns and
5 allocations where it cost 530 ns and 10, and 3 of the 5 are the standard library's.

What callers can see:

- The order of the formats in AlsoAccept makes no difference any more. Error messages name the written format
  first and the others in the order of Formats.
- An invalid format in AlsoAccept panics in AlsoAccept itself, with the same message.
- errors.Unwrap of an error about a rejected duration returns its cause, where it returned nil.

BENCHMARKS.md reports every part of the API next to four ways to do without the package, and the README links
to it.
The test compared json.Unmarshal with what the standard library allocates for an error of its own.
That count is 3 on darwin/arm64 and 4 on linux/amd64, so the test failed on Linux.
It now measures the decoder's own part, which is the same everywhere.
@pscheid92
pscheid92 merged commit 558ec8e into main Nov 10, 2026
19 checks passed
@pscheid92
pscheid92 deleted the share-options branch November 10, 2026 19:42
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