Skip to content

Commit d40a5f3

Browse files
committed
perf(ssz): bulk memcpy for uint list/vector serialize on little-endian
On little-endian platforms, uint types (u8–u256) have the same memory layout as their SSZ encoding. Replace per-element writeInt/readInt loops with a single memcpy for FixedListType and FixedVectorType serialize/deserialize. Add canMemcpySsz() comptime helper in type_kind.zig to centralize the endianness + type check, avoiding duplication across list.zig and vector.zig. Ref: https://blog.lambdaclass.com/libssz-a-blazing-fast-zkvm-friendly-ssz-rust-library/
1 parent 1060251 commit d40a5f3

3 files changed

Lines changed: 32 additions & 0 deletions

File tree

src/ssz/type/list.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const std = @import("std");
22
const TypeKind = @import("type_kind.zig").TypeKind;
33
const isBasicType = @import("type_kind.zig").isBasicType;
44
const isFixedType = @import("type_kind.zig").isFixedType;
5+
const canMemcpySsz = @import("type_kind.zig").canMemcpySsz;
56
const OffsetIterator = @import("offsets.zig").OffsetIterator;
67
const merkleize = @import("hashing").merkleize;
78
const mixInLength = @import("hashing").mixInLength;
@@ -109,6 +110,11 @@ pub fn FixedListType(comptime ST: type, comptime _limit: comptime_int) type {
109110
}
110111

111112
pub fn serializeIntoBytes(value: *const Type, out: []u8) usize {
113+
if (comptime canMemcpySsz(Element)) {
114+
const bytes = std.mem.sliceAsBytes(value.items);
115+
@memcpy(out[0..bytes.len], bytes);
116+
return bytes.len;
117+
}
112118
var i: usize = 0;
113119
for (value.items) |element| {
114120
i += Element.serializeIntoBytes(&element, out[i..]);
@@ -123,6 +129,10 @@ pub fn FixedListType(comptime ST: type, comptime _limit: comptime_int) type {
123129
}
124130

125131
try out.resize(allocator, len);
132+
if (comptime canMemcpySsz(Element)) {
133+
@memcpy(std.mem.sliceAsBytes(out.items[0..len]), data);
134+
return;
135+
}
126136
@memset(out.items[0..len], Element.default_value);
127137
for (0..len) |i| {
128138
try Element.deserializeFromBytes(

src/ssz/type/type_kind.zig

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ pub fn isBasicType(T: type) bool {
1111
return T.kind == .uint or T.kind == .bool;
1212
}
1313

14+
const builtin = @import("builtin");
15+
const native_endian = builtin.target.cpu.arch.endian();
16+
17+
/// Whether a bulk memcpy can replace per-element serialize/deserialize.
18+
///
19+
/// SSZ encodes integers in little-endian. On little-endian platforms,
20+
/// the in-memory layout of uint types already matches SSZ encoding,
21+
/// so a single memcpy replaces N individual writeInt/readInt calls.
22+
pub fn canMemcpySsz(comptime T: type) bool {
23+
return T.kind == .uint and native_endian == .little;
24+
}
25+
1426
// Fixed-size types have a known size
1527
pub fn isFixedType(T: type) bool {
1628
return switch (T.kind) {

src/ssz/type/vector.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const std = @import("std");
22
const TypeKind = @import("type_kind.zig").TypeKind;
33
const isBasicType = @import("type_kind.zig").isBasicType;
44
const isFixedType = @import("type_kind.zig").isFixedType;
5+
const canMemcpySsz = @import("type_kind.zig").canMemcpySsz;
56
const OffsetIterator = @import("offsets.zig").OffsetIterator;
67
const merkleize = @import("hashing").merkleize;
78
const maxChunksToDepth = @import("hashing").maxChunksToDepth;
@@ -76,6 +77,11 @@ pub fn FixedVectorType(comptime ST: type, comptime _length: comptime_int) type {
7677
}
7778

7879
pub fn serializeIntoBytes(value: *const Type, out: []u8) usize {
80+
if (comptime canMemcpySsz(Element)) {
81+
const bytes = std.mem.sliceAsBytes(value);
82+
@memcpy(out[0..fixed_size], bytes);
83+
return fixed_size;
84+
}
7985
var i: usize = 0;
8086
for (value) |element| {
8187
i += Element.serializeIntoBytes(&element, out[i..]);
@@ -88,6 +94,10 @@ pub fn FixedVectorType(comptime ST: type, comptime _length: comptime_int) type {
8894
return error.InvalidSize;
8995
}
9096

97+
if (comptime canMemcpySsz(Element)) {
98+
@memcpy(std.mem.sliceAsBytes(out), data[0..fixed_size]);
99+
return;
100+
}
91101
for (0..length) |i| {
92102
try Element.deserializeFromBytes(
93103
data[i * Element.fixed_size .. (i + 1) * Element.fixed_size],

0 commit comments

Comments
 (0)