新消息:Helius 收购 Light Protocol
优化 Solana 程序
博客/开发

优化 Solana 程序

集成工程师X 上的 Het DagliLinkedIn 上的 Het Dagli
阅读需 12 分钟

可执行要点

  • 对大型数据结构和高频操作使用零拷贝反序列化
  • 使用 nostd_entrypoint,而不是臃肿的 solana_program’s 入口点
  • 尽量减少动态分配,优先使用基于栈的数据结构
  • 实现自定义序列化/反序列化,避免 Borsh 开销
  • 使用 #[inline(always)] 标记关键函数,以获得潜在的性能提升
  • 使用位操作高效解析指令
  • 使用 Solana 专用的 C 系统调用,例如 sol_invoke_signed_c
  • 测量计算单元用量,为优化工作提供依据

简介

Solana 开发者在编写程序时需要做出多项选择:如何平衡易用性、性能和安全性。可选方案的一端是易于使用的 Anchor 框架,它简化了开发,但会带来一些额外开销;另一端则是使用 unsafe Rust 和直接系统调用的底层方案。后者能提供极致性能,但复杂度和潜在安全风险也更高。开发者需要考虑的关键问题不仅是如何优化,还有何时优化以及优化到什么程度。

本文将深入探讨这些选项,为开发者提供一份了解各种优化方案的路线图。我们将研究以下抽象层级:

  1. Anchor:大多数开发者首选的强大、高层级且约定明确的框架
  2. 使用零拷贝的 Anchor:针对大型数据结构优化的 Anchor 代码
  3. 纯 Rust,在控制能力和易用性之间取得平衡
  4. 使用直接系统调用(syscalls)的 Unsafe Rust:挑战性能极限

本文并非要给出一种适用于所有场景的方案,而是帮助开发者掌握必要知识,根据具体用例做出明智的程序编码决策。

读完本文后,你将更清楚地理解这些不同抽象层级,以及何时应沿着优化路径进一步深入。请记住,优化程度最高的代码不一定是最佳方案,关键在于找到适合项目需求的平衡点。

本文假设你熟悉 Rust 基础、Solana 的账户模型和 Anchor 框架。 

如果你想先看结论:

计算单元

Solana 的高性能架构依赖高效的资源管理。该系统的核心是计算单元(CU),用于衡量验证者处理给定交易时消耗的计算资源。

‍为什么要关注计算单元?

  1. **交易成功率:**每笔交易都有 CU 上限,超出上限会导致交易失败
  2. **成本效率:**CU 用量越低,交易费用越低
  3. **用户体验:**优化后的程序执行速度更快,可提升整体用户体验
  4. **可扩展性:**高效的程序让每个区块能够容纳更多交易,从而提高网络吞吐量

测量计算单元

solana_program::log::sol_log_compute_units() 系统调用会记录程序执行到特定位置时所消耗的计算单元数量。

下面是一个使用该系统调用的简单 compute_fn! 宏实现:

代码
#[macro_export]
macro_rules! compute_fn {
    ($msg:expr=> $($tt:tt)*) => {
        ::solana_program::msg!(concat!($msg, " {"));
        ::solana_program::log::sol_log_compute_units();
        let res = { $($tt)* };
        ::solana_program::log::sol_log_compute_units();
        ::solana_program::msg!(concat!(" } // ", $msg));
        res
    };
}

该宏取自用于 CU 优化的 Solana Developers GitHub 仓库。这段代码实现了一个计数器程序,其中包含两条指令:initialize 和 increment

在本文中,我们将用四种不同方式编写具有相同两条指令 initialize 和 increment 的计数器程序,并比较各自的 CU 用量:Anchor、使用零拷贝反序列化的 Anchor、原生 Rust 和 unsafe Rust

初始化账户并对其进行小幅修改(本例中是递增),是比较这些不同方案的合理基准。目前我们不会使用 PDA。

如果你想先看结论,下面是四种方案的 CU 对比:

开始吧……

零拷贝反序列化

