Skip to content

Commit bba4a58

Browse files
committed
Optimize Writer.writeLeb128
Rewrite `writeLeb128` to no longer use `writeMultipleOf7Leb128`, instead: * Make use of byte aligned ints * Use `writeableSliceGreedy` instead of an array * Special case small numbers (fitting inside 7 bits) Across larger integers (> 7 bits) this roughly doubles performance in a [micro-benchmark](https://zigbin.io/b8b396). Also add test coverage
1 parent bf90825 commit bba4a58

File tree

1 file changed

+185
-21
lines changed

1 file changed

+185
-21
lines changed

lib/std/Io/Writer.zig

Lines changed: 185 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1901,35 +1901,199 @@ 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 Byte = @Type(.{ .int = .{ .bits = 8, .signedness = info.signedness } });
1913+
const byte = switch (info.signedness) {
1914+
.signed => @as(Byte, @intCast(value)) & 0x7F,
1915+
.unsigned => @as(Byte, @intCast(value)),
19201916
};
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+
const bytes: []Byte = @ptrCast(try w.writableSliceGreedy(max_bytes));
1926+
1927+
var val: Int = value;
1928+
for (bytes, 1..) |*byte, len| {
1929+
const more = switch (info.signedness) {
1930+
.signed => val >> 6 != val >> (info.bits - 1),
1931+
.unsigned => val > std.math.maxInt(u7),
1932+
};
1933+
19211934
byte.* = .{
1922-
.bits = @bitCast(@as(@Type(.{ .int = .{
1923-
.signedness = value_info.signedness,
1924-
.bits = 7,
1925-
} }), @truncate(remaining))),
1935+
.bits = @intCast(val & 0x7F),
19261936
.more = more,
19271937
};
1928-
if (value_info.bits > 7) remaining >>= 7;
1929-
if (!more) return w.writeAll(@ptrCast(bytes[0..len]));
1938+
1939+
if (!more) {
1940+
w.advance(len);
1941+
return;
1942+
}
1943+
1944+
val >>= 7;
19301945
} else unreachable;
19311946
}
19321947

