Skip to content

Commit 45b2732

Browse files
committed
perf(msgpack): optimize parser with lookup table and fast path
- Introduce precomputed marker lookup table for O(1) conversions - Add readSimpleTypeFast to handle simple types without allocation - Extract complex parsing into readComplex for code organization - Optimize main loop to skip re-reading first marker
1 parent 330056d commit 45b2732

File tree

1 file changed

+110
-42
lines changed

1 file changed

+110
-42
lines changed

src/msgpack.zig

Lines changed: 110 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1245,46 +1245,59 @@ pub fn PackWithLimits(
12451245
return val;
12461246
}
12471247

1248+
/// Precomputed lookup table for marker byte to Markers enum conversion
1249+
/// This eliminates branch misprediction overhead from switch statements
1250+
const MARKER_LOOKUP_TABLE: [256]Markers = blk: {
1251+
var table: [256]Markers = undefined;
1252+
var i: usize = 0;
1253+
while (i < 256) : (i += 1) {
1254+
const byte: u8 = @intCast(i);
1255+
table[i] = switch (byte) {
1256+
0x00...0x7f => .POSITIVE_FIXINT,
1257+
0x80...0x8f => .FIXMAP,
1258+
0x90...0x9f => .FIXARRAY,
1259+
0xa0...0xbf => .FIXSTR,
1260+
0xc0 => .NIL,
1261+
0xc1 => .NIL, // Reserved byte, treat as NIL
1262+
0xc2 => .FALSE,
1263+
0xc3 => .TRUE,
1264+
0xc4 => .BIN8,
1265+
0xc5 => .BIN16,
1266+
0xc6 => .BIN32,
1267+
0xc7 => .EXT8,
1268+
0xc8 => .EXT16,
1269+
0xc9 => .EXT32,
1270+
0xca => .FLOAT32,
1271+
0xcb => .FLOAT64,
1272+
0xcc => .UINT8,
1273+
0xcd => .UINT16,
1274+
0xce => .UINT32,
1275+
0xcf => .UINT64,
1276+
0xd0 => .INT8,
1277+
0xd1 => .INT16,
1278+
0xd2 => .INT32,
1279+
0xd3 => .INT64,
1280+
0xd4 => .FIXEXT1,
1281+
0xd5 => .FIXEXT2,
1282+
0xd6 => .FIXEXT4,
1283+
0xd7 => .FIXEXT8,
1284+
0xd8 => .FIXEXT16,
1285+
0xd9 => .STR8,
1286+
0xda => .STR16,
1287+
0xdb => .STR32,
1288+
0xdc => .ARRAY16,
1289+
0xdd => .ARRAY32,
1290+
0xde => .MAP16,
1291+
0xdf => .MAP32,
1292+
0xe0...0xff => .NEGATIVE_FIXINT,
1293+
};
1294+
}
1295+
break :blk table;
1296+
};
1297+
1298+
/// Fast marker type lookup using precomputed table (O(1) with no branches)
12481299
inline fn markerU8To(_: Self, marker_u8: u8) Markers {
1249-
return switch (marker_u8) {
1250-
0x00...0x7f => .POSITIVE_FIXINT,
1251-
0x80...0x8f => .FIXMAP,
1252-
0x90...0x9f => .FIXARRAY,
1253-
0xa0...0xbf => .FIXSTR,
1254-
0xc0 => .NIL,
1255-
0xc1 => .NIL, // Reserved byte, treat as NIL
1256-
0xc2 => .FALSE,
1257-
0xc3 => .TRUE,
1258-
0xc4 => .BIN8,
1259-
0xc5 => .BIN16,
1260-
0xc6 => .BIN32,
1261-
0xc7 => .EXT8,
1262-
0xc8 => .EXT16,
1263-
0xc9 => .EXT32,
1264-
0xca => .FLOAT32,
1265-
0xcb => .FLOAT64,
1266-
0xcc => .UINT8,
1267-
0xcd => .UINT16,
1268-
0xce => .UINT32,
1269-
0xcf => .UINT64,
1270-
0xd0 => .INT8,
1271-
0xd1 => .INT16,
1272-
0xd2 => .INT32,
1273-
0xd3 => .INT64,
1274-
0xd4 => .FIXEXT1,
1275-
0xd5 => .FIXEXT2,
1276-
0xd6 => .FIXEXT4,
1277-
0xd7 => .FIXEXT8,
1278-
0xd8 => .FIXEXT16,
1279-
0xd9 => .STR8,
1280-
0xda => .STR16,
1281-
0xdb => .STR32,
1282-
0xdc => .ARRAY16,
1283-
0xdd => .ARRAY32,
1284-
0xde => .MAP16,
1285-
0xdf => .MAP32,
1286-
0xe0...0xff => .NEGATIVE_FIXINT,
1287-
};
1300+
return MARKER_LOOKUP_TABLE[marker_u8];
12881301
}
12891302

