Skip to content

Commit 64e22b7

Browse files
committed
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
1 parent 416bf1d commit 64e22b7

File tree

1 file changed

+149
-23
lines changed

1 file changed

+149
-23
lines changed

lib/std/Io/Writer.zig

Lines changed: 149 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,35 +1901,161 @@ pub fn writeSleb128(w: *Writer, value: anytype) Error!void {
19011901

19021902
/// Write a single integer as LEB128 to the given writer.
19031903
pub fn writeLeb128(w: *Writer, value: anytype) Error!void {
1904-
const value_info = @typeInfo(@TypeOf(value)).int;
1905-
try w.writeMultipleOf7Leb128(@as(@Type(.{ .int = .{
1906-
.signedness = value_info.signedness,
1907-
.bits = @max(std.mem.alignForwardAnyAlign(u16, value_info.bits, 7), 7),
1908-
} }), value));
1909-
}
1904+
const T = @TypeOf(value);
1905+
const info = switch (@typeInfo(T)) {
1906+
.int => |info| info,
1907+
else => @compileError(@tagName(T) ++ " not supported"),
1908+
};
19101909

1911-
fn writeMultipleOf7Leb128(w: *Writer, value: anytype) Error!void {
1912-
const value_info = @typeInfo(@TypeOf(value)).int;
1913-
const Byte = packed struct(u8) { bits: u7, more: bool };
1914-
var bytes: [@divExact(value_info.bits, 7)]Byte = undefined;
1915-
var remaining = value;
1916-
for (&bytes, 1..) |*byte, len| {
1917-
const more = switch (value_info.signedness) {
1918-
.signed => remaining >> 6 != remaining >> (value_info.bits - 1),
1919-
.unsigned => remaining > std.math.maxInt(u7),
1910+
const BoundInt = @Type(.{ .int = .{ .bits = 7, .signedness = info.signedness } });
1911+
if (info.bits <= 7 or (value >= std.math.minInt(BoundInt) and value <= std.math.maxInt(BoundInt))) {
1912+
const SByte = @Type(.{ .int = .{ .bits = 8, .signedness = info.signedness } });
1913+
const byte = switch (info.signedness) {
1914+
.signed => @as(SByte, @intCast(value)) & 0x7F,
1915+
.unsigned => @as(SByte, @intCast(value)),
19201916
};
1921-
byte.* = .{
1922-
.bits = @bitCast(@as(@Type(.{ .int = .{
1923-
.signedness = value_info.signedness,
1924-
.bits = 7,
1925-
} }), @truncate(remaining))),
1926-
.more = more,
1917+
try w.writeByte(@bitCast(byte));
1918+
return;
1919+
}
1920+
1921+
const Byte = packed struct { bits: u7, more: bool };
1922+
const Int = std.math.ByteAlignedInt(T);
1923+
1924+
const max_bytes = @divFloor(info.bits - 1, 7) + 1;
1925+
1926+
var val: Int = value;
1927+
for (0..max_bytes) |_| {
1928+
const more = switch (info.signedness) {
1929+
.signed => val >> 6 != val >> (info.bits - 1),
1930+
.unsigned => val > std.math.maxInt(u7),
19271931
};
1928-
if (value_info.bits > 7) remaining >>= 7;
1929-
if (!more) return w.writeAll(@ptrCast(bytes[0..len]));
1932+
1933+
try w.writeByte(@bitCast(@as(Byte, .{
1934+
.bits = @intCast(val & 0x7F),
1935+
.more = more,
1936+
})));
1937+
1938+
if (!more) return;
1939+
1940+
val >>= 7;
19301941
} else unreachable;
19311942
}
19321943

1944+
test "serialize signed LEB128" {
1945+
// Small values
1946+
try testLeb128Encoding(i7, 9, "\x09");
1947+
try testLeb128Encoding(i64, 125, "\xFD\x00");
1948+
1949+
try testLeb128Encoding(i7, -34, "\x5E");
1950+
try testLeb128Encoding(i64, -3, "\x7D");
1951+
1952+
// Random values
1953+
try testLeb128Encoding(i16, 19373, "\xAD\x97\x01");
1954+
try testLeb128Encoding(i32, 1628839242, "\xCA\xBA\xD8\x88\x06");
1955+
try testLeb128Encoding(i64, 3789169920125966546, "\xD2\xB1\xD0\xD5\xF6\xBE\xF5\xCA\x34");
1956+
try testLeb128Encoding(i128, 704622239050934257305893323522763588, "\xC4\xD6\x83\xC7\xE3\x91\x95\xC3\x96\x80\x8D\xA5\xF5\xDF\xA3\xDA\x87\x01");
1957+
1958+
try testLeb128Encoding(i16, -14558, "\xA2\x8E\x7F");
1959+
try testLeb128Encoding(i32, -1702738165, "\x8B\x8E\x89\xD4\x79");
1960+
try testLeb128Encoding(i64, -1709126996960612298, "\xB6\xE0\x87\xB1\xD3\xC1\xFD\xA3\x68");
1961+
try testLeb128Encoding(i128, -113498719181566012704681230050325944039, "\x99\xD2\x80\xBC\xE6\x95\xBC\xC8\xDE\xB4\x9D\x81\x9F\xCA\xC6\xF8\x9C\xD5\x7E");
1962+
1963+
// {min,max} values
1964+
try testLeb128Encoding(i16, std.math.maxInt(i16), "\xFF\xFF\x01");
1965+
try testLeb128Encoding(i32, std.math.maxInt(i32), "\xFF\xFF\xFF\xFF\x07");
1966+
try testLeb128Encoding(i64, std.math.maxInt(i64), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00");
1967+
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");
1968+
1969+
try testLeb128Encoding(i16, std.math.minInt(i16), "\x80\x80\x7E");
1970+
try testLeb128Encoding(i32, std.math.minInt(i32), "\x80\x80\x80\x80\x78");
1971+
try testLeb128Encoding(i64, std.math.minInt(i64), "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x7F");
1972+
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");
1973+
1974+
// Specific cases
1975+
try testLeb128Encoding(i0, 0, "\x00");
1976+
try testLeb128Encoding(i8, 0, "\x00");
1977+
1978+
try testLeb128Encoding(i2, -1, "\x7F");
1979+
try testLeb128Encoding(i8, -1, "\x7F");
1980+
1981+
try testLeb128Encoding(i2, 1, "\x01");
1982+
try testLeb128Encoding(i8, 1, "\x01");
1983+
1984+
// Encode byte boundaries
1985+
try testLeb128Encoding(i7, std.math.maxInt(i7), "\x3F");
1986+
try testLeb128Encoding(i8, std.math.maxInt(i7) + 1, "\xC0\x00");
1987+
try testLeb128Encoding(i14, std.math.maxInt(i14), "\xFF\x3F");
1988+
try testLeb128Encoding(i15, std.math.maxInt(i14) + 1, "\x80\xC0\x00");
1989+
try testLeb128Encoding(i49, std.math.maxInt(i49), "\xFF\xFF\xFF\xFF\xFF\xFF\x3F");
1990+
try testLeb128Encoding(i50, std.math.maxInt(i49) + 1, "\x80\x80\x80\x80\x80\x80\xC0\x00");
1991+
try testLeb128Encoding(i56, std.math.maxInt(i56), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F");
1992+
try testLeb128Encoding(i57, std.math.maxInt(i56) + 1, "\x80\x80\x80\x80\x80\x80\x80\xC0\x00");
1993+
try testLeb128Encoding(i63, std.math.maxInt(i63), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F");
1994+
try testLeb128Encoding(i64, std.math.maxInt(i63) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\xC0\x00");
1995+
1996+
try testLeb128Encoding(i7, std.math.minInt(i7), "\x40");
1997+
try testLeb128Encoding(i8, std.math.minInt(i7) - 1, "\xBF\x7F");
1998+
try testLeb128Encoding(i14, std.math.minInt(i14), "\x80\x40");
1999+
try testLeb128Encoding(i15, std.math.minInt(i14) - 1, "\xFF\xBF\x7F");
2000+
try testLeb128Encoding(i49, std.math.minInt(i49), "\x80\x80\x80\x80\x80\x80\x40");
2001+
try testLeb128Encoding(i50, std.math.minInt(i49) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
2002+
try testLeb128Encoding(i56, std.math.minInt(i56), "\x80\x80\x80\x80\x80\x80\x80\x40");
2003+
try testLeb128Encoding(i57, std.math.minInt(i56) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
2004+
try testLeb128Encoding(i63, std.math.minInt(i63), "\x80\x80\x80\x80\x80\x80\x80\x80\x40");
2005+
try testLeb128Encoding(i64, std.math.minInt(i63) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
2006+
}
2007+
2008+
test "serialize unsigned LEB128" {
2009+
// Small values
2010+
try testLeb128Encoding(u7, 12, "\x0C");
2011+
try testLeb128Encoding(u64, 201, "\xC9\x01");
2012+
2013+
// Random values
2014+
try testLeb128Encoding(u8, 254, "\xFE\x01");
2015+
try testLeb128Encoding(u16, 30241, "\xA1\xEC\x01");
2016+
try testLeb128Encoding(u32, 2173531193, "\xB9\xE8\xB5\x8C\x08");
2017+
try testLeb128Encoding(u64, 18321125691115744902, "\x86\xDD\xF2\x81\xF2\xD7\xED\xA0\xFE\x01");
2018+
try testLeb128Encoding(u128, 122619209508942982841456325819614676193, "\xE1\x89\xF3\xD9\xE3\xAD\xEC\xF4\x98\x95\xF8\xBB\xD7\xB8\xF2\xCC\xBF\xB8\x01");
2019+
2020+
// Max values
2021+
try testLeb128Encoding(u8, std.math.maxInt(u8), "\xFF\x01");
2022+
try testLeb128Encoding(u16, std.math.maxInt(u16), "\xFF\xFF\x03");
2023+
try testLeb128Encoding(u32, std.math.maxInt(u32), "\xFF\xFF\xFF\xFF\x0F");
2024+
try testLeb128Encoding(u64, std.math.maxInt(u64), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01");
2025+
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");
2026+
2027+
// Specific cases
2028+
try testLeb128Encoding(u0, 0, "\x00");
2029+
try testLeb128Encoding(u1, 0, "\x00");
2030+
try testLeb128Encoding(u8, 0, "\x00");
2031+
2032+
try testLeb128Encoding(u1, 1, "\x01");
2033+
try testLeb128Encoding(u8, 1, "\x01");
2034+
2035+
// Encode byte boundaries
2036+
try testLeb128Encoding(u7, std.math.maxInt(u7), "\x7F");
2037+
try testLeb128Encoding(u8, std.math.maxInt(u7) + 1, "\x80\x01");
2038+
try testLeb128Encoding(u14, std.math.maxInt(u14), "\xFF\x7F");
2039+
try testLeb128Encoding(u15, std.math.maxInt(u14) + 1, "\x80\x80\x01");
2040+
try testLeb128Encoding(u49, std.math.maxInt(u49), "\xFF\xFF\xFF\xFF\xFF\xFF\x7F");
2041+
try testLeb128Encoding(u50, std.math.maxInt(u49) + 1, "\x80\x80\x80\x80\x80\x80\x80\x01");
2042+
try testLeb128Encoding(u56, std.math.maxInt(u56), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F");
2043+
try testLeb128Encoding(u57, std.math.maxInt(u56) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\x01");
2044+
try testLeb128Encoding(u63, std.math.maxInt(u63), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F");
2045+
try testLeb128Encoding(u64, std.math.maxInt(u63) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01");
2046+
}
2047+
2048+
fn testLeb128Encoding(comptime T: type, value: T, encoding: []const u8) !void {
2049+
const info = @typeInfo(T).int;
2050+
const max_bytes = @divFloor(info.bits -| 1, 7) + 1;
2051+
var bytes: [max_bytes]u8 = undefined;
2052+
2053+
var fw: Writer = .fixed(&bytes);
2054+
try writeLeb128(&fw, value);
2055+
2056+
try std.testing.expectEqualSlices(u8, encoding, fw.buffered());
2057+
}
2058+
19332059
test "printValue max_depth" {
19342060
const Vec2 = struct {
19352061
const SelfType = @This();

0 commit comments

Comments
 (0)