1948+
test "serialize signed LEB128" {
1949+
// Encode byte boundaries
1950+
try testLeb128Encoding(i7, std.math.maxInt(i7), "\x3F");
1951+
try testLeb128Encoding(i8, std.math.maxInt(i7) + 1, "\xC0\x00");
1952+
try testLeb128Encoding(i14, std.math.maxInt(i14), "\xFF\x3F");
1953+
try testLeb128Encoding(i15, std.math.maxInt(i14) + 1, "\x80\xC0\x00");
1954+
try testLeb128Encoding(i21, std.math.maxInt(i21), "\xFF\xFF\x3F");
1955+
try testLeb128Encoding(i22, std.math.maxInt(i21) + 1, "\x80\x80\xC0\x00");
1956+
try testLeb128Encoding(i28, std.math.maxInt(i28), "\xFF\xFF\xFF\x3F");
1957+
try testLeb128Encoding(i29, std.math.maxInt(i28) + 1, "\x80\x80\x80\xC0\x00");
1958+
try testLeb128Encoding(i35, std.math.maxInt(i35), "\xFF\xFF\xFF\xFF\x3F");
1959+
try testLeb128Encoding(i36, std.math.maxInt(i35) + 1, "\x80\x80\x80\x80\xC0\x00");
1960+
try testLeb128Encoding(i42, std.math.maxInt(i42), "\xFF\xFF\xFF\xFF\xFF\x3F");
1961+
try testLeb128Encoding(i43, std.math.maxInt(i42) + 1, "\x80\x80\x80\x80\x80\xC0\x00");
1962+
try testLeb128Encoding(i49, std.math.maxInt(i49), "\xFF\xFF\xFF\xFF\xFF\xFF\x3F");
1963+
try testLeb128Encoding(i50, std.math.maxInt(i49) + 1, "\x80\x80\x80\x80\x80\x80\xC0\x00");
1964+
try testLeb128Encoding(i56, std.math.maxInt(i56), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F");
1965+
try testLeb128Encoding(i57, std.math.maxInt(i56) + 1, "\x80\x80\x80\x80\x80\x80\x80\xC0\x00");
1966+
try testLeb128Encoding(i63, std.math.maxInt(i63), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x3F");
1967+
try testLeb128Encoding(i64, std.math.maxInt(i63) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\xC0\x00");
1968+
try testLeb128Encoding(i64, std.math.maxInt(i64), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00");
1969+
try testLeb128Encoding(i65, std.math.maxInt(i64) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01");
1970+
1971+
try testLeb128Encoding(i7, std.math.minInt(i7), "\x40");
1972+
try testLeb128Encoding(i8, std.math.minInt(i7) - 1, "\xBF\x7F");
1973+
try testLeb128Encoding(i14, std.math.minInt(i14), "\x80\x40");
1974+
try testLeb128Encoding(i15, std.math.minInt(i14) - 1, "\xFF\xBF\x7F");
1975+
try testLeb128Encoding(i21, std.math.minInt(i21), "\x80\x80\x40");
1976+
try testLeb128Encoding(i22, std.math.minInt(i21) - 1, "\xFF\xFF\xBF\x7F");
1977+
try testLeb128Encoding(i28, std.math.minInt(i28), "\x80\x80\x80\x40");
1978+
try testLeb128Encoding(i29, std.math.minInt(i28) - 1, "\xFF\xFF\xFF\xBF\x7F");
1979+
try testLeb128Encoding(i35, std.math.minInt(i35), "\x80\x80\x80\x80\x40");
1980+
try testLeb128Encoding(i36, std.math.minInt(i35) - 1, "\xFF\xFF\xFF\xFF\xBF\x7F");
1981+
try testLeb128Encoding(i42, std.math.minInt(i42), "\x80\x80\x80\x80\x80\x40");
1982+
try testLeb128Encoding(i43, std.math.minInt(i42) - 1, "\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
1983+
try testLeb128Encoding(i49, std.math.minInt(i49), "\x80\x80\x80\x80\x80\x80\x40");
1984+
try testLeb128Encoding(i50, std.math.minInt(i49) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
1985+
try testLeb128Encoding(i56, std.math.minInt(i56), "\x80\x80\x80\x80\x80\x80\x80\x40");
1986+
try testLeb128Encoding(i57, std.math.minInt(i56) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
1987+
try testLeb128Encoding(i63, std.math.minInt(i63), "\x80\x80\x80\x80\x80\x80\x80\x80\x40");
1988+
try testLeb128Encoding(i64, std.math.minInt(i63) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xBF\x7F");
1989+
try testLeb128Encoding(i64, std.math.minInt(i64), "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x7F");
1990+
try testLeb128Encoding(i65, std.math.minInt(i64) - 1, "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7E");
1991+
1992+
// Random values
1993+
try testLeb128Encoding(i0, 0, "\x00");
1994+
try testLeb128Encoding(i1, 0, "\x00");
1995+
1996+
try testLeb128Encoding(i1, -1, "\x7F");
1997+
try testLeb128Encoding(i7, -42, "\x56");
1998+
try testLeb128Encoding(i7, -60, "\x44");
1999+
try testLeb128Encoding(i7, 28, "\x1C");
2000+
try testLeb128Encoding(i7, 48, "\x30");
2001+
try testLeb128Encoding(i7, 7, "\x07");
2002+
2003+
try testLeb128Encoding(i8, 75, "\xCB\x00");
2004+
try testLeb128Encoding(i8, -16, "\x70");
2005+
try testLeb128Encoding(i16, 8568, "\xF8\xC2\x00");
2006+
try testLeb128Encoding(i16, -22129, "\x8F\xD3\x7E");
2007+
try testLeb128Encoding(i32, 706871907, "\xE3\x84\x88\xD1\x02");
2008+
try testLeb128Encoding(i32, -1086310367, "\xA1\xF0\x80\xFA\x7B");
2009+
try testLeb128Encoding(i64, 3191772538352771995, "\x9B\xC7\xF4\x82\xE8\x86\xDD\xA5\x2C");
2010+
try testLeb128Encoding(i64, 6244662871897637219, "\xE3\x82\x9E\xEC\xBD\xF9\xDF\xD4\xD6\x00");
2011+
try testLeb128Encoding(i64, -7689149645688732033, "\xFF\x8C\x9C\xBF\xAD\xE4\xA9\xA5\x95\x7F");
2012+
try testLeb128Encoding(i64, -2013874151389131036, "\xE4\x9D\xF0\xC0\x91\x84\xD2\x86\x64");
2013+
2014+
// Ensure basic properties for every relevant type
2015+
inline for (0..128 + 1) |bits| {
2016+
const T = @Type(.{ .int = .{ .bits = bits, .signedness = .signed } });
2017+
2018+
try testLeb128Encoding(T, 0, "\x00");
2019+
if (bits > 0) {
2020+
try testLeb128Encoding(T, -1, "\x7F");
2021+
}
2022+
if (bits > 1) {
2023+
try testLeb128Encoding(T, 1, "\x01");
2024+
}
2025+
}
2026+
}
2027+
2028+
test "serialize unsigned LEB128" {
2029+
// Encode byte boundaries
2030+
try testLeb128Encoding(u7, std.math.maxInt(u7), "\x7F");
2031+
try testLeb128Encoding(u8, std.math.maxInt(u7) + 1, "\x80\x01");
2032+
try testLeb128Encoding(u14, std.math.maxInt(u14), "\xFF\x7F");
2033+
try testLeb128Encoding(u15, std.math.maxInt(u14) + 1, "\x80\x80\x01");
2034+
try testLeb128Encoding(u21, std.math.maxInt(u21), "\xFF\xFF\x7F");
2035+
try testLeb128Encoding(u22, std.math.maxInt(u21) + 1, "\x80\x80\x80\x01");
2036+
try testLeb128Encoding(u28, std.math.maxInt(u28), "\xFF\xFF\xFF\x7F");
2037+
try testLeb128Encoding(u29, std.math.maxInt(u28) + 1, "\x80\x80\x80\x80\x01");
2038+
try testLeb128Encoding(u35, std.math.maxInt(u35), "\xFF\xFF\xFF\xFF\x7F");
2039+
try testLeb128Encoding(u36, std.math.maxInt(u35) + 1, "\x80\x80\x80\x80\x80\x01");
2040+
try testLeb128Encoding(u42, std.math.maxInt(u42), "\xFF\xFF\xFF\xFF\xFF\x7F");
2041+
try testLeb128Encoding(u43, std.math.maxInt(u42) + 1, "\x80\x80\x80\x80\x80\x80\x01");
2042+
try testLeb128Encoding(u49, std.math.maxInt(u49), "\xFF\xFF\xFF\xFF\xFF\xFF\x7F");
2043+
try testLeb128Encoding(u50, std.math.maxInt(u49) + 1, "\x80\x80\x80\x80\x80\x80\x80\x01");
2044+
try testLeb128Encoding(u56, std.math.maxInt(u56), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F");
2045+
try testLeb128Encoding(u57, std.math.maxInt(u56) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\x01");
2046+
try testLeb128Encoding(u63, std.math.maxInt(u63), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F");
2047+
try testLeb128Encoding(u64, std.math.maxInt(u63) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x01");
2048+
try testLeb128Encoding(u64, std.math.maxInt(u64), "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x01");
2049+
try testLeb128Encoding(u64, std.math.maxInt(u7) + 1, "\x80\x01");
2050+
try testLeb128Encoding(u64, std.math.maxInt(u7), "\x7F");
2051+
try testLeb128Encoding(u65, std.math.maxInt(u64) + 1, "\x80\x80\x80\x80\x80\x80\x80\x80\x80\x02");
2052+
2053+
// Random values
2054+
try testLeb128Encoding(u0, 0, "\x00");
2055+
try testLeb128Encoding(u1, 0, "\x00");
2056+
2057+
try testLeb128Encoding(u1, 1, "\x01");
2058+
try testLeb128Encoding(u7, 10, "\x0A");
2059+
try testLeb128Encoding(u7, 12, "\x0C");
2060+
try testLeb128Encoding(u7, 32, "\x20");
2061+
try testLeb128Encoding(u7, 52, "\x34");
2062+
try testLeb128Encoding(u7, 98, "\x62");
2063+
2064+
try testLeb128Encoding(u8, 34, "\x22");
2065+
try testLeb128Encoding(u8, 133, "\x85\x01");
2066+
try testLeb128Encoding(u16, 64282, "\x9A\xF6\x03");
2067+
try testLeb128Encoding(u16, 43357, "\xDD\xD2\x02");
2068+
try testLeb128Encoding(u32, 677110117, "\xE5\xC2\xEF\xC2\x02");
2069+
try testLeb128Encoding(u32, 3777136920, "\x98\x92\x8A\x89\x0E");
2070+
try testLeb128Encoding(u64, 10540460130307935799, "\xB7\x9C\xB0\xE1\xC8\x94\xCF\xA3\x92\x01");
2071+
try testLeb128Encoding(u64, 13430772867946637439, "\xFF\xD0\xA7\xE7\xB9\x98\xEC\xB1\xBA\x01");
2072+
try testLeb128Encoding(u64, 3059925747106684405, "\xF5\xBB\xF6\xDA\x93\xC8\xC2\xBB\x2A");
2073+
try testLeb128Encoding(u64, 9843945664624830445, "\xED\xCF\xD6\x81\xD3\x8D\xAE\xCE\x88\x01");
2074+
2075+
// Ensure basic properties for every relevant type
2076+
inline for (0..128 + 1) |bits| {
2077+
const T = @Type(.{ .int = .{ .bits = bits, .signedness = .unsigned } });
2078+
2079+
try testLeb128Encoding(T, 0, "\x00");
2080+
if (bits > 0) {
2081+
try testLeb128Encoding(T, 1, "\x01");
2082+
}
2083+
}
2084+
}
2085+
2086+
fn testLeb128Encoding(comptime T: type, value: T, encoding: []const u8) !void {
2087+
const info = @typeInfo(T).int;
2088+
const max_bytes = @divFloor(info.bits -| 1, 7) + 1;
2089+
var bytes: [max_bytes]u8 = undefined;
2090+
2091+
var fw: Writer = .fixed(&bytes);
2092+
try writeLeb128(&fw, value);
2093+
2094+
try std.testing.expectEqualSlices(u8, encoding, fw.buffered());
2095+
}
2096+
19332097
test "printValue max_depth" {
19342098
const Vec2 = struct {
19352099
const SelfType = @This();

0 commit comments

Comments
 (0)