12901303
fn readTypeMarker(self: Self) !Markers {
@@ -1805,9 +1818,55 @@ pub fn PackWithLimits(
18051818

18061819
// ========== End of State Machine Helpers ==========
18071820

1821+
/// Fast path for simple types that don't require heap allocation or complex state management
1822+
inline fn readSimpleTypeFast(self: Self, marker: Markers, marker_u8: u8) !?Payload {
1823+
return switch (marker) {
1824+
.NIL => Payload{ .nil = void{} },
1825+
.TRUE => Payload{ .bool = true },
1826+
.FALSE => Payload{ .bool = false },
1827+
1828+
.POSITIVE_FIXINT => Payload{ .uint = marker_u8 },
1829+
.NEGATIVE_FIXINT => Payload{ .int = @as(i8, @bitCast(marker_u8)) },
1830+
1831+
.UINT8 => Payload{ .uint = try self.readV8Value() },
1832+
.UINT16 => Payload{ .uint = try self.readU16Value() },
1833+
.UINT32 => Payload{ .uint = try self.readU32Value() },
1834+
.UINT64 => Payload{ .uint = try self.readU64Value() },
1835+
1836+
.INT8 => Payload{ .int = try self.readI8Value() },
1837+
.INT16 => Payload{ .int = try self.readI16Value() },
1838+
.INT32 => Payload{ .int = try self.readI32Value() },
1839+
.INT64 => Payload{ .int = try self.readI64Value() },
1840+
1841+
.FLOAT32 => Payload{ .float = try self.readF32Value() },
1842+
.FLOAT64 => Payload{ .float = try self.readF64Value() },
1843+
1844+
// Note: FIXEXT4/FIXEXT8 could be timestamps, but we need to read ext_type first
1845+
// Since we can't "unread" in the stream, we handle all EXT types in the complex path
1846+
// to avoid consuming bytes that need to be re-processed.
1847+
1848+
else => null, // Not a simple type, needs complex handling
1849+
};
1850+
}
1851+
18081852
/// read a payload, please use payload.free to free the memory
18091853
/// This is an iterative implementation that avoids stack overflow from deep nesting
18101854
pub fn read(self: Self, allocator: Allocator) !Payload {
1855+
// Fast path optimization: handle simple types without state machine overhead
1856+
const first_marker_u8 = try self.readTypeMarkerU8();
1857+
const first_marker = self.markerU8To(first_marker_u8);
1858+
1859+
// Try fast path for simple types (no containers, no allocation needed)
1860+
if (try self.readSimpleTypeFast(first_marker, first_marker_u8)) |simple_payload| {
1861+
return simple_payload;
1862+
}
1863+
1864+
// Complex types: use full iterative parser
1865+
return self.readComplex(allocator, first_marker, first_marker_u8);
1866+
}
1867+
1868+
/// Internal iterative parser for complex types (arrays, maps, strings, etc.)
1869+
fn readComplex(self: Self, allocator: Allocator, first_marker: Markers, first_marker_u8: u8) !Payload {
18111870
// Explicit stack for iterative parsing (on heap)
18121871
var parse_stack = if (current_zig.minor == 14)
18131872
std.ArrayList(ParseState).init(allocator)
@@ -1818,17 +1877,26 @@ pub fn PackWithLimits(
18181877
// Root payload to return
18191878
var root: ?Payload = null;
18201879

1880+
// Start with the already-read first marker
1881+
var marker_u8 = first_marker_u8;
1882+
var marker = first_marker;
1883+
18211884
// Main loop (replaces recursion)
1885+
// Process first marker directly, then read subsequent markers in loop
1886+
var is_first = true;
18221887
while (true) {
18231888
// Check depth limit
18241889
if (parse_stack.items.len >= parse_limits.max_depth) {
18251890
cleanupParseStack(&parse_stack, allocator);
18261891
return MsgPackError.MaxDepthExceeded;
18271892
}
18281893

1829-
// Read type marker
1830-
const marker_u8 = try self.readTypeMarkerU8();
1831-
const marker = self.markerU8To(marker_u8);
1894+
// Read next type marker (skip on first iteration)
1895+
if (!is_first) {
1896+
marker_u8 = try self.readTypeMarkerU8();
1897+
marker = self.markerU8To(marker_u8);
1898+
}
1899+
is_first = false;
18321900

18331901
// Current payload being constructed
18341902
var current_payload: Payload = undefined;

0 commit comments

Comments
 (0)