Skip to content

Speed up struct encode/decode with typed field paths#25

Merged
kelindar merged 2 commits into
masterfrom
struct-codec
Jul 20, 2026
Merged

Speed up struct encode/decode with typed field paths#25
kelindar merged 2 commits into
masterfrom
struct-codec

Conversation

@kelindar

@kelindar kelindar commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fast-path addressable struct encode/decode for primitive fields using packed offset/kind metadata
  • Keep non-primitive fields on the existing Codec path
  • Clean up encoder tests (table-driven TestEncoder) and fold struct-slice cases into TestStruct
  • Add binary/trace-enc / binary/trace-dec benches for nested struct slices

Benchmarks

Compared struct-codec against v1.1.0 (vs v1.1.0 = delta vs tagged release):

name                 time/op      ops/s        allocs/op    vs v1.1.0
-------------------- ------------ ------------ ------------ ------------------
binary/enc           114.8 ns     8.7M         🟰 2          ✅ +7%
binary/enc-to        85.7 ns      11.7M        🟰 0          🟰 similar
binary/dec           83.4 ns      12.0M        🟰 1          ✅ +10%
binary/map-enc       8.0 µs       124.6K       🟰 209        🟰 similar
binary/map-dec       12.3 µs      81.2K        🟰 500        🟰 similar
binary/slice-enc     7.6 µs       131.9K       🟰 9          ✅ +8%
binary/slice-dec     6.2 µs       162.5K       🟰 100        ✅ +8%
binary/nest-enc      3.9 µs       253.3K       🟰 13         ✅ +8%
binary/nest-dec      3.5 µs       289.3K       🟰 63         ✅ +8%
binary/bytes-enc     848.8 ns     1.2M         🟰 3          🟰 similar
binary/bytes-dec     166.3 ns     6.0M         🟰 0          🟰 similar
binary/u64-enc       36.6 µs      27.3K        🟰 3          🟰 similar
binary/u64-dec       53.7 µs      18.6K        🟰 0          🟰 similar
binary/reuse-enc     89.4 ns      11.2M        🟰 0          🟰 similar
binary/stream-dec    130.6 ns     7.7M         🟰 1          ✅ +5%
binary/trace-enc     11.8 µs      85.0K        9            new
binary/trace-dec     12.2 µs      82.3K        640          new
nocopy/str-enc       100.8 ns     9.9M         🟰 3          🟰 similar
nocopy/str-dec       31.4 ns      31.9M        🟰 0          🟰 similar
nocopy/dict-enc      172.1 ns     5.8M         🟰 2          🟰 similar
nocopy/dict-dec      142.6 ns     7.0M         🟰 2          🟰 similar
nocopy/bmap-enc      307.1 ns     3.3M         🟰 5          🟰 similar
nocopy/bmap-dec      149.5 ns     6.7M         🟰 2          🟰 similar
nocopy/hmap-enc      300.3 ns     3.3M         🟰 5          🟰 similar
nocopy/hmap-dec      134.6 ns     7.4M         🟰 2          🟰 similar
nocopy/bytes-enc     848.7 ns     1.2M         🟰 3          🟰 similar
nocopy/bytes-dec     32.9 ns      30.4M        🟰 0          🟰 similar
nocopy/u64-enc       5.3 µs       189.4K       🟰 3          🟰 similar
nocopy/u64-dec       29.8 ns      33.5M        🟰 0          🟰 similar
nocopy/col-enc       466.5 ns     2.1M         🟰 8          🟰 similar
nocopy/col-dec       329.9 ns     3.0M         🟰 6          🟰 similar
nocopy/struct-enc    149.8 ns     6.7M         🟰 3          🟰 similar
nocopy/struct-dec    66.7 ns      15.0M        🟰 0          🟰 similar
sorted/i32-enc       76.7 µs      13.0K        🟰 5          🟰 similar
sorted/i32-dec       53.8 µs      18.6K        🟰 0          🟰 similar
sorted/u32-enc       73.1 µs      13.7K        🟰 5          🟰 similar
sorted/u32-dec       47.7 µs      20.9K        🟰 0          🟰 similar
sorted/ts-enc        47.7 µs      21.0K        🟰 6          🟰 similar
sorted/ts-dec        21.3 µs      46.9K        🟰 2          🟰 similar
sorted/tsz-enc       122.2 µs     8.2K         🟰 7          🟰 similar
sorted/tsz-dec       114.0 µs     8.8K         🟰 3          🟰 similar
sorted/tcz-enc       79.8 µs      12.5K        🟰 6          ✅ +8%
sorted/tcz-dec       73.3 µs      13.6K        🟰 3          🟰 similar
unsafe/u64-enc       479.0 ns     2.1M         🟰 3          🟰 similar
unsafe/u64-dec       452.3 ns     2.2M         🟰 2          🟰 similar

Test plan

  • go test ./... passes
  • go vet ./... is clean
  • cd bench && go run . -bench binary/trace runs cleanly

Pack field offset/kind into fieldCodec and write/read primitive struct
fields via unsafe pointers when addressable, avoiding per-field
reflect. Also tidy encoder tests and add a trace-payload bench.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
81.0%

passed

████████████████░░░░81.0% on changed lines

197 lines changed   36 uncovered   2 files

 📂 2 files changed
File Coverage Uncovered Lines
🟡 codecs.go 80.0% 342, 360, 361, 370, 371, 372, 373, 380, 381, 390 (+26 more)

| 🟢 | scanner.go | 100.0% | — |

 📋 Full diff-cover report

Diff Coverage

