From a659e7cfd9c197e33b20ca3f125f4e2dd19fe386 Mon Sep 17 00:00:00 2001 From: Matteo Merli Date: Thu, 30 Jul 2026 07:59:16 +0200 Subject: [PATCH 1/2] Clear only present message fields via set-bit iteration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Union-like messages declare many singular message fields but only ever set one or two: Pulsar's BaseCommand has ~60, and its clear() walked a sequential `if (hasX()) x.clear()` guard for every one of them on every parse and every fill cycle. Flame graphs showed the guard chain as ~5-7% of self time, and removing it measured far larger — sixty correlated branches per clear() cost more in branch-prediction pressure than their profile share suggested. For messages with at least 4 singular message fields, clear() now iterates the set bits of (_bitFieldN & _MSG_FIELDS_MASKN) with a numberOfTrailingZeros loop and a dense switch on the field index, clearing only the children that are actually present: O(set message fields) instead of O(declared fields). Messages below the threshold keep the plain per-field guards and generate byte-identical code. Throughput (JMH, Apple M-series, ops/us, 2-3 forks): BaseCommand deserialize 36.1 -> 48.1 (+33%) BaseCommand serialize 25.4 -> 29.7 (+17%) MessageMetadata ser/deser unchanged (no singular message fields) --- .../generator/LightProtoMessage.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoMessage.java b/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoMessage.java index 9aa3bdf..548d1d2 100644 --- a/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoMessage.java +++ b/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoMessage.java @@ -183,13 +183,70 @@ private void generateParseFrom(PrintWriter w) { w.format(" }\n"); } + // Below this count of singular message fields, the plain per-field + // `if (hasX()) x.clear()` guards are cheaper than the set-bit loop. + private static final int BIT_DRIVEN_CLEAR_THRESHOLD = 4; + + /** + * Non-oneof singular message fields: the only fields whose clear() emission is + * presence-bit-guarded. Union-like messages (e.g. Pulsar's BaseCommand) declare + * dozens of them but only ever set one or two, so clear() iterates the set bits + * instead of walking every guard. + */ + private List bitGuardedMessageFields() { + List result = new ArrayList<>(); + for (LightProtoField f : fields) { + if (f instanceof LightProtoMessageField && !f.isOneofMember()) { + result.add(f); + } + } + return result; + } + + private boolean useBitDrivenClear() { + return bitGuardedMessageFields().size() >= BIT_DRIVEN_CLEAR_THRESHOLD; + } + private void generateClear(PrintWriter w) { w.println(" /** Reset all fields to their default values, allowing this instance to be reused. */"); w.format(" public %s clear() {\n", message.getName()); + boolean bitDriven = useBitDrivenClear(); for (LightProtoField f : fields) { + if (bitDriven && f instanceof LightProtoMessageField && !f.isOneofMember()) { + continue; // handled by the set-bit loop below + } f.clear(w); } + if (bitDriven) { + // Clear only the message fields that are actually present: iterate the + // set bits of each word restricted to message-field masks, instead of + // testing every field's has() guard sequentially. + List msgFields = bitGuardedMessageFields(); + for (int word = 0; word < bitFieldsCount(); word++) { + final int wordIdx = word; + List inWord = msgFields.stream() + .filter(f -> f.index() / 32 == wordIdx) + .collect(Collectors.toList()); + if (inWord.isEmpty()) { + continue; + } + w.format(" int _mBits%d = _bitField%d & _MSG_FIELDS_MASK%d;\n", word, word, word); + w.format(" while (_mBits%d != 0) {\n", word); + w.format(" int _mIdx = Integer.numberOfTrailingZeros(_mBits%d);\n", word); + w.format(" _mBits%d &= _mBits%d - 1;\n", word, word); + w.format(" switch (_mIdx) {\n"); + for (LightProtoField f : inWord) { + w.format(" case %d:\n", f.index() % 32); + // The presence bit implies the child was allocated + w.format(" %s.clear();\n", f.ccName); + w.format(" break;\n"); + } + w.format(" }\n"); + w.format(" }\n"); + } + } + w.format(" _parsedBuffer = null;\n"); w.format(" _cachedSize = -1;\n"); for (int i = 0; i < bitFieldsCount(); i++) { @@ -516,6 +573,15 @@ private void generateBitFields(PrintWriter w) { }); w.println(";"); } + if (useBitDrivenClear()) { + w.format("private static final int _MSG_FIELDS_MASK%d = 0", i); + bitGuardedMessageFields().forEach(f -> { + if (f.index() / 32 == idx) { + w.format(" | %s", f.fieldMask()); + } + }); + w.println(";"); + } } } From ebb34ef01f789eb276b1fca34867aaf553c87e55 Mon Sep 17 00:00:00 2001 From: Matteo Merli Date: Thu, 30 Jul 2026 08:11:31 +0200 Subject: [PATCH 2/2] Iterate presence bits in writeTo() and getSerializedSize() as well Same principle as the clear() change: union-like messages set only one or two of their many fields, yet serialization and size computation walked every field's has() guard. For messages where all fields are singular and non-oneof (so every field carries a presence bit), _writeTo() and getSerializedSize() now iterate the set bits per word. Set bits ascend, so fields are emitted in declaration order and the output bytes are identical to the guard walk; proto3 implicit-presence fields keep their non-default check inside the case, and required fields always have their bit set once checkRequiredFields() has passed. Messages with repeated, map or oneof fields keep the guard walk (ordering across unbitted fields could not be preserved otherwise), as do messages below an 8-field threshold. Throughput (JMH, Apple M-series, ops/us, 2 forks, same-session baseline on top of the bit-driven clear() commit): BaseCommand serialize 28.8 -> 43.4 (+50%) BaseCommand deserialize unchanged (52.3 +- 1.9) MessageMetadata ser/deser unchanged (controls) --- .../generator/LightProtoMessage.java | 115 +++++++++++++++--- 1 file changed, 97 insertions(+), 18 deletions(-) diff --git a/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoMessage.java b/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoMessage.java index 548d1d2..7367b1e 100644 --- a/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoMessage.java +++ b/code-generator/src/main/java/io/streamnative/lightproto/generator/LightProtoMessage.java @@ -207,6 +207,70 @@ private boolean useBitDrivenClear() { return bitGuardedMessageFields().size() >= BIT_DRIVEN_CLEAR_THRESHOLD; } + // Below this field count the plain guard walk is cheaper than the loop. + private static final int BIT_DRIVEN_TRAVERSAL_THRESHOLD = 8; + + /** + * Whether serialization and size computation can iterate presence bits instead + * of walking every field's guard. Requires every field to carry a presence bit + * (no repeated, map or oneof fields): set-bit iteration yields ascending field + * indexes, so output order — and therefore the emitted bytes — stay identical + * to the declaration-order guard walk only when all fields participate. + */ + private boolean useBitDrivenTraversal() { + if (fields.size() < BIT_DRIVEN_TRAVERSAL_THRESHOLD) { + return false; + } + for (LightProtoField f : fields) { + if (f.isRepeated() || f.isOneofMember()) { + return false; + } + } + return true; + } + + /** + * Residual per-field condition once the presence bit is implied by the set-bit + * loop: proto3 implicit-presence fields still skip default values; everything + * else serializes unconditionally when present. + */ + private static String residualSerializeCondition(LightProtoField f) { + return f.field.hasImplicitPresence() ? f.nonDefaultCondition("") : null; + } + + /** Emit the per-word set-bit loops, delegating each field's body to the consumer. */ + private void emitBitDrivenTraversal(PrintWriter w, java.util.function.Consumer body) { + for (int word = 0; word < bitFieldsCount(); word++) { + final int wordIdx = word; + List inWord = fields.stream() + .filter(f -> f.index() / 32 == wordIdx) + .collect(Collectors.toList()); + if (inWord.isEmpty()) { + continue; + } + w.format(" int _fBits%d = _bitField%d;\n", word, word); + w.format(" while (_fBits%d != 0) {\n", word); + w.format(" int _fIdx = Integer.numberOfTrailingZeros(_fBits%d);\n", word); + w.format(" _fBits%d &= _fBits%d - 1;\n", word, word); + w.format(" switch (_fIdx) {\n"); + for (LightProtoField f : inWord) { + w.format(" case %d: {\n", f.index() % 32); + String residual = residualSerializeCondition(f); + if (residual != null) { + w.format(" if (%s) {\n", residual); + body.accept(f); + w.format(" }\n"); + } else { + body.accept(f); + } + w.format(" break;\n"); + w.format(" }\n"); + } + w.format(" }\n"); + w.format(" }\n"); + } + } + private void generateClear(PrintWriter w) { w.println(" /** Reset all fields to their default values, allowing this instance to be reused. */"); w.format(" public %s clear() {\n", message.getName()); @@ -327,14 +391,22 @@ private void generateSerialize(PrintWriter w) { if (hasRequiredFields()) { w.format(" checkRequiredFields();\n"); } - for (LightProtoField f : fields) { - String condition = f.serializeCondition(); - if (condition != null) { - w.format(" if (%s) {\n", condition); - f.serialize(w); - w.format(" }\n"); - } else { - f.serialize(w); + if (useBitDrivenTraversal()) { + // Iterate only the fields that are present instead of testing every + // guard: set bits ascend, so the output order (and bytes) match the + // declaration-order guard walk. Required fields always have their bit + // set here — checkRequiredFields() has already thrown otherwise. + emitBitDrivenTraversal(w, f -> f.serialize(w)); + } else { + for (LightProtoField f : fields) { + String condition = f.serializeCondition(); + if (condition != null) { + w.format(" if (%s) {\n", condition); + f.serialize(w); + w.format(" }\n"); + } else { + f.serialize(w); + } } } @@ -351,16 +423,23 @@ private void generateGetSerializedSize(PrintWriter w) { w.format("\n"); w.format(" int _size = 0;\n"); - fields.forEach(field -> { - String condition = field.serializeCondition(); - if (condition != null) { - w.format(" if (%s) {\n", condition); - field.serializedSize(w); - w.format(" }\n"); - } else { - field.serializedSize(w); - } - }); + if (useBitDrivenTraversal()) { + // Same set-bit traversal as _writeTo: the two must agree exactly on + // which fields contribute, since writeTo() writes into an array sized + // by this method. + emitBitDrivenTraversal(w, f -> f.serializedSize(w)); + } else { + fields.forEach(field -> { + String condition = field.serializeCondition(); + if (condition != null) { + w.format(" if (%s) {\n", condition); + field.serializedSize(w); + w.format(" }\n"); + } else { + field.serializedSize(w); + } + }); + } w.format(" _cachedSize = _size;\n"); w.format(" return _size;\n");