Skip to content

perf: typed columns for pine-go/pine-java ColumnFrame (issue #156) - #162

Merged
Liam0205 merged 3 commits into
masterfrom
feat/typed-columns
Jul 7, 2026
Merged

perf: typed columns for pine-go/pine-java ColumnFrame (issue #156)#162
Liam0205 merged 3 commits into
masterfrom
feat/typed-columns

Conversation

@Liam0205

@Liam0205 Liam0205 commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Closes #156.

Summary

Ports pine-cpp's Column hierarchy (include/pine/column.hpp) to pine-go and pine-java: fixed-width typed columns (float64/double, string, bool) with a validity bitmap, plus a JSON fallback for mixed/composite/present-null data. This cashes in the batch access API from PR #155 — with the per-element interface tax gone, boxing was the next bottleneck (investigation: typed scan 257ns ≈ ideal boxed scan 262ns, so typed columns only pay off with the batch API in place; both prerequisites now hold).

Design (mirrors pine-cpp, one documented divergence)

  • Construction-time inference (makeColumn / Column.build): one pass per field over the items, picking float64/string/bool columns for homogeneous fields; present-null or mixed types fall to the JSON column. Mirrors pine-cpp make_column.
  • Runtime promotion: type-mismatched or null writes into a typed column promote it to the JSON column and retry — same semantics as pine-cpp write_item_field_locked.
  • Documented divergence: typed dispatch is by exact runtime type (only float64/Double for numerics), unlike pine-cpp which folds all numerics to DoubleColumn. Rationale (in column.go's header): Go's any / Java's boxing preserve int-vs-float distinctions that downstream contracts observe (%T error messages, type-prefixed dedup keys). JSON-sourced data always arrives as float64/Double, so the common case still gets the typed path.
  • Typed fast path: OperatorInput.ItemColumnFloat64 (Go) / itemColumnDouble (Java) return the raw primitive array zero-copy when the column is typed AND fully present (so item-defaults can never fire). normalize/sort hot loops use it, skipping both boxing and per-element type assertions. Falls back to the boxed ItemColumn otherwise.
  • Removals/reorder move into the column layer: bitmap compaction and cycle-following permutation with scratch shared across columns (as in pine-cpp's ColumnStore).

pine-cpp is untouched (it already is the template).

Benchmarks (Apple M5 Pro, same-run A/B)

Benchmark row column (typed) note
e2e transform-heavy 1000 ~1.37ms ~0.89ms (-35%) bytes -51%
e2e transform-heavy 5000 ~5.6ms ~3.6ms bytes -51%
Removals micro (1000×10) 5KB/2 allocs 1KB/1 alloc was 88KB/21 allocs
New micro 672KB/4004 allocs 94KB/40 allocs was 177KB/33

Verification

  • pine-go: full test suite + -race green, golangci-lint 0 issues; new typed_column_test.go (inference / promotion / present-nil / fast-path gating / row-vs-column result parity)
  • pine-java: 256 tests green + checkstyle clean; new TypedColumnTest.java (same coverage)
  • cross-validate sections 3/4/5: 95/95 execution + column-store parity, 30/30 error parity (Go vs Java vs C++)
  • 120-round three-engine differential fuzz, 0 divergence (incl. the defaults+nil dimension that exercises promotion paths)

Port pine-cpp's Column hierarchy (include/pine/column.hpp) to the other
two engines: fixed-width typed columns (float64/double, string, bool)
with a validity bitmap, plus a JSON fallback for mixed/composite/
present-null data. Cashes in the batch access API from PR #155 — with
the per-element interface tax gone, boxing was the next bottleneck.

pine-go (internal/dataframe/column.go + column_frame.go rewrite):
- column interface + typedColumn[T] generics + jsonColumn fallback
- construction-time type inference (one pass per field, mirrors
  pine-cpp make_column); present-nil disqualifies typed storage
- runtime promotion on type-mismatched / nil writes (toJSON + retry),
  matching pine-cpp write_item_field_locked semantics
- deliberate divergence from pine-cpp, documented in column.go: typed
  dispatch is by EXACT runtime type (float64 only for numerics) —
  Go's `any` preserves int vs float64 and downstream contracts observe
  it (%T error messages, type-prefixed dedup keys); JSON-sourced data
  is always float64 so the common case still gets the typed path
- types.Float64ColumnReader optional interface +
  OperatorInput.ItemColumnFloat64: zero-copy raw []float64 window,
  ok only when fully present (defaults can never fire); normalize and
  sort hot loops use it, skipping boxing + type assertions entirely
- removals/reorder move to bitmap-compaction / cycle-following inside
  each column (scratch shared across columns)

pine-java (Column.java + ColumnFrame.java rewrite): same model —
Column.build inference, DoubleColumn/StringColumn/BoolColumn/JsonColumn,
promotion on set/append failure, Frame.itemColumnDoubleView default
method + OperatorInput.itemColumnDouble, TransformNormalize/ReorderSort
fast paths.

Benchmarks (Apple M5 Pro, high ambient load — same-run A/B):
- pine-go New/column: 33→40 allocs but bytes 177KB→94KB (typed arrays)
- Removals/column: 88KB→1KB per op, 21→1 allocs (bitmap compaction)
- e2e transform-heavy 1000: column ~0.89ms vs row ~1.37ms (~35% faster,
  bytes -51%); 5000: column ~3.6ms vs row ~5.6ms

Tests: typed inference / promotion / present-nil semantics / fast-path
gating / row-vs-column result parity on both engines (Go
typed_column_test.go, Java TypedColumnTest.java); full suites green
(go test -race + golangci-lint 0 issues; mvn 256 tests + checkstyle);
cross-validate sections 3/4/5 green (95/95 column-store parity);
120-round three-engine differential fuzz 0 divergence.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔍 PR 审查

项目 结果
结论 💬 COMMENT
审查截止 01cc9b0b839816710e80585faefd22c6c7045b13

整体质量很高:typed column 分层清晰、与 pine-cpp 语义对齐、promotion / present-null / bitmap 压缩 / cycle-following 置换都实现正确,reorder 的 gather 语义(new[i] = old[order[i]])与既有 row frame(row_frame.goreordered[newIdx] = f.items[origIdx])一致,permutation 校验也补上了。发现 1 个不影响正确性但违背本 PR 性能目标的问题。

🟠 重要建议 (1)

  • 文件: pine-java/src/main/java/page/liam/pine/operators/ReorderSort.java 代码链接
  • 问题: List<int[]> entries 被声明并在循环里填充了 nnew int[]{i},但之后再也没有被读取——实际排序用的是 Integer[] indices。这是从 pine-go 的 entry 结构翻译时残留的死代码,会在排序热路径上白白产生 n 次小数组分配。本 PR 的卖点之一正是「New micro 4004 allocs → 40 allocs」,这段无用分配与该目标直接相悖。
  • 建议: 删除 entries 的声明及其填充循环(第 48 行、第 61-63 行),只保留 indices 排序逻辑即可。

🟢 小问题 (1)

  • 文件: pine-go/internal/dataframe/column_frame.go / pine-java/.../ColumnFrame.java
  • 问题: additions Pass 2 里 promotion 后的 jsonColumn 未预先 grow(newCap),靠逐个 appendVal 增长(Go column_frame.go 的 promotion 分支、Java ColumnFrame.java 同理)。仅在「批量新增时发生类型提升」这一少见路径上多几次 realloc,不影响正确性,可按需优化。

说明:typed 快路径只在「double 列且窗口全 present」时命中,而 normalize/sort 的 ItemInput 是 strict,BuildInput 已保证无 null,因此快路径与 boxed 回退路径在这些算子上语义等价,未发现分歧。

Both verified real:

- pine-java ReorderSort: remove the dead `List<int[]> entries` — a
  translation leftover from pine-go's entry struct that allocated n
  small arrays per sort without ever being read, directly contradicting
  this PR's allocation-reduction goal.
- pine-go/pine-java additions Pass 2: pre-grow the promoted JsonColumn
  to newCap so the remaining appends in a batch-addition-with-promotion
  path don't realloc element by element.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 01cc9b0..7d4d0cb
审查截止 7d4d0cb664544526afc111b61504a43279a6147e

上一轮提出的两个问题均已正确修复,增量改动干净、无回归。

✅ 已验证修复项 (2)

1. 删除 ReorderSort 死代码 entries

  • 文件: pine-java/src/main/java/page/liam/pine/operators/ReorderSort.java 代码链接
  • List<int[]> entries 的声明及其 nnew int[]{i} 填充循环已删除,排序热路径不再产生无用小数组分配,符合本 PR 的减分配目标。
  • List / ArrayList 的 import 仍被保留但依然有效——第 70 行 List<Integer> order = new ArrayList<>(n) 仍在使用,无未用 import 问题。

2. promotion 后 JsonColumn 预扩容 grow(newCap)

  • 文件: pine-go/internal/dataframe/column_frame.go 代码链接 / pine-java/src/main/java/page/liam/pine/ColumnFrame.java 代码链接
  • 批量新增触发类型提升时,提升后的 jsonColumngrow(newCap) 再逐个 append,避免后续元素逐个 realloc。
  • 语义已确认安全:两侧 grow 都只扩容量、不改逻辑长度(Go column.gogrow 保持 len(c.data) 不变、仅提升 cap;Java JsonColumn.grow 保持 size 不变、仅扩 backing array),因此 grow(newCap) + append 行为正确。newCap 在两处 promotion 点均在作用域内。

代码良好,可合并。

Per the new convention (llmdoc updates land inside the PR, before
merge, once CI is green and review items are resolved):

- perf-evolution-roadmap: step 1 marked implemented — typed columns
  landed on pine-go/pine-java (pine-cpp was the template); records the
  deliberate exact-type-dispatch divergence rationale and the remaining
  string-arena follow-up
- column-vs-row reflection: phase-2 implementation record (results,
  fast-path gating) + two new lessons (template alignment is not
  line-by-line copying; translation-leftover dead code is a high-risk
  defect class in porting PRs)
- index.md: sync both entries
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔍 PR 增量审查

项目 结果
结论 ✅ APPROVE
审查范围 7d4d0cb..f4b1660
审查截止 f4b1660023b9553e16699793a8ba524a407b866c

本轮增量仅一个 commit(f4b1660),全部为 llmdoc 文档更新,无代码改动。文档准确记录了 typed columns 的实现与刻意分歧论证,事实核对通过:

  • pine-go/internal/dataframe/column.goItemColumnFloat64(Go)、itemColumnDouble(Java)、StringColumn[]string 底层)均与代码一致。
  • 「pine-cpp 折叠数值到 DoubleColumn vs Go/Java 精确类型派发」的刻意分歧论证归档到 reflection,符合先前 review 中确认的语义。
  • perf-evolution-roadmap 与 index 的状态更新(第一步 typed 列已实现、剩余为字符串 arena)与前两轮已合入的实现改动吻合。

前两轮代码审查提出的问题均已修复(死代码 entries 删除、promotion 后 grow(newCap) 预扩容)。文档增量干净、无回归,可合并。

@Liam0205
Liam0205 merged commit 48a6b35 into master Jul 7, 2026
21 checks passed
Liam0205 added a commit that referenced this pull request Jul 7, 2026
Both verified real:

- pine-java ReorderSort: remove the dead `List<int[]> entries` — a
  translation leftover from pine-go's entry struct that allocated n
  small arrays per sort without ever being read, directly contradicting
  this PR's allocation-reduction goal.
- pine-go/pine-java additions Pass 2: pre-grow the promoted JsonColumn
  to newCap so the remaining appends in a batch-addition-with-promotion
  path don't realloc element by element.
@Liam0205
Liam0205 deleted the feat/typed-columns branch July 7, 2026 13:51
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.

perf: typed columns + string arena for ColumnFrame — cash in the batch access API (roadmap step 1)

1 participant