Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed
- Decoding an optional is about 18% cheaper, by letting each type's own unpacker recognise a nil header instead of testing for one beforehand
- Decoding string and binary values copies straight out of the reader's buffer when the value is already buffered, which is 3-7% cheaper on string-heavy messages

### Removed
- `omit_nulls` and `omit_defaults` on `UnionAsMapOptions`, which were never read; a union-as-map always writes exactly one entry, so there is nothing to omit
Expand Down
5 changes: 3 additions & 2 deletions src/binary.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const maybeUnpackNull = @import("null.zig").maybeUnpackNull;

const packHeaderAndInt = @import("utils.zig").packHeaderAndInt;
const unpackIntValue = @import("int.zig").unpackIntValue;
const readSliceFast = @import("utils.zig").readSliceFast;
const unpackShortIntValue = @import("int.zig").unpackShortIntValue;

pub fn sizeOfPackedBinaryHeader(len: usize) !usize {
Expand Down Expand Up @@ -62,7 +63,7 @@ pub fn unpackBinary(reader: *std.Io.Reader, allocator: std.mem.Allocator) ![]u8
const data = try allocator.alloc(u8, len);
errdefer allocator.free(data);

try reader.readSliceAll(data);
try readSliceFast(reader, data);
return data;
}

Expand All @@ -74,7 +75,7 @@ pub fn unpackBinaryInto(reader: *std.Io.Reader, buf: []u8) ![]u8 {
}

const data = buf[0..len];
try reader.readSliceAll(data);
try readSliceFast(reader, data);
return data;
}

Expand Down
5 changes: 3 additions & 2 deletions src/string.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const maybeUnpackNull = @import("null.zig").maybeUnpackNull;

const packHeaderAndInt = @import("utils.zig").packHeaderAndInt;
const reserveArray = @import("utils.zig").reserveArray;
const readSliceFast = @import("utils.zig").readSliceFast;
const unpackIntValue = @import("int.zig").unpackIntValue;
const unpackShortIntValue = @import("int.zig").unpackShortIntValue;

Expand Down Expand Up @@ -103,7 +104,7 @@ pub fn unpackStringValue(reader: *std.Io.Reader, allocator: std.mem.Allocator, c
const data = try allocator.alloc(u8, len);
errdefer allocator.free(data);

try reader.readSliceAll(data);
try readSliceFast(reader, data);
return data;
}

Expand Down Expand Up @@ -148,7 +149,7 @@ pub fn unpackStringInto(reader: *std.Io.Reader, buf: []u8) ![]u8 {
}

const data = buf[0..len];
try reader.readSliceAll(data);
try readSliceFast(reader, data);
return data;
}

Expand Down
38 changes: 38 additions & 0 deletions src/utils.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ pub inline fn takeInt(reader: *std.Io.Reader, comptime T: type) !T {
return reader.takeInt(T, .big);
}

/// Copies `dest.len` bytes out of the reader. When they are already buffered,
/// which is always the case when decoding from a slice, this is a plain memcpy
/// and skips `readSliceAll`'s short-read loop. Falls back to it otherwise, so a
/// streaming reader whose buffer cannot hold the value still works.
pub inline fn readSliceFast(reader: *std.Io.Reader, dest: []u8) !void {
const buffered = reader.buffered();
if (buffered.len >= dest.len) {
@branchHint(.likely);
@memcpy(dest, buffered[0..dest.len]);
reader.toss(dest.len);
return;
}
return reader.readSliceAll(dest);
}

/// Compares `value` against a comptime-known `name`. The length test is a
/// compare against a constant, and the byte compare that follows has a
/// comptime-known length, so it lowers to inline compares rather than a call.
Expand Down Expand Up @@ -93,3 +108,26 @@ pub const NoAllocator = struct {
};
}
};

test "readSliceFast: copies straight out of the buffer when fully buffered" {
const data = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8 };
var reader = std.Io.Reader.fixed(&data);

var dest: [5]u8 = undefined;
try readSliceFast(&reader, &dest);
try std.testing.expectEqualSlices(u8, data[0..5], &dest);
// The bytes must be consumed, not just peeked.
try std.testing.expectEqualSlices(u8, data[5..], reader.buffered());
}

test "readSliceFast: falls back when the value is not fully buffered" {
const data = [_]u8{ 1, 2, 3, 4, 5, 6, 7, 8 };
var reader = std.Io.Reader.fixed(&data);
// Pretend only half the value has arrived so far.
reader.end = 4;

var dest: [8]u8 = undefined;
// A fixed reader cannot refill, so taking the fallback surfaces
// EndOfStream. Taking the fast path here would instead read past `end`.
try std.testing.expectError(error.EndOfStream, readSliceFast(&reader, &dest));
}
Loading