跨程序调用(CPI)每次调用都会产生 1,000 个计算单元的基础开销。对于在同一条指令中频繁接收 CPI 的程序来说,这种开销会成为显著的性能瓶颈。
为了解决这一低效问题,Dean 在 p-token 的 这个 PR 中引入了 “batch” 指令,使得可以在一次 CPI 调用中执行多个操作。
Batch 指令允许在一次 CPI 中处理多个操作,而不需要为每个操作分别发起调用。这可以显著降低在处理多个相关操作时的计算单元消耗。
Batch 指令使用了一种增强的头部结构,包含:
该头部结构使得在一个 batch 中高效地处理多个“内部”指令成为可能。系统会循环并按顺序处理每一个内部指令。
我们使用
u8::MAX(255)作为 batch 指令的判别值(discriminator)。
<ArticleSection name="Entrypoint Design" id="entrypoint-design" level="h2" />
入口函数首先会检查指令的判别值,以决定是处理 batch 指令还是普通指令。这可以防止在 batch 指令中再嵌套 batch 指令的情况出现,因为那样是不安全的。
如下所示:
#[inline(always)]
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let [discriminator, remaining @ ..] = instruction_data else {
return Err(TokenError::InvalidInstruction.into());
};
let result = if *discriminator == 255 {
// 255 - Batch
#[cfg(feature = "logging")]
pinocchio::msg!("Instruction: Batch");
process_batch(accounts, remaining)
} else {
inner_process_instruction(accounts, instruction_data)
};
result.inspect_err(log_error)
}
process_batch 函数负责处理 batch 的核心逻辑:
/// The size of the batch instruction header.
///
/// The header of each instruction consists of two `u8` values:
/// * number of the accounts
/// * length of the instruction data
const IX_HEADER_SIZE: usize = 2;
pub fn process_batch(mut accounts: &[AccountInfo], mut instruction_data: &[u8]) -> ProgramResult {
loop {
// Validates the instruction data and accounts offset.
if instruction_data.len() < IX_HEADER_SIZE {
// The instruction data must have at least two bytes.
return Err(TokenError::InvalidInstruction.into());
}
// SAFETY: The instruction data is guaranteed to have at least two bytes
// (header) + one byte (discriminator) and the values are within the bounds
// of an `usize`.
let expected_accounts = unsafe { *instruction_data.get_unchecked(0) as usize };
let data_offset = IX_HEADER_SIZE + unsafe { *instruction_data.get_unchecked(1) as usize };
if instruction_data.len() < data_offset || data_offset == IX_HEADER_SIZE {
return Err(TokenError::InvalidInstruction.into());
}
if accounts.len() < expected_accounts {
return Err(ProgramError::NotEnoughAccountKeys);
}
// Process the instruction.
// SAFETY: The instruction data and accounts lengths are already validated so
// all slices are guaranteed to be valid.
let (ix_accounts, ix_data) = unsafe {
(
accounts.get_unchecked(..expected_accounts),
instruction_data.get_unchecked(IX_HEADER_SIZE..data_offset),
)
};
inner_process_instruction(ix_accounts, ix_data)?;
if data_offset == instruction_data.len() {
// The batch is complete.
break;
}
accounts = &accounts[expected_accounts..];
instruction_data = &instruction_data[data_offset..];
}
Ok(())
}
该函数中发生的流程如下:
inner_process_instruction 函数,并传入对应的账户和数据来处理每一条内部指令。由于运行时只会在 batch 处理结束时统一检查账户所有权,因此某些指令在 batch 中执行时可能需要显式地进行所有权校验。那些不会修改账户,或已经显式执行了所有权校验的指令,则不需要额外的验证。