Skip to content

perf: cache command string and add zero-allocation integer parsing#189

Merged
tonivade merged 3 commits intotonivade:masterfrom
fanson:perf/zero-alloc-parsing
Mar 30, 2026
Merged

perf: cache command string and add zero-allocation integer parsing#189
tonivade merged 3 commits intotonivade:masterfrom
fanson:perf/zero-alloc-parsing

Conversation

@fanson
Copy link
Copy Markdown

@fanson fanson commented Mar 23, 2026

Summary

  • Cache the command name string in DefaultRequest to avoid repeated SafeString.toString() conversions.
  • Add SafeString.parseIntAfterPrefix() for zero-allocation integer parsing of RESP protocol headers, replacing the substring(1) + Integer.parseInt() pattern in RedisParser.

Motivation

In the hot path of command processing:

  1. DefaultRequest.getCommand() is called multiple times per request (dispatch, logging, exit check). Each call previously allocated a new String via SafeString.toString().
  2. RedisParser parses every RESP frame header (*3, $5, :1) by first creating a substring (allocation), then parsing the integer. parseIntAfterPrefix() reads digits directly from the ByteBuffer — zero allocation.

Changes

  • DefaultRequest.java: lazy-cache commandString field; reuse in isExit()
  • SafeString.java: new parseIntAfterPrefix() method — parses integer from buffer position+1, handles negative numbers
  • RedisParser.java: use parseIntAfterPrefix() in parseToken(), parseIntegerToken(), and parseStringToken()

Test plan

  • Existing tests pass
  • Verify RESP parsing correctness with negative integers and edge cases

haiyang.zhou added 2 commits March 23, 2026 09:49
Two related allocation-reduction optimizations:

1. Cache the result of getCommand() in DefaultRequest to avoid
   repeated SafeString.toString() conversions. The command name is
   immutable per request, so lazy-init caching is safe.

2. Add SafeString.parseIntAfterPrefix() that parses the integer
   value directly from the underlying ByteBuffer, skipping the
   first byte (the RESP type prefix). This replaces the pattern of
   substring(1) + Integer.parseInt() in RedisParser, eliminating
   a temporary String allocation per parsed RESP header.

Made-with: Cursor
- SafeStringTest: verify parseIntAfterPrefix() for positive, negative,
  zero, and large integers with various RESP prefixes
- DefaultRequestTest: verify getCommand() returns same cached instance
  across calls, and isExit() correctly uses the cached value

Made-with: Cursor
for (int i = start; i < lim; i++) {
byte b = buffer.get(i);
if (b < '0' || b > '9') {
break;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

this should throw a NumberFormatException, right? also this case should be verified in tests

@tonivade tonivade merged commit 9857aeb into tonivade:master Mar 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants