commit ceba9572ef5fc3a4c7e2e3f315d05842d2422d0c (tree)
parent 854774d468b0a439def7eef73c544ac8d64f1b62
Author: Nico Elbers <nico.b.elbers@gmail.com>
Date: Mon, 3 Nov 2025 12:05:57 +0100
Optimize Writer.writeLeb128
Rewrite `writeLeb128` to no longer use `writeMultipleOf7Leb128` and instead:
* Make use of byte aligned ints
* Special case small numbers (fitting inside 7 bits)
Amongst u8, u16, u32 and u64 performance gains are between ~1.5x and ~2x
Amongst i8, i16, i32 ane i64 perfromance gains are between ~2x and >4x
Additinally add test coverage for written encodings
Microbenchmark: https://zigbin.io/7ed5fe
Diffstat:
| M | lib/std/Io/Writer.zig | | | 173 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------- |
1 file changed, 150 insertions(+), 23 deletions(-)
diff --git a/lib/std/Io/Writer.zig b/lib/std/Io/Writer.zig
@@ -1911,35 +1911,162 @@ pub fn writeSleb128(w: *Writer, value: anytype) Error!void {
/// Write a single integer as LEB128 to the given writer.
pub fn writeLeb128(w: *Writer, value: anytype) Error!void {
- const value_info = @typeInfo(@TypeOf(value)).int;
- try w.writeMultipleOf7Leb128(@as(@Int(
- value_info.signedness,
- @max(std.mem.alignForwardAnyAlign(u16, value_info.bits, 7), 7),
- ), value));
-}
+ const T = @TypeOf(value);
+ const info = switch (@typeInfo(T)) {
+ .int => |info| info,
+ else => @compileError(@typeName(T) ++ " not supported"),
+ };
-fn writeMultipleOf7Leb128(w: *Writer, value: anytype) Error!void {
- const value_info = @typeInfo(@TypeOf(value)).int;
- const Byte = packed struct(u8) { bits: u7, more: bool };
- var bytes: [@divExact(value_info.bits, 7)]Byte = undefined;
- var remaining = value;
- for (&bytes, 1..) |*byte, len| {
- const more = switch (value_info.signedness) {
- .signed => remaining >> 6 != remaining >> (value_info.bits - 1),
- .unsigned => remaining > std.math.maxInt(u7),
+ const BoundInt = @Int(info.signedness, 7);
+ if (info.bits <= 7 or (value >= std.math.minInt(BoundInt) and value <= std.math.maxInt(BoundInt))) {
+ const Bits = @Int(info.signedness, 8);
+ const byte = switch (info.signedness) {
+ .signed => @as(Bits, @intCast(value)) & 0x7F,
+ .unsigned => @as(Bits, @intCast(value)),
};
- byte.* = .{
- .bits = @bitCast(@as(
- @Int(value_info.signedness, 7),
- @truncate(remaining),
- )),
- .more = more,
+ try w.writeByte(@bitCast(byte));
+ return;
+ }
+
+ const Byte = packed struct { bits: u7, more: bool };
+ const Int = std.math.ByteAlignedInt(T);
+
+ const max_bytes = @divFloor(info.bits - 1, 7) + 1;
+
+ const sign_value = value >> (info.bits - 1);
+ var val: Int = value;
+ for (0..max_bytes) |_| {
+ const more = switch (info.signedness) {
+ .signed => val >> 6 != sign_value,
+ .unsigned => val > std.math.maxInt(u7),
};
- if (value_info.bits > 7) remaining >>= 7;
- if (!more) return w.writeAll(@ptrCast(bytes[0..len]));
+
+ try w.writeByte(@bitCast(@as(Byte, .{
+ .bits = @intCast(val & 0x7F),
+ .more = more,
+ })));
+
+ if (!more) return;
+
+ val >>= 7;
} else unreachable;
}
+test "serialize signed LEB128" {
+ // Small values
+ try testLeb128Encoding(i7, 9, "\x09");
+ try testLeb128Encoding(i64, 125, "\xFD\x00");
+
+ try testLeb128Encoding(i7, -34, "\x5E");
+ try testLeb128Encoding(i64, -3, "\x7D");
+
+ // Random values
+ try testLeb128Encoding(i16, 19373, "\xAD\x97\x01");
+ try testLeb128Encoding(i32, 1628839242, "\xCA\xBA\xD8\x88\x06");
+ try testLeb128Encoding(i64, 3789169920125966546, "\xD2\xB1\xD0\xD5\xF6\xBE\xF5\xCA\x34");
+ try testLeb128Encoding(i128, 704622239050934257305893323522763588, "\xC4\xD6\x83\xC7\xE3\x91\x95\xC3\x96\x80\x8D\xA5\xF5\xDF\xA3\xDA\x87\x01");
+
+ try testLeb128Encoding(i16, -14558, "\xA2\x8E\x7F");
+ try testLeb128Encoding(i32, -1702738165, "\x8B\x8E\x89\xD4\x79");
+ try testLeb128Encoding(i64, -1709126996960612298, "\xB6\xE0\x87\xB1\xD3\xC1\xFD\xA3\x68");
+ try testLeb128Encoding(i128, -113498719181566012704681230050325944039, "\x99\xD2\x80\xBC\xE6\x95\xBC\xC8\xDE\xB4\x9D\x81\x9F\xCA\xC6\xF8\x9C\xD5\x7E");
+
+ // {min,max} values
+ try testLeb128Encoding(i16, std.math.maxInt(i16), "\xFF\xFF\x01");
+ try testLeb128Encoding(i32, std.math.maxInt(i32), "\xFF\xFF\xFF\xFF\x07");
+ try testLeb128Encoding(i64, std.math.maxInt(i64), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00");
+ try testLeb128Encoding(i128, std.math.maxInt(i128), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01");
+
+ try testLeb128Encoding(i16, std.math.minInt(i16), "\x80\x80\x7E");
+ try testLeb128Encoding(i32, std.math.minInt(i32), "\x80\x80\x80\x80\x78");
+ try testLeb128Encoding(i64, std.math.minInt(i64), "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x7F");
+ try testLeb128Encoding(i128, std.math.minInt(i128), "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x80\x7E");
+
+ // Specific cases
+ try testLeb128Encoding(i0, 0, "\x00");
+ try testLeb128Encoding(i8, 0, "\x00");
+
+ try testLeb128Encoding(i2, -1, "\x7F");
+ try testLeb128Encoding(i8, -1, "\x7F");
+
+ try testLeb128Encoding(i2, 1, "\x01");
+ try testLeb128Encoding(i8, 1, "\x01");
+
+ // Encode byte boundaries
+ try testLeb128Encoding(i7, std.math.maxInt(i7), "\x3F");
+ try testLeb128Encoding(i8, std.math.maxInt(i7) + 1, "\xC0\x00");
+ try testLeb128Encoding(i14, std.math.maxInt(i14), "\xFF\x3F");
+ try testLeb128Encoding(i15, std.math.maxInt(i14) + 1, "\x80\xC0\x00");
+ try testLeb128Encoding(i49, std.math.maxInt(i49), "\xFF\xFF\xFF\xFF\xFF\xFF\x3F");
+ try testLeb128Encoding(i50, std.math.maxInt(i49) + 1, "\x80\x80\x80\x80\x80\x80\xC0\x00");
+ try testLeb128Encoding(i56, std.math.maxInt(i56), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F");
+ try testLeb128Encoding(i57, std.math.maxInt(i56) + 1, "\x80\x80\x80\x80\x80\x80\x80\xC0\x00");
+ try testLeb128Encoding(i63, std.math.maxInt(i63), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F");
+ try testLeb128Encoding(i64, std.math.maxInt(i63) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\xC0\x00");
+
+ try testLeb128Encoding(i7, std.math.minInt(i7), "\x40");
+ try testLeb128Encoding(i8, std.math.minInt(i7) - 1, "\xBF\x7F");
+ try testLeb128Encoding(i14, std.math.minInt(i14), "\x80\x40");
+ try testLeb128Encoding(i15, std.math.minInt(i14) - 1, "\xFF\xBF\x7F");
+ try testLeb128Encoding(i49, std.math.minInt(i49), "\x80\x80\x80\x80\x80\x80\x40");
+ try testLeb128Encoding(i50, std.math.minInt(i49) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
+ try testLeb128Encoding(i56, std.math.minInt(i56), "\x80\x80\x80\x80\x80\x80\x80\x40");
+ try testLeb128Encoding(i57, std.math.minInt(i56) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
+ try testLeb128Encoding(i63, std.math.minInt(i63), "\x80\x80\x80\x80\x80\x80\x80\x80\x40");
+ try testLeb128Encoding(i64, std.math.minInt(i63) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
+}
+
+test "serialize unsigned LEB128" {
+ // Small values
+ try testLeb128Encoding(u7, 12, "\x0C");
+ try testLeb128Encoding(u64, 201, "\xC9\x01");
+
+ // Random values
+ try testLeb128Encoding(u8, 254, "\xFE\x01");
+ try testLeb128Encoding(u16, 30241, "\xA1\xEC\x01");
+ try testLeb128Encoding(u32, 2173531193, "\xB9\xE8\xB5\x8C\x08");
+ try testLeb128Encoding(u64, 18321125691115744902, "\x86\xDD\xF2\x81\xF2\xD7\xED\xA0\xFE\x01");
+ try testLeb128Encoding(u128, 122619209508942982841456325819614676193, "\xE1\x89\xF3\xD9\xE3\xAD\xEC\xF4\x98\x95\xF8\xBB\xD7\xB8\xF2\xCC\xBF\xB8\x01");
+
+ // Max values
+ try testLeb128Encoding(u8, std.math.maxInt(u8), "\xFF\x01");
+ try testLeb128Encoding(u16, std.math.maxInt(u16), "\xFF\xFF\x03");
+ try testLeb128Encoding(u32, std.math.maxInt(u32), "\xFF\xFF\xFF\xFF\x0F");
+ try testLeb128Encoding(u64, std.math.maxInt(u64), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01");
+ try testLeb128Encoding(u128, std.math.maxInt(u128), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x03");
+
+ // Specific cases
+ try testLeb128Encoding(u0, 0, "\x00");
+ try testLeb128Encoding(u1, 0, "\x00");
+ try testLeb128Encoding(u8, 0, "\x00");
+
+ try testLeb128Encoding(u1, 1, "\x01");
+ try testLeb128Encoding(u8, 1, "\x01");
+
+ // Encode byte boundaries
+ try testLeb128Encoding(u7, std.math.maxInt(u7), "\x7F");
+ try testLeb128Encoding(u8, std.math.maxInt(u7) + 1, "\x80\x01");
+ try testLeb128Encoding(u14, std.math.maxInt(u14), "\xFF\x7F");
+ try testLeb128Encoding(u15, std.math.maxInt(u14) + 1, "\x80\x80\x01");
+ try testLeb128Encoding(u49, std.math.maxInt(u49), "\xFF\xFF\xFF\xFF\xFF\xFF\x7F");
+ try testLeb128Encoding(u50, std.math.maxInt(u49) + 1, "\x80\x80\x80\x80\x80\x80\x80\x01");
+ try testLeb128Encoding(u56, std.math.maxInt(u56), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F");
+ try testLeb128Encoding(u57, std.math.maxInt(u56) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\x01");
+ try testLeb128Encoding(u63, std.math.maxInt(u63), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F");
+ try testLeb128Encoding(u64, std.math.maxInt(u63) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01");
+}
+
+fn testLeb128Encoding(comptime T: type, value: T, encoding: []const u8) !void {
+ const info = @typeInfo(T).int;
+ const max_bytes = @divFloor(info.bits -| 1, 7) + 1;
+ var bytes: [max_bytes]u8 = undefined;
+
+ var fw: Writer = .fixed(&bytes);
+ try writeLeb128(&fw, value);
+
+ try std.testing.expectEqualSlices(u8, encoding, fw.buffered());
+}
+
test "printValue max_depth" {
const Vec2 = struct {
const SelfType = @This();