零拷贝反序列化让我们无需分配新内存或复制数据,就能直接解释账户数据。这项技术可以降低 CPU 和内存用量,并有望提高指令效率。

先从一个基础的 Anchor 计数器程序开始:‍

代码
use anchor_lang::prelude::*;

declare_id!("37oUa3WkeqwnFxSCqyMnpC3CfTSwtvyJxnwYQc3u6U7C");

#[program]
pub mod counter {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        counter.count = 0;
        Ok(())
    }

    pub fn increment(ctx: Context<Update>) -> Result<()> {
        let counter = &mut ctx.accounts.counter;
        //Not doing checked_add, wrapping add or any overflow checks
        //to keep it simple
        counter.count += 1;
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = user, space = 8 + 8)]
    pub counter: Account<'info, Counter>,
    #[account(mut)]
    pub user: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Update<'info> {
    #[account(mut)]
    pub counter: Account<'info, Counter>,
    pub user: Signer<'info>,
}

#[account]
pub struct Counter {
    pub count: u64,
}

上面的代码很普通。现在使用 zero_copy 来升级它:

代码
use anchor_lang::prelude::*;

declare_id!("7YkAh5yHbLK4uZSxjGYPsG14VUuDD6RQbK6k4k3Ji62g");

#[program]
pub mod counter {
 use super::*;

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
 let mut counter = ctx.accounts.counter.load_init()?;
 counter.count = 0;
 Ok(())
 }

pub fn increment(ctx: Context<Update>) -> Result<()> {
 let mut counter = ctx.accounts.counter.load_mut()?;
 counter.count += 1;
 Ok(())
 }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
 #[account(init, payer = user, space = 8 + std::mem::size_of::<CounterData>())]
 pub counter: AccountLoader<'info, CounterData>,
 #[account(mut)]
 pub user: Signer<'info>,
 pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Update<'info> {
 #[account(mut)]
 pub counter: AccountLoader<'info, CounterData>,
 pub user: Signer<'info>,
}

#[account(zero_copy)]
pub struct CounterData {
 pub count: u64,
}

主要改动:

我们做了以下主要改动:

1. 使用 AccountLoader 而不是 Account

现在我们使用 AccountLoader<’info, CounterData>,而不是 Account<’info, Counter>。这样就能以零拷贝方式访问账户数据。

2. 零拷贝属性

CounterData 上的 #[account(zero_copy)] 属性表示,可以直接根据内存中的原始字节来解释该结构体。

3. 直接访问数据

在 initialize 和 increment 函数中,我们分别使用 load_init() 和 load_mut(),在不复制数据的情况下获得账户数据的可变访问权限

4. 缓解重复账户漏洞 

零拷贝反序列化可以解决 Borsh 序列化中的一个潜在漏洞。使用 Borsh 时,系统会创建并修改账户的独立副本,然后将其复制回同一地址。如果同一账户在一笔交易中多次出现,这个过程可能导致数据不一致。

而零拷贝会直接在同一内存地址上读取和写入。这能确保一笔交易中对某个账户的所有引用都操作相同的数据,消除重复账户导致数据不一致的风险。

5. 内存布局保证

zero_copy 属性确保 CounterData 具有一致的内存布局,因此可以安全地根据原始字节重新解释数据。这个实现将 initialize 指令的 CU 用量从 5095 降至 5022,并将 increment 指令的 CU 用量从 1162 降至 1124。

在我们的用例中,零拷贝带来的改进很小,基本可以忽略不计。不过,在处理大型数据结构时,零拷贝反序列化可能很有用。这是因为在处理存储复杂或大量数据的账户时,它能显著降低 CPU 和内存用量

权衡与注意事项

零拷贝也面临一些挑战:

1. 复杂度增加

代码会略微复杂,需要谨慎处理原始数据

2. 兼容性

并非所有数据结构都适合零拷贝反序列化,它们必须拥有可预测的内存布局。例如,包含 Vec 或 String 等动态大小字段的结构与零拷贝反序列化不兼容。

是否使用零拷贝应取决于你的具体用例。对于我们的计数器这类简单程序,收益可能很小。但随着程序日益复杂并开始处理更大的数据结构,零拷贝可以成为强大的优化工具。

尽管零拷贝优化并未显著改善这个简单的计数器程序,但效率优化之路并未就此结束。接下来探索另一种方案:不使用 Anchor 框架,直接用 Rust 编写原生 Solana 程序。这种方式提供了更多控制能力和优化空间,但复杂度也更高。

使用原生 Rust

原生 Rust 程序提供更底层的接口,开发者需要自行处理 Anchor 自动完成的各种任务,包括账户反序列化、序列化和各类安全检查。虽然这对开发者提出了更高要求,但也为精细优化创造了机会。

下面看看计数器程序的原生 Rust 实现:‍

代码
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    program_error::ProgramError,
    pubkey::Pubkey,
    rent::Rent,
    system_instruction,
    program::invoke,
    sysvar::Sysvar,
};
use std::mem::size_of;

// Define the state struct
struct Counter {
    count: u64,
}

// Declare and export the program's entrypoint
entrypoint!(process_instruction);

// Program entrypoint's implementation
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instruction = instruction_data
        .get(0)
        .ok_or(ProgramError::InvalidInstructionData)?;

    match instruction {
        0 => initialize(program_id, accounts),
        1 => increment(accounts),
        _ => Err(ProgramError::InvalidInstructionData),
    }
}

fn initialize(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let counter_account = next_account_info(account_info_iter)?;
    let user = next_account_info(account_info_iter)?;
    let system_program = next_account_info(account_info_iter)?;

    if !user.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    if counter_account.owner != program_id {
        let rent = Rent::get()?;
        let space = size_of::<Counter>();
        let rent_lamports = rent.minimum_balance(space);

        invoke(
            &system_instruction::create_account(
                user.key,
                counter_account.key,
                rent_lamports,
                space as u64,
                program_id,
            ),
            &[user.clone(), counter_account.clone(), system_program.clone()],
        )?;
    }

    let mut counter_data = Counter { count: 0 };
    counter_data.serialize(&mut &mut counter_account.data.borrow_mut()[..])?;

    Ok(())
}

fn increment(accounts: &[AccountInfo]) -> ProgramResult {
    let account_info_iter = &mut accounts.iter();
    let counter_account = next_account_info(account_info_iter)?;
    let user = next_account_info(account_info_iter)?;

    if !user.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
    }

    let mut counter_data = Counter::deserialize(&counter_account.data.borrow())?;

    //Not doing checked_add, wrapping add or any overflow checks to keep it simple
    counter_data.count += 1;
    counter_data.serialize(&mut &mut counter_account.data.borrow_mut()[..])?;

    Ok(())
}

impl Counter {
    fn serialize(&self, data: &mut [u8]) -> ProgramResult {
        if data.len() < size_of::<Self>() {
            return Err(ProgramError::AccountDataTooSmall);
        }

        //First 8 bytes is the count
        data[..8].copy_from_slice(&self.count.to_le_bytes());
        Ok(())
    }

    fn deserialize(data: &[u8]) -> Result<Self, ProgramError> {
        if data.len() < size_of::<Self>() {
            return Err(ProgramError::AccountDataTooSmall);
        }

        //First 8 bytes is the count
        let count = u64::from_le_bytes(data[..8].try_into().unwrap());
        Ok(Self { count })
    }
}

主要差异与注意事项

以下几点值得注意:

1. 手动解析指令

Anchor 会自动路由指令,而我们需要手动解析指令数据,并将其路由到相应函数。

代码
let instruction = instruction_data
        .get(0)
        .ok_or(ProgramError::InvalidInstructionData)?;

    match instruction {
        0 => initialize(program_id, accounts),
        1 => increment(accounts),
        _ => Err(ProgramError::InvalidInstructionData),
    }

2. 账户管理

我们使用 next_account_info 遍历账户,并手动检查签名者和所有者。Anchor 会通过 #[derive(Accounts)] 宏自动完成这些操作。

代码
let account_info_iter = &mut accounts.iter();
    let counter_account = next_account_info(account_info_iter)?;
    let user = next_account_info(account_info_iter)?;

    if !user.is_signer {
        return Err(ProgramError::MissingRequiredSignature);
}

3. 自定义序列化

我们为 Counter 结构体实现自定义的 serialize 和 deserialize 方法。Anchor 默认使用 Borsh 序列化,隐藏了这些细节。

代码
impl Counter {
    fn serialize(&self, data: &mut [u8]) -> ProgramResult {
        if data.len() < size_of::<Self>() {
            return Err(ProgramError::AccountDataTooSmall);
        }

        //First 8 bytes is the count
        data[..8].copy_from_slice(&self.count.to_le_bytes());
        Ok(())
    }

    fn deserialize(data: &[u8]) -> Result<Self, ProgramError> {
        if data.len() < size_of::<Self>() {
            return Err(ProgramError::AccountDataTooSmall);
        }

        //First 8 bytes is the count
        let count = u64::from_le_bytes(data[..8].try_into().unwrap());
        Ok(Self { count })
    }
}

4. 与系统程序交互

创建账户需要使用 invoke 与系统程序直接交互,并执行跨程序调用(CPI)。Anchor 通过 init 约束简化了这个过程:

代码
invoke(
      &system_instruction::create_account(
          user.key,
          counter_account.key,
          rent_lamports,
          space as u64,
          program_id,
      ),
      &[user.clone(), counter_account.clone(), system_program.clone()],
)?;

5. 精细控制

总体而言,原生程序不受单一约定式框架的限制,可以更好地控制数据布局和处理流程,从而编写出优化程度更高的代码。

如何看待 Anchor 与原生 Rust

以下几点值得思考:

1. 显式与隐式

原生程序要求显式处理许多由 Anchor 隐式管理的方面,包括账户验证、序列化和指令路由

2. 安全注意事项

如果没有 Anchor 的内置检查,开发者必须谨慎实施适当的安全措施,例如检查账户所有权和签名者状态

3. 性能调优

原生程序支持更精细的性能优化,但要求开发者更深入地理解 Solana 的运行时行为

4. 样板代码

对于 Anchor 已抽象封装的常见操作,你需要编写更多样板代码

5. 学习曲线

原生编程可能效率更高,但学习曲线也更陡峭,并且要求开发者更深入地了解 Solana 架构

简而言之

从 Anchor 转向原生 Rust 时,最大的限制因素是序列化和反序列化的处理。在我们的用例中,这相对简单。但随着状态管理日益复杂,处理难度也会不断提高。

不过,Anchor 使用的 Borsh 确实会消耗大量计算资源,因此付出这些努力是值得的。

我们的优化之旅并未到此结束。下一节将进一步突破边界,使用直接系统调用并避开 Rust 标准库。

这种方案很有挑战性,但我保证,它能帮助你深入了解 Solana 运行时的内部工作原理。

使用 Unsafe Rust 和直接系统调用挑战极限

为了将计数器程序的性能推向极限,我们现在来探索 unsafe Rust 和直接系统调用。Unsafe Rust 允许开发者绕过标准安全检查,直接操作内存并进行底层优化。同时,系统调用提供了直接访问 Solana 运行时的接口。

这种方案很复杂,需要严谨开发,但能显著节省 CU。不过,它也要求开发者更深入地理解 Solana 架构,并格外重视程序安全。潜在的性能提升十分可观,但也意味着更大的责任。

下面看看计数器程序的高度优化版本,它使用了这些高级技术:

代码
use solana_nostd_entrypoint::{
    basic_panic_impl, entrypoint_nostd, noalloc_allocator,
    solana_program::{
        entrypoint::ProgramResult, log, program_error::ProgramError, pubkey::Pubkey, system_program,
    },
    InstructionC, NoStdAccountInfo,
};

entrypoint_nostd!(process_instruction, 32);

