Skip to content

Commit

Permalink
perf(bincode): faster de/serialization of raw bytes (#351)
Browse files Browse the repository at this point in the history
Optimize bincode to directly copy bytes instead of attempting to serialize/deserialize them as individual integers. This speeds up Entry de/serialization by 3-4x.

Also adds a benchmark to serialize Entry

before:
```
Benchmark        Iterations    Min(ns)    Max(ns)   Variance   Mean(ns)
-----------------------------------------------------------------------
serializeEntry       200000       1083      70417     191263       1367
---
deserializeEntry     200000       2250      60291     116981       2515
```
after:
```
serializeEntry       200000        250      14959       8130        350
---
deserializeEntry     200000        666      21666      12248        784
```
  • Loading branch information
dnut authored Nov 1, 2024
1 parent 1dd9166 commit 7ad3927
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 74 deletions.
8 changes: 8 additions & 0 deletions src/benchmarks.zig
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,14 @@ pub fn main() !void {
.milliseconds,
);
}

if (std.mem.startsWith(u8, filter, "bincode") or run_all_benchmarks) {
num_errors += try benchmark(
@import("bincode/benchmarks.zig").BenchmarkEntry,
max_time_per_bench,
.nanoseconds,
);
}
if (num_errors != 0) {
return error.CompletedWithErrors;
}
Expand Down
33 changes: 33 additions & 0 deletions src/bincode/benchmarks.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
pub const std = @import("std");
pub const sig = @import("../sig.zig");

pub const Entry = sig.core.entry.Entry;
pub const test_entry = sig.core.entry.test_entry;

pub const BenchmarkEntry = struct {
pub const min_iterations = 200_000;
pub const max_iterations = 200_000;

pub fn serializeEntry() !sig.time.Duration {
const allocator = std.heap.c_allocator;

var timer = try sig.time.Timer.start();
const actual_bytes = try sig.bincode.writeAlloc(allocator, test_entry.as_struct, .{});
defer allocator.free(actual_bytes);
return timer.read();
}

pub fn deserializeEntry() !sig.time.Duration {
const allocator = std.heap.c_allocator;

var timer = try sig.time.Timer.start();
const actual_struct = try sig.bincode.readFromSlice(
allocator,
Entry,
&test_entry.bincode_serialized_bytes,
.{},
);
defer actual_struct.deinit(allocator);
return timer.read();
}
};
11 changes: 11 additions & 0 deletions src/bincode/bincode.zig
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,17 @@ pub fn getConfig(comptime T: type) ?FieldConfig(T) {
hashmap.hashMapFieldConfig(T, .{})
else if (comptime arrayListInfo(T) != null)
arraylist.standardConfig(T)
else if (T == u8)
int.U8Config()
else if (T == []const u8)
int.U8ConstSliceConfig()
else if (T == []u8)
int.U8SliceConfig()
else if (@typeInfo(T) == .Array and @typeInfo(T).Array.child == u8)
if (@typeInfo(T).Array.sentinel) |_|
int.U8ArraySentinelConfig(@typeInfo(T).Array.len)
else
int.U8ArrayConfig(@typeInfo(T).Array.len)
else
null;
}
Expand Down
65 changes: 59 additions & 6 deletions src/bincode/int.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ pub fn defaultOnEof(comptime T: type, comptime eof_value: T) bincode.FieldConfig
params: bincode.Params,
) anyerror!T {
var fba = comptime std.heap.FixedBufferAllocator.init(&.{});
return bincode.read(fba.allocator(), T, reader, params) catch |err| switch (err) {
error.EndOfStream => eof_value,
else => |e| e,
};
return bincode.read(fba.allocator(), T, reader, params) catch |err|
if (err == error.EndOfStream) eof_value else err;
}

fn serializer(
Expand All @@ -31,7 +29,6 @@ pub fn defaultOnEof(comptime T: type, comptime eof_value: T) bincode.FieldConfig
};
}

