Solana 技术训练营 2026

2026年01月09日更新 202 人订阅
课程介绍
C4: Solana 程序开发入门: 简单链上数据存储程序扩展为可交易的代币程序
C6:Anchor 入门: SPL 与 Token 2022

中间件入口点 - 为常规操作提供快速路径

中间件入口点(Middleware entrypoint)

并非所有指令都是一样的。有些指令被调用的频率远高于其他指令,从而造成性能瓶颈。

为了优先考虑效率和优化,我们需要一种不同的方式来处理这些高频指令。

这正是 “hot / cold 路径” 策略发挥作用的地方。

“hot” 路径为高频调用的指令创建了一个经过优化的入口点,其设计目标是尽可能快地进入“失败”状态,从而在必要时回退到标准的 Pinocchio 入口点。

Hot 路径

Hot 路径绕过了标准入口点中在处理前对所有账户数据进行反序列化的逻辑,而是直接使用原始数据,以获得最大的性能收益。

标准路径 vs. Hot 路径处理

在标准入口点中,加载器会将一条指令所需的所有内容打包成一个扁平的、C 风格的记录,并存储在 BPF VM 的输入页中。入口点宏会解包该记录,并提供三个安全的切片:program_id、accounts 和 instruction_data

在 Hot 路径中,由于我们完全清楚期望的数据结构,因此可以直接从原始入口点数据中检查和操作账户,从而消除不必要的反序列化开销。

原始输入结构如下所示:

pub struct Entrypoint {
    account_len: u64,
    account_info: [AccountRaw; account_len]
    instruction_len: u64,
    instruction_data: [u8; instruction_len]
    program_id: [u8; 32],
}

pub struct AccountRaw {
    is_duplicate: u8,
    is_signer: u8,
    is_writable: u8,
    executable: u8,
    alignment: u32,
    key: [u8; 32],
    owner: [u8; 32],
    lamports: u64,
    data_len: usize,
    data: [u8; data_len],
    padding: [u8; 10_240],
    alignment_padding: [u8; ?],
    rent_epoch: i64,
}

Hot 路径判别方式(Discriminators)

在设计 Hot 路径时,我们需要一种可靠的方法,能够尽可能快地判断当前指令是否应由 Hot 路径处理,或者被路由到 Cold 路径。

传统的判别方式在这里并不适用,因为判别值会随着账户数量和数据大小的不同而出现在不同的偏移位置。

我们需要检查始终位于固定偏移位置的数据。目前有两种方式可以用来判别这些指令:

  • 账户数量:由于账户数量是原始入口点输入中的第一个字段,我们可以根据传入的账户数量进行判别,例如:if *input == 4u64。 这种方式可行,因为账户数量在输入数据的最开始位置,偏移是固定的。
  • 第一个账户的公钥:由于第一个账户的公钥位于固定的偏移位置,我们可以在程序设计中始终将一个固定的密钥对(例如 authority 或某个程序账户)放在第一个位置,并基于该公钥进行判别。

目前有一个 SIMD 提案计划在寄存器 r2 中提供指令数据。这意味着在该特性实现之后,我们将能够直接使用指令判别值来构建 Hot 路径。

通过 这个 PRDean 在 Pinocchio 中引入了一个名为 middleware_entrypoint 的新入口点,使得为程序创建 Hot 路径变得更加容易。

其实现方式如下:

/// A "dummy" function with a hint to the compiler that it is unlikely to be
/// called.
///
/// This function is used as a hint to the compiler to optimize other code paths
/// instead of the one where the function is used.
#[cold]
pub const fn cold_path() {}

/// Return the given `bool` value with a hint to the compiler that `true` is the
/// likely case.
#[inline(always)]
pub const fn likely(b: bool) -> bool {
    if b {
        true
    } else {
        cold_path();
        false
    }
}

middleware_entrypoint!(hot, process_instruction);

#[inline(always)]
pub fn hot(input: *mut u8) -> u64 {
    unsafe { *input as u64 }
}

#[inline(always)]
fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {    
    match instruction_data.split_first() {
        Some((Instruction1::DISCRIMINATOR, data)) => Instruction1::try_from((data, accounts))?.process(),
        Some((Instruction2::DISCRIMINATOR, _)) => Instruction2::try_from(accounts)?.process(),
        _ => Err(ProgramError::InvalidInstructionData)
    }
}

工作原理

入口点会首先调用 “hot” 函数,检查其是否返回错误;如果返回错误,则回退到默认的 Pinocchio 入口点。这样既为常见操作提供了一条快速路径,又能保持兼容性。

#[cold] 属性用于告知编译器该函数极少被调用。在优化过程中,编译器会降低对 cold 路径代码的优先级,并将更多资源用于优化 Hot 路径。

Hot 路径设计原则

在设计 Hot 路径时,由于直接操作原始输入数据,必须进行严格的校验。任何未定义行为都有可能危及整个程序。

始终验证账户偏移和长度是否符合预期。可以使用 sbpf.xyz 来确定正确的偏移量,然后像下面这样进行校验:

if *input == 4
    && (*input.add(ACCOUNT1_DATA_LEN).cast::<u64>() == 165)
    && (*input.add(ACCOUNT2_DATA_LEN).cast::<u64>() == 82)
    && (*input.add(IX12_ACCOUNT3_DATA_LEN).cast::<u64>() == 165)
{
    //...
}

当账户的数据长度是可变的时,可以生成动态偏移量来定位指令数据,例如:

/// Align an address to the next multiple of 8.
#[inline(always)]
fn align(input: u64) -> u64 {
    (input + 7) & (!7)
}

//...

// The `authority` account can have variable data length.
    let account_4_data_len_aligned =
        align(*input.add(IX12_ACCOUNT4_DATA_LEN).cast::<u64>()) as usize;
    let offset = IX12_EXPECTED_INSTRUCTION_DATA_LEN_OFFSET + account_4_data_len_aligned;

在校验通过后,提取指令数据、转换账户类型,并按常规方式处理:

// Check that we have enough instruction data.
if input.add(offset).cast::<usize>().read() >= INSTRUCTION_DATA_SIZE {
    let discriminator = input.add(offset + size_of::<u64>()).cast::<u8>().read();

    // Check for instruction discriminator.
    if likely(discriminator == 12) {
        // instruction data length (u64) + discriminator (u8)
        let instruction_data = unsafe { from_raw_parts(input.add(offset + size_of::<u64>() + size_of::<u8>()), INSTRUCTION_DATA_SIZE - size_of::<u8>()) };

        let accounts = unsafe {
            [
                transmute::<*mut u8, AccountInfo>(input.add(ACCOUNT1_HEADER_OFFSET)),
                transmute::<*mut u8, AccountInfo>(input.add(ACCOUNT2_HEADER_OFFSET)),
                transmute::<*mut u8, AccountInfo>(input.add(IX12_ACCOUNT3_HEADER_OFFSET)),
                transmute::<*mut u8, AccountInfo>(input.add(IX12_ACCOUNT4_HEADER_OFFSET)),
            ]
        };

        return match Instruction1::try_from((instruction_data, accounts))?.process() {
            Ok(()) => SUCCESS,
            Err(error) => {
                log_error(&error);
                error.into()
            }
        };
    }
}
点赞 0
收藏 0
分享

0 条评论

请先 登录 后评论