pub const ID: Pubkey = solana_nostd_entrypoint::solana_program::pubkey!(
    "EgB1zom79Ek4LkvJjafbkUMTwDK9sZQKEzNnrNFHpHHz"
);

noalloc_allocator!();
basic_panic_impl!();

const ACCOUNT_DATA_LEN: usize = 8; // 8 bytes for u64 counter

/*
 * Program Entrypoint
 * ------------------
 * Entrypoint receives:
 * - program_id: The public key of the program's account
 * - accounts: An array of accounts required for the instruction
 * - instruction_data: A byte array containing the instruction data
 *
 * Instruction data format:
 * ------------------------
 * | Bit 0 | Bits 1-7 |
 * |-------|----------|
 * |  0/1  |  Unused  |
 *
 * 0: Initialize
 * 1: Increment
 */
#[inline(always)]
pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[NoStdAccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {

    if instruction_data.is_empty() {
        return Err(ProgramError::InvalidInstructionData);
    }

    // Use the least significant bit to determine the instruction
    match instruction_data[0] & 1 {
        0 => initialize(accounts),
        1 => increment(accounts),
        _ => unreachable!(),
    }
}

/*
 * Initialize Function
 * -------------------
 * This function initializes a new counter account.
 *
 * Account structure:
 * ------------------
 * 1. Payer account (signer, writable)
 * 2. Counter account (writable)
 * 3. System program
 *
 * Memory layout of instruction_data:
 * -----------------------------------------
 * | Bytes    | Content                     |
 * |----------|----------------------------|
 * | 0-3      | Instruction discriminator  |
 * | 4-11     | Required lamports (u64)    |
 * | 12-19    | Space (u64)                |
 * | 20-51    | Program ID                 |
 * | 52-55    | Unused                     |
 */
#[inline(always)]
fn initialize(accounts: &[NoStdAccountInfo]) -> ProgramResult {

    let [payer, counter, system_program] = match accounts {
        [payer, counter, system_program, ..] => [payer, counter, system_program],
        _ => return Err(ProgramError::NotEnoughAccountKeys),
    };

    if counter.key() == &system_program::ID {
        return Err(ProgramError::InvalidAccountData);
    }

    let rent = solana_program::rent::Rent::default();
    let required_lamports = rent.minimum_balance(ACCOUNT_DATA_LEN);

    let mut instruction_data = [0u8; 56];
    instruction_data[4..12].copy_from_slice(&required_lamports.to_le_bytes());
    instruction_data[12..20].copy_from_slice(&(ACCOUNT_DATA_LEN as u64).to_le_bytes());
    instruction_data[20..52].copy_from_slice(ID.as_ref());

    let instruction_accounts = [
        payer.to_meta_c(),
        counter.to_meta_c(),
    ];

    let instruction = InstructionC {
        program_id: &system_program::ID,
        accounts: instruction_accounts.as_ptr(),
        accounts_len: instruction_accounts.len() as u64,
        data: instruction_data.as_ptr(),
        data_len: instruction_data.len() as u64,
    };

    let infos = [payer.to_info_c(), counter.to_info_c()];

    // Invoke system program to create account
    #[cfg(target_os = "solana")]
    unsafe {
        solana_program::syscalls::sol_invoke_signed_c(
            &instruction as *const InstructionC as *const u8,
            infos.as_ptr() as *const u8,
            infos.len() as u64,
            std::ptr::null(),
            0,
        );
    }

    // Initialize counter to 0
    let mut counter_data = counter.try_borrow_mut_data().ok_or(ProgramError::AccountBorrowFailed)?;
    counter_data[..8].copy_from_slice(&0u64.to_le_bytes());

    Ok(())
}

/*
 * Increment Function
 * ------------------
 * This function increments the counter in the counter account.
 *
 * Account structure:
 * ------------------
 * 1. Counter account (writable)
 * 2. Payer account (signer)
 *
 * Counter account data layout:
 * ----------------------------
 * | Bytes | Content        |
 * |-------|----------------|
 * | 0-7   | Counter (u64)  |
 */
#[inline(always)]
fn increment(accounts: &[NoStdAccountInfo]) -> ProgramResult {

    let [counter, payer] = match accounts {
        [counter, payer, ..] => [counter, payer],
        _ => return Err(ProgramError::NotEnoughAccountKeys),
    };

    if !payer.is_signer() || counter.owner() != &ID {
        return Err(ProgramError::IllegalOwner);
    }

    let mut counter_data = counter.try_borrow_mut_data().ok_or(ProgramError::AccountBorrowFailed)?;

    if counter_data.len() != 8 {
        return Err(ProgramError::UninitializedAccount);
    }

    let mut value = u64::from_le_bytes(counter_data[..8].try_into().unwrap());
    value += 1;
    counter_data[..8].copy_from_slice(&value.to_le_bytes());

    Ok(())
}

主要差异与优化

下面看看具体做了哪些优化:

1. No-std 环境

我们使用 solana_nostd_entrypoint,它提供 no-std 环境。这可以消除 Rust 标准库的开销,减小程序体积,并可能提升性能。感谢 cavemanloverboy 及其关于 Solana 程序 no-std 入口点的 GitHub 仓库。

2. 内联函数

关键函数使用 #[inline(always)] 标记。内联是一种编译器优化方式,会将函数体插入调用位置,从而消除函数调用开销。这样可以加快执行速度,尤其适合小型且频繁调用的函数。

3. 使用位操作解析指令

我们使用位操作instruction_data[0] & 1 来确定指令类型,这可能比其他解析方法更高效:

代码
// Use the least significant bit to determine the instruction
    match instruction_data[0] & 1 {
        0 => initialize(accounts),
        1 => increment(accounts),
        _ => unreachable!(),
 }

4. 零成本内存管理与最简 panic 处理

noalloc_allocator! 和 basic_panic_impl! 宏实现了最简、零开销的内存管理和 panic 处理:

Noalloc_allocator! 定义了一个自定义分配器:任何分配尝试都会触发 panic,而释放内存时不执行任何操作。将其设为 Solana 程序的全局分配器,可以有效阻止运行时进行任何动态内存分配:

代码
#[macro_export]
macro_rules! noalloc_allocator {
    () => {
        pub mod allocator {
            pub struct NoAlloc;
            extern crate alloc;
            unsafe impl alloc::alloc::GlobalAlloc for NoAlloc {
                #[inline]
                unsafe fn alloc(&self, _: core::alloc::Layout) -> *mut u8 {
                    panic!("no_alloc :)");
                }
                #[inline]
                unsafe fn dealloc(&self, _: *mut u8, _: core::alloc::Layout) {}
            }

            #[cfg(target_os = "solana")]
            #[global_allocator]
            static A: NoAlloc = NoAlloc;
        }
    };
}

这一点至关重要,原因如下:

  1. 它消除了内存分配和释放操作的开销
  2. 它迫使开发者使用基于栈或静态内存,这类内存通常速度更快,性能也更可预测
  3. 它减少了程序的内存占用

basic_panic_impl! 提供一个最简 panic 处理程序,只会记录一条“panicked!”消息:

代码
#[macro_export]
macro_rules! basic_panic_impl {
    () => {
        #[cfg(target_os = "solana")]
        #[no_mangle]
        fn custom_panic(_info: &core::panic::PanicInfo<'_>) {
            log::sol_log("panicked!");
        }
    };
}

5. 高效准备 CPI

InstructionC 结构体以及 to_meta_c 和 to_info_c 函数,提供了底层且高效的 CPI 数据准备方式:

代码
let instruction_accounts = [
        payer.to_meta_c(),
        counter.to_meta_c(),
    ];

    let instruction = InstructionC {
        program_id: &system_program::ID,
        accounts: instruction_accounts.as_ptr(),
        accounts_len: instruction_accounts.len() as u64,
        data: instruction_data.as_ptr(),
        data_len: instruction_data.len() as u64,
 	};

 let infos = [payer.to_info_c(), counter.to_info_c()
];

这些函数会创建与 C 兼容的结构体,可将其直接传给 sol_invoke_signed_c 系统调用。通过避开 Rust 高层抽象的开销,直接操作原始指针和与 C 兼容的结构体,这些函数可以最大限度降低 CPI 准备过程的计算成本。

这种方案减少了使用更抽象的 Rust 类型时通常产生的内存分配、复制和转换,因此能够节省 CU。

例如,to_info_c 方法使用直接指针运算高效构造 AccountInfoC 结构体:

代码
pub fn to_info_c(&self) -> AccountInfoC {
  AccountInfoC {
  key: offset(self.inner, 8),
  lamports: offset(self.inner, 72),
  data_len: self.data_len() as u64,
  data: offset(self.inner, 88),
  owner: offset(self.inner, 40),
  // … other fields …
  }
}

直接操作内存布局,可以极其高效地创建 CPI 所需的结构体,从而降低这些操作的 CU 成本。

6. 直接系统调用与 Unsafe Rust

这种方案绕过常规 Rust 抽象,直接与 Solana 运行时交互,能显著提升性能。但它也增加了复杂度,并要求谨慎处理 unsafe Rust:

代码
// Invoke system program to create account
#[cfg(target_os = "solana")]
unsafe {
    solana_program::syscalls::sol_invoke_signed_c(
        &instruction as *const InstructionC as *const u8,
        infos.as_ptr() as *const u8,
        infos.len() as u64,
        std::ptr::null(),
        0,
    );
}

7. 条件编译:

#[cfg(target_os = “solana”)] 属性确保这段代码只在以 Solana 运行时为目标时编译。这是必要的,因为这些系统调用仅在该环境中可用。

Unsafe Rust 的潜在问题 

Unsafe Rust 虽然强大,但如果处理不当,可能引发严重问题:

  • 内存泄漏和损坏
  • 未定义行为
  • 竞态条件

使用 unsafe Rust 时,可以采取以下措施降低风险:

  • 尽量少用 unsafe 块,只在必要时使用
  • 记录所有安全假设和不变量
  • 使用 Miri 和 Rust 内置的消毒器等工具进行测试
  • 对关键部分考虑采用形式化验证技术
  • 开展全面的代码审查,重点检查 unsafe 块

简而言之

虽然这一切都很有意思,但要在保护真实资金的生产级程序中采用这种高度优化的方案并不容易,因为它会增加复杂度、出错概率和维护难度。对于大多数应用而言,引入严重错误的风险往往超过性能收益。

采用这种方案,很可能让你落入过早优化的陷阱。

‍不过,其中有些做法很容易复用:

  1. 使用 nostd_entrypoint,而不是由 solana_program 提供的臃肿 entrypoint
  2. 尽可能使用内联函数
  3. 尽量减少动态分配,优先使用基于栈的数据结构‍

总结 

本文探讨了 Solana 程序的多种优化层级,从高层级的 Anchor 开发,到使用直接系统调用的底层 unsafe Rust。我们看到,每种方案都需要在易用性、安全性和性能之间做出不同权衡。‍

主要结论:

  • Anchor 提供易于使用的框架,但存在一定性能开销
  • 零拷贝反序列化可以显著提高大型数据结构的处理效率
  • 原生 Rust 提供更多控制能力和优化空间
  • Unsafe Rust 和直接系统调用能提供最高性能,但复杂度和风险也更高

你应根据具体用例、性能要求和风险承受能力选择优化层级。务必衡量优化带来的实际影响,并考虑所做选择对长期维护的影响。

如果你读到了这里,感谢你,匿名朋友!请务必在下方输入电子邮箱地址,这样就不会错过 Solana 的任何最新动态。准备继续深入探索?立即阅读 Helius 博客上的最新文章,继续你的 Solana 之旅。

其他资源

订阅 Helius

及时了解 Solana 开发的最新动态,并在我们发布新内容时收到更新

放大图片