Skip to content

Replace exception-driven associative array key normalization with a non-throwing scan - #561

Open
bertysentry wants to merge 4 commits into
mainfrom
559-performance-replace-exception-driven-associative-array-key-normalization-with-a-non-throwing-scan
Open

Replace exception-driven associative array key normalization with a non-throwing scan#561
bertysentry wants to merge 4 commits into
mainfrom
559-performance-replace-exception-driven-associative-array-key-normalization-with-a-non-throwing-scan

Conversation

@bertysentry

Copy link
Copy Markdown
Contributor

Fixes #559

Problem

AssocArray.toLongKey normalized every associative-array key by calling Long.parseLong inside a try/catch. For any non-integer key — every multidimensional arr[x,y] key (SUBSEP-joined) and every plain string key — this allocated a NumberFormatException and filled in its stack trace (~600 ns per call, growing with Java stack depth). This ran on every get, put, and in test; arr[k]++ paid it twice.

Fix

  • toLongKey is now a hand-rolled, non-throwing scan mirroring Long.parseLong(String) (radix 10) exactly: optional leading +/-, leading zeros, negative accumulation so Long.MIN_VALUE parses, overflow guards at the range boundaries, and Character.digit so non-ASCII digits (Arabic-Indic, fullwidth, …) keep behaving exactly as before.
  • AssocArray.isIn had the same exception-driven pattern inline; it now reuses toLongKey.

Semantics

Identical by construction, and locked in by a new unit test (AssocArrayToLongKeyTest): targeted boundary cases plus an exhaustive parity check asserting toLongKey accepts/rejects/produces exactly what Long.parseLong does across ~50 samples (signs, whitespace, leading zeros, Long.MIN_VALUE/MAX_VALUE ±1, >19-digit strings, SUBSEP keys, Unicode digits). No entry in behavior-changes.md since user-visible behavior is unchanged.

Verification

mvn clean verify: 765 unit tests pass, checkstyle/pmd/spotbugs clean. Compatibility-test results unchanged from the pre-existing baseline.

🤖 Generated with Claude Code

…on-throwing scan

AssocArray.toLongKey normalized every key by calling Long.parseLong
inside a try/catch, allocating a NumberFormatException (with full
stack-trace fill) for every non-integer key: every multidimensional
arr[x,y] key and every plain string key, on every get/put/in.

Replace it with a hand-rolled scan that accepts and rejects exactly
the same inputs as Long.parseLong (radix 10) without ever throwing:
same handling of leading +/-, leading zeros, overflow at the Long
range boundaries, and non-ASCII digits via Character.digit. Also
reuse toLongKey in AssocArray.isIn, which had the same
exception-driven pattern.

Semantics are unchanged; a parity unit test asserts identical
accept/reject/value behavior against Long.parseLong across boundary
and Unicode-digit samples.

Fixes #559

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f63cecf7b8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

try {
return Long.parseLong(key.toString());
} catch (Exception e) { // NOPMD - EmptyCatchBlock: intentionally ignored
final String str = key.toString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve nonnumeric keys whose toString throws

When an embedding or extension uses an associative array as a key in HashAssocArray, this direct key.toString() call propagates the intentional AwkRuntimeException from HashAssocArray.toString() instead of treating the key as nonnumeric. The previous implementation caught that exception and allowed put, get, remove, and isIn to operate on the object key; AssocArray.mapString() also explicitly supports associative arrays as keys. Catch exceptions from the conversion itself, while retaining the non-throwing scan for ordinary strings.

Useful? React with 👍 / 👎.

An AssocArray used as a key (allowed for extensions) throws
AwkRuntimeException from toString(); the previous catch-all swallowed
that, so guard the conversion itself while keeping the digit scan
non-throwing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex please review again

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b146b30332

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return null;
}
}
final int len = str.length();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat null toString results as nonnumeric

When an embedding uses an object key whose toString() returns null, the old Long.parseLong(key.toString()) path caught the resulting NumberFormatException and treated the key as nonnumeric, but this dereference now throws a NullPointerException. Consequently, HashAssocArray can no longer put, get, or remove that otherwise valid object key; check str for null before scanning it.

Useful? React with 👍 / 👎.

A contract-violating toString() returning null previously fell into
the Long.parseLong(null) NumberFormatException path and was treated
as non-numeric; keep that behavior with an explicit null check
(suppressing the SpotBugs redundant-nullcheck false positive).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex please review again

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Unknown error
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 2382c485fc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Measures AssocArray.toLongKey across key shapes, HashAssocArray
get/put with string keys, and the former exception-driven
Long.parseLong implementation as an in-run baseline.

Measured on JDK 21 / Windows 11: non-numeric keys drop from
~1100 ns/op (exception-driven) to ~2 ns/op; a 5M-iteration
string-keyed arr[k]++ AWK workload runs 2.7x faster end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bertysentry

Copy link
Copy Markdown
Contributor Author

@codex review

@bertysentry

Copy link
Copy Markdown
Contributor Author

JMH results for the new AssocArrayKeyBenchmark (JDK 21, Windows 11, avgt, 2 forks):

Benchmark ns/op
baselineParseLongNumeric (old impl) 17.8 ± 1.8
baselineParseLongWordString (old impl) 1100.2 ± 272.7
baselineParseLongSubsep (old impl) 1096.8 ± 134.5
toLongKeyNumeric (new) 20.5 ± 2.5
toLongKeyWordString (new) 2.0 ± 0.1
toLongKeySubsep (new) 2.1 ± 0.3
toLongKeyShortString (new) 2.1 ± 0.3
toLongKeyDigitPrefix (new) 7.1 ± 0.9
toLongKeyOverflowDigits (new) 28.9 ± 4.5
hashGetWordString (new) 2.3 ± 0.3
hashGetSubsep (new) 2.2 ± 0.3
hashPutWordString (new) 7.0 ± 3.4
hashPutSubsep (new) 6.4 ± 1.1

Non-numeric key normalization drops from ~1100 ns to ~2 ns per call (~550×); numeric keys stay in the same range (17.8 → 20.5 ns).

End-to-end AWK workload (5M iterations of c["key" (i%1000)]++ plus d[i%1000,"x"]++), main jar vs this branch, 3 alternating runs:

round main this PR
1 13,037 ms 4,813 ms
2 13,606 ms 5,041 ms
3 13,638 ms 4,885 ms

~2.7× faster on string-keyed array workloads.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: 21d995845f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

Performance: replace exception-driven associative array key normalization with a non-throwing scan

1 participant