零拷贝bytemuck与 borsh
在上一篇博文《深入理解 Serde、Bincode 与 Borsh 的关系与区别》介绍了常用的几种解析二进制数据的方法,主要有 bincode 与 borsh, 并提到过在区块链领域里一般推荐使用 borsh 解析数据。但随着合约的开发使用borsh的地方越来越多,会经常遇到提示超出 4K Stack 大小的错误。这是因为在solana里,虚拟机 sbf 限制了一个合约最大允许使用的statck大小上限为 4k。尽管我们使用完一个大变量通过一些方法,如变量作用域、通过Box将内存移动到heap、或手动drop立即释放内存。但仍有些场景是没有采用这种办法的,这时应该如何办呢?
如果经常看一些优秀的开源项目的话,会发现有一个 bytemuck
的crate,它是一个 zerocopy
库,可以避免内存复制带来的开销,加速解析数据速度,这里给出一个测试代码
use borsh::{BorshDeserialize, BorshSerialize};
use bytemuck::{Pod, Zeroable};
use solana_program::pubkey::Pubkey;
use std::time::Instant;
/// -------- 零拷贝结构 (定长布局) --------
#[repr(C, packed)]
#[derive(Clone, Copy, Pod, Zeroable, Debug)]
pub struct AccountZC {
pub lamports: u64, // 8字节
pub data: [u8; 32], // 32字节
pub owner: Pubkey, // 32字节
}
/// -------- Borsh 结构 --------
#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)]
pub struct AccountBorsh {
pub lamports: u64,
pub data: [u8; 32],
pub owner: Pubkey,
}
fn main() {
// 模拟数据
let lamports: u64 = 123456789;
let data: [u8; 32] = [7u8; 32]; // 32字节全是 7
let owner = Pubkey::new_unique();
// ---------- 用 Borsh 序列化测试数据 ----------
let account_borsh = AccountBorsh {
lamports,
data,
owner,
};
let serialized_raw = borsh::to_vec(&account_borsh).unwrap();
println!("raw序列化长度 = {}", serialized_raw.len());
println!("serialized_raw首地址 = {:p}", serialized_raw.as_ptr());
// ---------- 1. 用 Borsh 反序列化解析 ----------
let start = Instant::now();
let parsed_borsh = AccountBorsh::try_from_slice(&serialized_raw).unwrap();
println!("零拷贝解析结果 = {:?}", parsed_borsh);
println!("反序列化耗时: {:?}", start.elapsed());
println!("Borsh struct首地址 = {:p}", &parsed_borsh);
// ---------- 2. 零拷贝解析 ----------
// 因为 AccountZC 是定长布局,可以直接按字节存放
let start = Instant::now();
assert!(serialized_raw.len() >= std::mem::size_of::<AccountZC>());
let acc: &AccountZC = bytemuck::from_bytes(&mut serialized_raw.as_ref());
println!("零拷贝解析结果 = {:?}", acc);
println!("反序列化耗时: {:?}", start.elapsed(),);
println!("Bytemuck struct首地址 = {:p}", acc);
}
输出