Skip to content

Loop fission: split ASCII fast path from multi-byte encoding in writeStringSegmentAsUTF8 #66

Description

@stalep

Background

Richard Startin's loop fission article demonstrates that splitting loops with multiple distinct operations into separate loops allows C2 to optimize each loop more aggressively — vectorization, unrolling, and better register allocation. C2 does not perform loop fission automatically.

Current code

BytOutput.writeStringSegmentAsUTF8() fuses ASCII detection with byte writing in a single loop:

private void writeStringSegmentAsUTF8(String s, int start, int end) {
    int segLen = end - start;
    ensureCapacity(segLen * 3);
    for (int i = start; i < end; i++) {
        char c = s.charAt(i);
        if (c < 0x80) {
            buf[pos++] = (byte) c;           // ASCII fast path
        } else if (c < 0x800) {
            buf[pos++] = (byte) (0xC0 | (c >> 6));    // 2-byte UTF-8
            buf[pos++] = (byte) (0x80 | (c & 0x3F));
        } else if (Character.isHighSurrogate(c) && i + 1 < end) {
            // 4-byte UTF-8 surrogate pair
            ...
        } else {
            // 3-byte UTF-8
            ...
        }
    }
}

The multi-byte UTF-8 branches prevent C2 from vectorizing or aggressively unrolling the ASCII copy, even though for h5m's production data (93% strings, almost entirely ASCII), the branch is never taken for most strings.

Proposed optimization

Split into an ASCII scan + bulk copy, with a fallback for non-ASCII:

private void writeStringSegmentAsUTF8(String s, int start, int end) {
    ensureCapacity((end - start) * 3);
    // Scan for the end of the ASCII prefix
    int asciiEnd = start;
    while (asciiEnd < end && s.charAt(asciiEnd) < 0x80) asciiEnd++;
    // Bulk copy the ASCII prefix — tight loop with no branches
    for (int i = start; i < asciiEnd; i++) {
        buf[pos++] = (byte) s.charAt(i);
    }
    // Handle remaining non-ASCII characters (if any)
    if (asciiEnd < end) {
        writeStringSegmentAsUTF8Full(s, asciiEnd, end);
    }
}

The ASCII copy loop (buf[pos++] = (byte) s.charAt(i)) has no branches in the body — C2 can unroll it aggressively and potentially vectorize the copy. The multi-byte encoding is moved to a separate method that C2 optimizes independently.

Why this helps

From the Startin article: a fused loop "advances at the pace of the slowest operation." The multi-byte UTF-8 branches are the slowest operation — they prevent C2 from treating the loop as a simple copy. Splitting them out lets the ASCII loop run at full speed.

For h5m's production data:

  • 93% of JSON values are strings
  • Strings are almost entirely ASCII (field names, URLs, timestamps, metric names)
  • Each string's writeStringSegmentAsUTF8 call would use the branchless ASCII loop
  • Only strings with non-ASCII content (rare) fall through to the multi-byte path

What's already fissioned in jjq

jjq already applies loop fission in several places:

  • needsEscaping() scans for escape chars, then escapeJson() does the escaping — two separate loops
  • SWAR string scanning (parseStringBytes) scans for quote/backslash in bulk, then processes escapes separately
  • SWAR whitespace skipping scans 8 bytes at a time, then falls back to scalar

This change would extend the fission pattern to the UTF-8 encoding path.

Expected impact

Low-medium. The existing serialization is already 60% faster than Jackson on 14MB production data. The benefit would be most visible for workloads with many short ASCII strings where the per-string writeStringSegmentAsUTF8 overhead is proportionally larger.

Should benchmark before and after to verify. The serialization path is not the primary bottleneck for h5m (parsing and query execution dominate).

Reference

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions