Skip to content

perf: validate actor path elements without String.charAt - #3542

Open
pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:actorpath-no-charat
Open

pjfanning wants to merge 1 commit into
apache:mainfrom
pjfanning:actorpath-no-charat

Conversation

@pjfanning

Copy link
Copy Markdown
Member

Motivation

ActorPath.findInvalidPathElementCharPosition runs for every named actorOf. It scanned the name with String.charAt inside a pattern match with guards, plus a ValidSymbols.indexOf for every non-alphanumeric character.

String.charAt inlines to isLatin1() ? StringLatin1.charAt : StringUTF16.charAt, and that branch is profiled once, JVM-wide, in String.charAt's own bytecode. Any non-ASCII string handled anywhere in the process (a Jackson payload, a log line) pollutes that profile, after which C2 compiles both coders into every charAt loop. Measured locally, the validator halves in throughput once the profile is polluted.

Modification

  • Copy the element out once with getBytes(ISO_8859_1) — an intrinsic array copy for Latin-1 strings — and scan the byte[] with a 128-entry flag table (valid-char bit, hex-digit bit) instead of calling charAt per character. Characters outside Latin-1 encode as ? and 0x80–0xFF as negative bytes; both are invalid, so the accepted set and the reported position are unchanged.
  • Add directional ActorPathSpec tests for accepted names, rejected names and the reported error position, including Latin-1, non-Latin-1 and surrogate-pair input.
  • Extend ActorPathValidationBenchmark with a polluted param that pre-warms String.charAt with UTF-16 strings, and keep the previous charAt-based validator there as charAtLoop* for comparison.

Result

Name validation no longer depends on the String.charAt coder profile. Local JMH (-f 1, short run, noisy machine — the trend is what matters):

Benchmark polluted=false polluted=true
charAtLoopActor_1 (previous code) 101 ops/µs 49 ops/µs
handLoopActor_1 (this PR) 139 ops/µs 162 ops/µs
charAtLoop7000 (previous code) 0.197 ops/µs 0.107 ops/µs
handLoop7000 (this PR) 0.176 ops/µs 0.142 ops/µs

I tried the same treatment on the other hot charAt users, MurmurHash.stringHash and Helpers.base64, and measured no benefit (hash arithmetic and StringBuilder.append dominate; toCharArray only adds an allocation), so those are left as they are.

Tests

  • sbt "actor-tests/testOnly org.apache.pekko.actor.ActorPathSpec org.apache.pekko.actor.LocalActorRefProviderSpec" — 25 passed
  • sbt "actor-tests/testOnly org.apache.pekko.routing.ConsistentHashingRouterSpec" — passed
  • sbt "actor/mimaReportBinaryIssues" — no issues
  • sbt "bench-jmh/Jmh/compile" and Jmh/run of ActorPathValidationBenchmark
  • scalafmt on changed Scala files, git diff --check

References

None - performance follow-up to the hand-written actor name validator

Motivation:
ActorPath.findInvalidPathElementCharPosition runs for every named
actorOf and scanned the name with String.charAt plus a pattern match
with guards and a ValidSymbols.indexOf per non-alphanumeric character.
String.charAt inlines to `isLatin1() ? StringLatin1.charAt :
StringUTF16.charAt`, and that branch is profiled once, JVM-wide, in
String.charAt's own bytecode. Any non-ASCII string handled anywhere in
the process pollutes it, after which C2 compiles both coders into every
charAt loop. Measured locally, the validator halves in throughput
(101 -> 49 ops/us for "actor-1") once the profile is polluted.

Modification:
- Copy the element out once with getBytes(ISO_8859_1) (an intrinsic
  array copy for Latin-1 strings) and scan the byte[] with a 128-entry
  flag table (valid char / hex digit) instead of calling charAt per
  character. Characters outside Latin-1 encode as '?' and 0x80-0xFF as
  negative bytes, both invalid, so accepted set and reported position
  are unchanged.
- Add directional ActorPathSpec tests for accepted names, rejected
  names and the reported position, including Latin-1, non-Latin-1 and
  surrogate-pair input.
- Extend ActorPathValidationBenchmark with a `polluted` param that
  pre-warms String.charAt with UTF-16 strings, and keep the previous
  charAt-based validator there as charAtLoop* for comparison.

Result:
Name validation no longer depends on the String.charAt coder profile:
138-162 ops/us for "actor-1" polluted or not, versus 101 unpolluted and
49 polluted before. Also tried the same for MurmurHash.stringHash and
Helpers.base64 and found no benefit (hash arithmetic and
StringBuilder.append dominate), so those are unchanged.

Tests:
- sbt "actor-tests/testOnly org.apache.pekko.actor.ActorPathSpec org.apache.pekko.actor.LocalActorRefProviderSpec" (25 passed)
- sbt "actor-tests/testOnly org.apache.pekko.routing.ConsistentHashingRouterSpec" (earlier run, passed)
- sbt "actor/mimaReportBinaryIssues" (no issues)
- sbt "bench-jmh/Jmh/compile" and Jmh/run of the pollution benchmark
- scalafmt on changed Scala files, git diff --check

References:
None - performance follow-up to the hand-written actor name validator
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