Diff: origin/master...HEAD, staged and unstaged changes

  • codecs.go (80.0%): Missing lines 342,360-361,370-373,380-381,390,406,421-422,428-429,459-465,494-507
  • scanner.go (100%)

Summary

  • Total: 197 lines
  • Missing: 36 lines
  • Coverage: 81%

codecs.go

  338 		base := unsafe.Pointer(rv.UnsafeAddr())
  339 		for i := range c {
  340 			field := &c[i]
  341 			if field.Field&fieldIncluded == 0 {
! 342 				continue
  343 			}
  344 			pointer := unsafe.Add(base, field.offset())
  345 			switch field.kind() {
  346 			case reflect.String:

  356 			case reflect.Int32:
  357 				e.WriteVarint(int64(*(*int32)(pointer)))
  358 			case reflect.Int64:
  359 				e.WriteVarint(*(*int64)(pointer))
! 360 			case reflect.Uint:
! 361 				e.WriteUvarint(uint64(*(*uint)(pointer)))
  362 			case reflect.Uint8:
  363 				e.WriteUvarint(uint64(*(*uint8)(pointer)))
  364 			case reflect.Uint16:
  365 				e.WriteUvarint(uint64(*(*uint16)(pointer)))

  366 			case reflect.Uint32:
  367 				e.WriteUvarint(uint64(*(*uint32)(pointer)))
  368 			case reflect.Uint64:
  369 				e.WriteUvarint(*(*uint64)(pointer))
! 370 			case reflect.Complex64:
! 371 				e.writeComplex64(*(*complex64)(pointer))
! 372 			case reflect.Complex128:
! 373 				e.writeComplex128(*(*complex128)(pointer))
  374 			case reflect.Float32:
  375 				e.WriteFloat32(*(*float32)(pointer))
  376 			case reflect.Float64:
  377 				e.WriteFloat64(*(*float64)(pointer))

  376 			case reflect.Float64:
  377 				e.WriteFloat64(*(*float64)(pointer))
  378 			default:
  379 				if err = field.Codec.EncodeTo(e, rv.Field(i)); err != nil {
! 380 					return
! 381 				}
  382 			}
  383 		}
  384 		return
  385 	}

  386 
  387 	for i := range c {
  388 		field := &c[i]
  389 		if field.Field&fieldIncluded == 0 {
! 390 			continue
  391 		}
  392 		if err = field.Codec.EncodeTo(e, rv.Field(i)); err != nil {
  393 			return
  394 		}

  402 		base := unsafe.Pointer(rv.UnsafeAddr())
  403 		for i := range c {
  404 			field := &c[i]
  405 			if field.Field&fieldWritable == 0 {
! 406 				continue
  407 			}
  408 			pointer := unsafe.Add(base, field.offset())
  409 			switch field.kind() {
  410 			case reflect.String:

  417 			case reflect.Bool:
  418 				var value bool
  419 				value, err = d.ReadBool()
  420 				if err != nil {
! 421 					return
! 422 				}
  423 				*(*bool)(pointer) = value
  424 			case reflect.Int:
  425 				var value int64
  426 				value, err = d.ReadVarint()

  424 			case reflect.Int:
  425 				var value int64
  426 				value, err = d.ReadVarint()
  427 				if err != nil {
! 428 					return
! 429 				}
  430 				*(*int)(pointer) = int(value)
  431 			case reflect.Int8:
  432 				var value int64
  433 				value, err = d.ReadVarint()

  455 				if err != nil {
  456 					return
  457 				}
  458 				*(*int64)(pointer) = value
! 459 			case reflect.Uint:
! 460 				var value uint64
! 461 				value, err = d.ReadUvarint()
! 462 				if err != nil {
! 463 					return
! 464 				}
! 465 				*(*uint)(pointer) = uint(value)
  466 			case reflect.Uint8:
  467 				var value uint64
  468 				value, err = d.ReadUvarint()
  469 				if err != nil {

  490 				if err != nil {
  491 					return
  492 				}
  493 				*(*uint64)(pointer) = value
! 494 			case reflect.Complex64:
! 495 				var value complex64
! 496 				value, err = d.readComplex64()
! 497 				if err != nil {
! 498 					return
! 499 				}
! 500 				*(*complex64)(pointer) = value
! 501 			case reflect.Complex128:
! 502 				var value complex128
! 503 				value, err = d.readComplex128()
! 504 				if err != nil {
! 505 					return
! 506 				}
! 507 				*(*complex128)(pointer) = value
  508 			case reflect.Float32:
  509 				var value float32
  510 				value, err = d.ReadFloat32()
  511 				if err != nil {

🛡️ diff-cover-action

@coveralls

coveralls commented Jul 20, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 29778670976

Coverage decreased (-1.3%) to 90.282%

Details

  • Coverage decreased (-1.3%) from the base build.
  • Patch coverage: 36 uncovered changes across 1 file (161 of 197 lines covered, 81.73%).
  • 2 coverage regressions across 1 file.

Uncovered Changes

File Changed Covered %
codecs.go 180 144 80.0%
Total (2 files) 197 161 81.73%

Coverage Regressions

2 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
codecs.go 2 88.32%

Coverage Stats

Coverage Status
Relevant Lines: 1770
Covered Lines: 1598
Line Coverage: 90.28%
Coverage Strength: 593.93 hits per line

💛 - Coveralls

@kelindar
kelindar merged commit e40235c into master Jul 20, 2026
1 check passed
@kelindar
kelindar deleted the struct-codec branch July 20, 2026 21:08
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