// TODO: make this the default behaviour for bincode []u8 slices
pub fn U8SliceConfig() bincode.FieldConfig([]u8) {
const S = struct {
pub fn serialize(writer: anytype, data: anytype, _: bincode.Params) !void {
Expand Down Expand Up @@ -63,7 +60,44 @@ pub fn U8SliceConfig() bincode.FieldConfig([]u8) {
};
}

// TODO: make this the default behaviour for bincode [size]u8 arrays
pub fn U8Config() bincode.FieldConfig(u8) {
const S = struct {
pub fn serialize(writer: anytype, data: anytype, _: bincode.Params) !void {
try writer.writeByte(data);
}

pub fn deserialize(_: std.mem.Allocator, reader: anytype, _: bincode.Params) !u8 {
return try reader.readByte();
}

pub fn free(_: std.mem.Allocator, _: anytype) void {}
};

return bincode.FieldConfig(u8){
.serializer = S.serialize,
.deserializer = S.deserialize,
.free = S.free,
};
}

pub fn U8ConstSliceConfig() bincode.FieldConfig([]const u8) {
const S = struct {
pub fn deserialize(
allocator: std.mem.Allocator,
reader: anytype,
params: bincode.Params,
) ![]const u8 {
return U8SliceConfig().deserializer.?(allocator, reader, params);
}
};

return bincode.FieldConfig([]const u8){
.serializer = U8SliceConfig().serializer,
.deserializer = S.deserialize,
.free = U8SliceConfig().free,
};
}

pub fn U8ArrayConfig(comptime size: u64) bincode.FieldConfig([size]u8) {
const S = struct {
pub fn serialize(writer: anytype, data: anytype, params: bincode.Params) !void {
Expand Down Expand Up @@ -94,3 +128,22 @@ pub fn U8ArrayConfig(comptime size: u64) bincode.FieldConfig([size]u8) {
.free = S.free,
};
}

pub fn U8ArraySentinelConfig(comptime size: u64) bincode.FieldConfig([size:0]u8) {
const S = struct {
pub fn deserialize(allocator: std.mem.Allocator, reader: anytype, params: bincode.Params) ![size:0]u8 {
if (params.include_fixed_array_length) {
_ = try bincode.read(allocator, u64, reader, .{});
}
var buf: [size:0]u8 = undefined;
@memcpy(&buf, &try reader.readBytesNoEof(size));
return buf;
}
};

return bincode.FieldConfig([size:0]u8){
.serializer = U8ArrayConfig(size).serializer,
.deserializer = S.deserialize,
.free = U8ArrayConfig(size).free,
};
}
5 changes: 0 additions & 5 deletions src/core/account.zig
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
const sig = @import("../sig.zig");
const bincode = sig.bincode;

const Pubkey = sig.core.Pubkey;
const Epoch = sig.core.Epoch;
const AccountInFile = sig.accounts_db.accounts_file.AccountInFile;

const U8SliceConfig = bincode.int.U8SliceConfig;

pub const Account = struct {
lamports: u64,
data: []u8,
owner: Pubkey,
executable: bool,
rent_epoch: Epoch,

pub const @"!bincode-config:data" = U8SliceConfig();

pub fn deinit(self: Account, allocator: std.mem.Allocator) void {
allocator.free(self.data);
}
Expand Down
34 changes: 16 additions & 18 deletions src/core/entry.zig
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,25 @@ pub const Entry = struct {
};

test "Entry serialization and deserialization" {
const entry = try test_entry.asStruct(std.testing.allocator);
defer entry.deinit(std.testing.allocator);
const entry = test_entry.as_struct;
try sig.bincode.testRoundTrip(entry, &test_entry.bincode_serialized_bytes);
}

const test_entry = struct {
pub fn asStruct(allocator: std.mem.Allocator) !Entry {
var transactions = try std.ArrayListUnmanaged(core.VersionedTransaction)
.initCapacity(allocator, 2);
transactions.appendAssumeCapacity(
try core.transaction.test_v0_transaction.asStruct(allocator),
);
transactions.appendAssumeCapacity(
try core.transaction.test_v0_transaction.asStruct(allocator),
);
return .{
.num_hashes = 149218308,
.hash = try core.Hash.parseBase58String("G8T3smgLc4XavAtxScD3u4FTAqPtwbFCEJKwJbfoECcd"),
.transactions = transactions,
};
}
pub const test_entry = struct {
pub const as_struct = Entry{
.num_hashes = 149218308,
.hash = core.Hash
.parseBase58String("G8T3smgLc4XavAtxScD3u4FTAqPtwbFCEJKwJbfoECcd") catch unreachable,
.transactions = .{
.items = txns[0..2],
.capacity = 2,
},
};

var txns = [_]core.VersionedTransaction{
core.transaction.test_v0_transaction.as_struct,
core.transaction.test_v0_transaction.as_struct,
};

pub const bincode_serialized_bytes = [_]u8{
4, 228, 228, 8, 0, 0, 0, 0, 224, 199, 210, 235, 148, 143, 98, 241, 248, 45,
Expand Down
1 change: 0 additions & 1 deletion src/core/pubkey.zig
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ pub const Pubkey = extern struct {
const Self = @This();

pub const size = 32;
pub const @"!bincode-config:data" = sig.bincode.int.U8ArrayConfig(size);

pub const ZEROES: Pubkey = .{ .data = .{0} ** size };

Expand Down
83 changes: 39 additions & 44 deletions src/core/transaction.zig
Original file line number Diff line number Diff line change
Expand Up @@ -719,33 +719,32 @@ test "Message sanitize fails if account index is out of bounds" {
}

test "V0Message serialization and deserialization" {
const message = try test_v0_message.asStruct(std.testing.allocator);
defer message.deinit(std.testing.allocator);
const message = test_v0_message.as_struct;
try sig.bincode.testRoundTrip(message, &test_v0_message.bincode_serialized_bytes);
}

test "VersionedTransaction v0 serialization and deserialization" {
const transaction = try test_v0_transaction.asStruct(std.testing.allocator);
defer transaction.deinit(std.testing.allocator);
const transaction = test_v0_transaction.as_struct;
try sig.bincode.testRoundTrip(transaction, &test_v0_transaction.bincode_serialized_bytes);
}

test "VersionedMessage v0 serialization and deserialization" {
const versioned_message = try test_v0_versioned_message.asStruct(std.testing.allocator);
defer versioned_message.deinit(std.testing.allocator);
const versioned_message = test_v0_versioned_message.as_struct;
try sig.bincode.testRoundTrip(versioned_message, &test_v0_versioned_message.bincode_serialized_bytes);
}

pub const test_v0_transaction = struct {
pub fn asStruct(allocator: std.mem.Allocator) !VersionedTransaction {
return .{
.signatures = try allocator.dupe(Signature, &.{
try Signature.fromString("2cxn1LdtB7GcpeLEnHe5eA7LymTXKkqGF6UvmBM2EtttZEeqBREDaAD7LCagDFHyuc3xXxyDkMPiy3CpK5m6Uskw"),
try Signature.fromString("4gr9L7K3bALKjPRiRSk4JDB3jYmNaauf6rewNV3XFubX5EHxBn98gqBGhbwmZAB9DJ2pv8GWE1sLoYqhhLbTZcLj"),
}),
.message = .{ .v0 = try test_v0_message.asStruct(allocator) },
};
}
pub const as_struct = VersionedTransaction{
.signatures = &.{
Signature.fromString(
"2cxn1LdtB7GcpeLEnHe5eA7LymTXKkqGF6UvmBM2EtttZEeqBREDaAD7LCagDFHyuc3xXxyDkMPiy3CpK5m6Uskw",
) catch unreachable,
Signature.fromString(
"4gr9L7K3bALKjPRiRSk4JDB3jYmNaauf6rewNV3XFubX5EHxBn98gqBGhbwmZAB9DJ2pv8GWE1sLoYqhhLbTZcLj",
) catch unreachable,
},
.message = .{ .v0 = test_v0_message.as_struct },
};

pub const bincode_serialized_bytes = [_]u8{
2, 81, 7, 106, 50, 99, 54, 99, 92, 187, 47, 10, 170, 102, 132, 42, 25, 4,
Expand All @@ -770,9 +769,7 @@ pub const test_v0_transaction = struct {
};

pub const test_v0_versioned_message = struct {
pub fn asStruct(allocator: std.mem.Allocator) !VersionedMessage {
return .{ .v0 = try test_v0_message.asStruct(allocator) };
}
pub const as_struct = VersionedMessage{ .v0 = test_v0_message.as_struct };

pub const bincode_serialized_bytes = [_]u8{
128, 39, 12, 102, 2, 236, 88, 117, 221, 34, 125, 55, 183, 193, 174, 21, 99, 70,
Expand All @@ -789,33 +786,31 @@ pub const test_v0_versioned_message = struct {
};

pub const test_v0_message = struct {
pub fn asStruct(allocator: std.mem.Allocator) !V0Message {
return .{
.header = .{
.num_required_signatures = 39,
.num_readonly_signed_accounts = 12,
.num_readonly_unsigned_accounts = 102,
pub const as_struct = V0Message{
.header = .{
.num_required_signatures = 39,
.num_readonly_signed_accounts = 12,
.num_readonly_unsigned_accounts = 102,
},
.account_keys = &.{
Pubkey.fromString("GubTBrbgk9JwkwX1FkXvsrF1UC2AP7iTgg8SGtgH14QE") catch unreachable,
Pubkey.fromString("5yCD7QeAk5uAduhLZGxePv21RLsVEktPqJG5pbmZx4J4") catch unreachable,
},
.recent_blockhash = Hash.parseBase58String("4xzjBNLkRqhBVmZ7JKcX2UEP8wzYKYWpXk7CPXzgrEZW") catch unreachable,
.instructions = &.{.{
.program_id_index = 100,
.accounts = &.{ 1, 3 },
.data = &.{
104, 232, 42, 254, 46, 48, 104, 89, 101, 211, 253, 161, 65, 155, 204, 89,
126, 187, 180, 191, 60, 59, 88, 119, 106, 20, 194, 80, 11, 200, 76, 0,
},
.account_keys = try allocator.dupe(Pubkey, &.{
try Pubkey.fromString("GubTBrbgk9JwkwX1FkXvsrF1UC2AP7iTgg8SGtgH14QE"),
try Pubkey.fromString("5yCD7QeAk5uAduhLZGxePv21RLsVEktPqJG5pbmZx4J4"),
}),
.recent_blockhash = try Hash.parseBase58String("4xzjBNLkRqhBVmZ7JKcX2UEP8wzYKYWpXk7CPXzgrEZW"),
.instructions = try allocator.dupe(CompiledInstruction, &.{.{
.program_id_index = 100,
.accounts = try allocator.dupe(u8, &.{ 1, 3 }),
.data = try allocator.dupe(u8, &.{
104, 232, 42, 254, 46, 48, 104, 89, 101, 211, 253, 161, 65, 155, 204, 89,
126, 187, 180, 191, 60, 59, 88, 119, 106, 20, 194, 80, 11, 200, 76, 0,
}),
}}),
.address_table_lookups = try allocator.dupe(MessageAddressTableLookup, &.{.{
.account_key = try Pubkey.fromString("ZETAxsqBRek56DhiGXrn75yj2NHU3aYUnxvHXpkf3aD"),
.writable_indexes = try allocator.dupe(u8, &.{ 1, 3, 5, 7, 90 }),
.readonly_indexes = &.{},
}}),
};
}
}},
.address_table_lookups = &.{.{
.account_key = Pubkey.fromString("ZETAxsqBRek56DhiGXrn75yj2NHU3aYUnxvHXpkf3aD") catch unreachable,
.writable_indexes = &.{ 1, 3, 5, 7, 90 },
.readonly_indexes = &.{},
}},
};

pub const bincode_serialized_bytes = [_]u8{
39, 12, 102, 2, 236, 88, 117, 221, 34, 125, 55, 183, 193, 174, 21, 99, 70, 167,
Expand Down
1 change: 1 addition & 0 deletions src/crypto/base58.zig
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub fn Base58Sized(decoded_size: usize) type {

pub fn decode(str: []const u8) ![decoded_size]u8 {
var result_data: [decoded_size]u8 = undefined;
@setEvalBranchQuota(6100);
const decoded_len = try decoder.decode(str, &result_data);
if (decoded_len != decoded_size) return error.InvalidDecodedSize;
return result_data;
Expand Down

0 comments on commit 7ad3927

Please sign in to comment.