-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a performant way to (de-)serialize fixed-size byte arrays #127
base: main
Are you sure you want to change the base?
Conversation
First observed in est31/serde-big-array#19, there's no performant way to serialize fixed-size byte arrays with serde/postcard. Add a `FixedSizeByteArray` type that can be used to serialize fixed-size byte arrays faster than all other available methods. This type only works with postcard. A more general solution would need to be implemented in serde itself. This works around serde-rs/serde#2680. ``` serialize16/own [3.8846 ns 3.8909 ns 3.8988 ns] serialize16/bytes [9.9192 ns 9.9503 ns 9.9907 ns] serialize16/byte_array [4.1374 ns 4.1461 ns 4.1564 ns] serialize16/big_array [68.245 ns 68.344 ns 68.454 ns] serialize16/fixed_size [9.9966 ns 10.030 ns 10.076 ns] serialize16/variable_size [15.696 ns 15.795 ns 15.898 ns] serialize32/own [4.1744 ns 4.1857 ns 4.2014 ns] serialize32/bytes [9.6590 ns 9.6825 ns 9.7151 ns] serialize32/byte_array [4.5584 ns 4.5663 ns 4.5747 ns] serialize32/big_array [135.38 ns 135.78 ns 136.34 ns] serialize32/fixed_size [13.989 ns 14.034 ns 14.091 ns] serialize32/variable_size [21.223 ns 21.270 ns 21.328 ns] deserialize16/own [5.9018 ns 5.9143 ns 5.9272 ns] deserialize16/bytes [21.198 ns 21.278 ns 21.376 ns] deserialize16/byte_array [6.8560 ns 6.8719 ns 6.8934 ns] deserialize16/big_array [19.027 ns 19.098 ns 19.187 ns] deserialize16/fixed_size [7.9197 ns 7.9471 ns 7.9817 ns] deserialize16/variable_size [36.804 ns 36.871 ns 36.946 ns] deserialize32/own [8.8860 ns 8.8989 ns 8.9139 ns] deserialize32/bytes [21.241 ns 21.271 ns 21.321 ns] deserialize32/byte_array [11.441 ns 11.459 ns 11.481 ns] deserialize32/big_array [31.553 ns 31.618 ns 31.697 ns] deserialize32/fixed_size [17.334 ns 17.360 ns 17.386 ns] deserialize32/variable_size [59.705 ns 59.815 ns 59.949 ns] ``` - `own` is the new `FixedSizeByteArray`, no length prefix. - `bytes` is `serde_bytes::Bytes`, it has a length prefix. - `byte_array` is `serde_byte_array::ByteArray`, it has a length prefix. - `big_array` is `serde_big_array::Array`, no length prefix. - `fixed_size` is `[u8; _]`, no length prefix. - `variable_size` is `[u8]`, it has a length prefix.
✅ Deploy Preview for cute-starship-2d9c9b canceled.
|
This was motivated by benchmarking the serialization of a Merkle tree structure that serializes lots of hashes represented as fixed size byte arrays. The serialization of that tree has seen a >2x performance increase using this code. |
src/ser/serializer.rs
Outdated
@@ -300,8 +300,12 @@ where | |||
} | |||
|
|||
#[inline] | |||
fn serialize_struct(self, _name: &'static str, _len: usize) -> Result<Self::SerializeStruct> { | |||
Ok(self) | |||
fn serialize_struct(self, name: &'static str, _len: usize) -> Result<Self::SerializeStruct> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes me a little worried about degrading performance for all other cases to optimize specifically for the byte array case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe (have not measured) that the performance degradation is going to be minimal. The extra code is a single pointer comparison, likely optimized out because this is in monomorphized code.
It's also the same way serde_json::value::RawValue
is implemented. I believe serde_json
is a high-performance serde JSON implementation ensured by benchmarks.
Hi @hrxi, thanks for the PR! I'm sympathetic to this issue, but I'm not sure if I'm convinced this is a good solution for postcard. This adds conditionals and panicking branches to every ser/de struct field path. I think it should be possible to implement |
Yes, that would be possible. It's what e.g.
I'm not sure if this additional conditional/panic is a problem, I'd guess "no". Would you be interested in me figuring out what the impact of this is precisely? If it does have an impact, would you consider adding this |
Thanks for taking a look at the PR and being quick to answer with your feelings. :) |
I am interested in knowing the difference! https://github.com/djkoloski/rust_serialization_benchmark is likely a good baseline, it should be possible to add a "patched postcard" to do a side-by-side with stock postcard. I'm interested in poking around with what serde-big-array is doing. I wonder if you had a SPECIFIC type for bytes, rather than "any This isn't a hard no on merging this, it just doesn't sit great with me due to the special-cased nature of it. If it is really the only way and doesn't have other perf impact tradeoffs, then I'm happy to consider it further. I'll plan to poke around the "what if you do the tuple thing but specifically for bytes where you don't need the whole drop-partially-init data" stuff that serde-big-array does, unless you beat me to it. |
Note that the serialization in serde-big-array doesn't do anything weird, it's just a simple for loop, but it's still slow. |
I see what you mean, but there's a fair bit of state in Feel free to ignore my gut until I (or you!) come up with data to prove it right/wrong. |
AFAICT, this is only in the impl<'de, T, const N: usize> BigArray<'de, T> for [T; N] {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: Serializer,
T: Serialize,
{
let mut seq = serializer.serialize_tuple(self.len())?;
for elem in &self[..] {
seq.serialize_element(elem)?;
}
seq.end()
}
[…]
} I agree that the |
Not looking good from a first benchmark:
|
Moving the serialization hack from |
Would you be sympathetic to have this, or to have this as an opt-in feature? |
I'm not sure yet! I likely won't be able to poke around in this before the weekend. |
They're used less often in real code.
I'm interested in the general sentiment on this PR because depending on the result, I have to take different routes for the serialization in my project. Have you had time to look at it yet? |
Hey @hrxi, I haven't had a chance to prioritize this. I have a client deadline coming up soon, and will likely not have time to spend on this in the near future. I'm interested in hearing about your exploration in this space, but I'd generally like to keep postcard as straightforward and predictable as possible, rather than optimizing any one specific use case. If this blocks you - I'd suggest you fork postcard and use that for your project if this optimization is critical. As the wire format of postcard is stable, it should be straightforward to use |
Thanks for the quick answer!
Yup, that's definitely an option I'll consider. |
Add a FixedSizeByteArray type that can be used to serialize fixed-size byte arrays faster than all other available methods. This type only works with postcard. A more general solution would need to be implemented in serde itself. This works around serde-rs/serde#2680.
First observed in est31/serde-big-array#19, there's no performant way to serialize fixed-size byte arrays with serde/postcard.
Add a
FixedSizeByteArray
type that can be used to serialize fixed-size byte arrays faster than all other available methods. This type only works with postcard. A more general solution would need to be implemented in serde itself.This works around serde-rs/serde#2680.
own
is the newFixedSizeByteArray
, no length prefix.bytes
isserde_bytes::Bytes
, it has a length prefix.byte_array
isserde_byte_array::ByteArray
, it has a length prefix.big_array
isserde_big_array::Array
, no length prefix.fixed_size
is[u8; _]
, no length prefix.variable_size
is[u8]
, it has a length prefix.