
Otimizando programas Solana
Insights práticos
- Use desserialização zero-copy para estruturas de dados grandes e operações de alta frequência
- Use
nostd_entrypointem vez do entrypoint inchadosolana_program’s - Minimize alocações dinâmicas, priorizando estruturas de dados baseadas na stack
- Implemente serialização/desserialização personalizada para evitar o overhead do Borsh
- Marque funções críticas com
#[inline(always)]para obter possíveis ganhos de desempenho - Use manipulação de bits para fazer o parsing eficiente de instruções
- Use syscalls C específicas da Solana, como
sol_invoke_signed_c - Meça o uso de unidades computacionais para orientar os esforços de otimização
Introdução
Ao escrever programas, os desenvolvedores Solana precisam tomar várias decisões para equilibrar facilidade de uso, desempenho e segurança. Esse espectro vai do framework Anchor, acessível e fácil de usar, que simplifica o desenvolvimento ao custo de algum overhead, até abordagens de baixo nível que usam Rust unsafe e syscalls diretas. Embora a segunda opção ofereça desempenho máximo, ela traz maior complexidade e possíveis riscos de segurança. A principal questão para os desenvolvedores não é apenas como otimizar, mas quando e em que medida.
Este artigo explora essas opções em detalhes e oferece um roteiro para ajudar os desenvolvedores a navegar pelo cenário da otimização. Analisaremos os seguintes níveis de abstração:
- Anchor: o framework de alto nível, poderoso e opinativo mais usado pela maioria dos desenvolvedores
- Anchor com zero-copy: código Anchor escrito para otimizar estruturas de dados grandes
- Rust puro para equilibrar controle e facilidade de uso
- Rust unsafe com chamadas de sistema (syscalls) diretas: levando o desempenho ao limite
O objetivo não é prescrever uma solução universal, mas oferecer aos desenvolvedores o conhecimento necessário para tomar decisões fundamentadas sobre como programar com base em seus casos de uso específicos.
Ao final deste artigo, você entenderá melhor como avaliar esses diferentes níveis de abstração e quando considerar avançar pelo caminho da otimização. Lembre-se: o código mais otimizado nem sempre é a melhor solução — o importante é encontrar o equilíbrio certo para as necessidades do seu projeto.
Este artigo pressupõe familiaridade com conceitos básicos de Rust, o modelo de contas da Solana e o framework Anchor.
Para quem está com pressa:
Unidades computacionais
A arquitetura de alto desempenho da Solana depende de uma gestão eficiente de recursos. No centro desse sistema estão as unidades computacionais (CUs), uma medida dos recursos computacionais gastos pelos validadores para processar determinada transação.
Por que se importar com unidades computacionais?
- Sucesso da transação: cada transação tem um limite de CUs. Excedê-lo causa a falha da transação
- Eficiência de custos: usar menos CUs significa pagar taxas de transação menores
- Experiência do usuário: programas otimizados são executados mais rapidamente, melhorando a experiência geral
- Escalabilidade: programas eficientes permitem mais transações por bloco, aumentando a capacidade de processamento da rede
Como medir unidades computacionais
A syscall solana_program::log::sol_log_compute_units() registra o número de unidades computacionais que um programa consome em um ponto específico da execução.
Veja uma implementação simples da macro compute_fn! usando a syscall:
#[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
};
}Essa macro foi obtida no repositório do Solana Developers no GitHub para otimizações de CUs. Esse trecho de código implementa um programa de contador com duas instruções, initialize e increment
Neste artigo, escreveremos o mesmo programa de contador com as mesmas duas instruções, initialize e increment, de quatro maneiras diferentes e compararemos o uso de CUs de todas elas: Anchor, Anchor com desserialização zero-copy, Rust nativo e Rust unsafe
Inicializar uma conta e fazer uma pequena alteração nela — neste caso, incrementá-la — é um bom benchmark para comparar essas diferentes abordagens. Por enquanto, não usaremos PDAs.
Para quem está com pressa, estas são as comparações de CUs entre as quatro abordagens:
Vamos começar…
Desserialização zero-copy
A desserialização zero-copy permite interpretar diretamente os dados da conta sem alocar nova memória nem copiar dados. Essa técnica pode reduzir o uso de CPU e o consumo de memória, além de potencialmente gerar instruções mais eficientes.
Vamos começar com um programa básico de contador em 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,
}Nada sofisticado acima. Agora vamos aprimorá-lo com 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,
}Principais mudanças:
Estas são as principais mudanças que fizemos:
1. AccountLoader em vez de Account
Agora usamos AccountLoader<’info, CounterData> em vez de Account<’info, Counter>. Isso permite acesso zero-copy aos dados da conta.
2. Atributo zero-copy
O atributo #[account(zero_copy)] em CounterData indica que essa struct pode ser interpretada diretamente a partir dos bytes brutos na memória.
3. Acesso direto aos dados
Nas funções initialize e increment, usamos load_init() e load_mut(), respectivamente, para obter acesso mutável aos dados da conta sem copiá-los
4. Mitigação de vulnerabilidades de contas duplicadas
A desserialização zero-copy resolve uma possível vulnerabilidade presente na serialização Borsh. Com Borsh, cópias distintas das contas são criadas e alteradas, sendo depois copiadas de volta para o mesmo endereço. Esse processo pode gerar inconsistências se a mesma conta for incluída várias vezes em uma transação.
Já o zero-copy lê e grava diretamente no mesmo endereço de memória. Essa abordagem garante que todas as referências a uma conta em uma transação operem sobre os mesmos dados, eliminando o risco de inconsistências causadas por contas duplicadas.
5. Garantias de layout de memória
O atributo zero_copy garante que CounterData tenha um layout de memória consistente, permitindo a reinterpretação segura de bytes brutos. Essa implementação reduziu o uso de CUs da instrução initialize de 5095 para 5022 e da instrução increment de 1162 para 1124.
Em nosso caso, o zero-copy gera melhorias mínimas e pouco significativas. No entanto, a desserialização zero-copy pode ser útil ao trabalhar com estruturas de dados grandes. Isso ocorre porque ela pode reduzir substancialmente o uso de CPU e memória ao lidar com contas que armazenam dados complexos ou extensos
Trade-offs e considerações
O zero-copy também apresenta desafios:
1. Maior complexidade
O código fica um pouco mais complexo e exige cuidado ao manipular dados brutos
2. Compatibilidade
Nem todas as estruturas de dados são adequadas para desserialização zero-copy — elas precisam ter um layout de memória previsível. Por exemplo, estruturas com campos de tamanho dinâmico, como Vec ou String, são incompatíveis com a desserialização zero-copy.
A decisão de usar zero-copy deve se basear no seu caso de uso específico. Para programas simples como nosso contador, os benefícios podem ser mínimos. Porém, à medida que seus programas ficam mais complexos e processam estruturas de dados maiores, o zero-copy pode se tornar uma ferramenta poderosa de otimização.
Embora a otimização zero-copy não tenha gerado melhorias significativas em nosso programa de contador simples, a busca por eficiência não termina aqui. Vamos explorar outro caminho: escrever programas Solana nativos em Rust sem o framework Anchor. Essa abordagem oferece mais controle e potencial de otimização, embora aumente a complexidade.
Usando Rust nativo
Programas em Rust nativo oferecem uma interface de nível mais baixo e exigem que os desenvolvedores executem as diversas tarefas automatizadas pelo Anchor. Isso inclui desserialização e serialização de contas, além de várias verificações de segurança. Embora exija mais do desenvolvedor, essa abordagem também cria oportunidades para otimizações refinadas.
Vamos analisar a implementação em Rust nativo do nosso programa de contador:
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 })
}
}Principais diferenças e considerações
Veja alguns pontos a considerar:
1. Parsing manual de instruções
Diferentemente do Anchor, que encaminha as instruções automaticamente, fazemos manualmente o parsing dos dados da instrução e os encaminhamos para a função apropriada.
let instruction = instruction_data
.get(0)
.ok_or(ProgramError::InvalidInstructionData)?;
match instruction {
0 => initialize(program_id, accounts),
1 => increment(accounts),
_ => Err(ProgramError::InvalidInstructionData),
}2. Gerenciamento de contas
Usamos next_account_info para percorrer as contas, verificando manualmente signatários e proprietários. O Anchor faz isso automaticamente com sua macro #[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. Serialização personalizada
Implementamos métodos personalizados serialize e deserialize para nossa struct Counter. Por padrão, o Anchor usa a serialização Borsh e abstrai esse processo.
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. Interações com o System Program
A criação de contas envolve interação direta com o System Program, usando invoke e realizando uma invocação entre programas (CPI), que o Anchor simplifica com sua restrição 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. Controle granular
Em geral, programas nativos oferecem mais controle sobre o layout e o processamento dos dados, pois não seguem um único framework opinativo, permitindo um código mais otimizado.
Como avaliar Anchor versus Rust nativo
Veja alguns pontos para considerar:
1. Explícito vs. implícito
Programas nativos exigem o tratamento explícito de muitos aspectos que o Anchor gerencia implicitamente. Isso inclui validação e serialização de contas, além do encaminhamento de instruções
2. Considerações de segurança
Sem as verificações integradas do Anchor, os desenvolvedores precisam implementar com atenção as medidas de segurança adequadas, como verificar o proprietário da conta e o status do signatário
3. Ajuste de desempenho
Programas nativos permitem otimizações de desempenho mais granulares, mas exigem um entendimento mais profundo do comportamento do runtime da Solana
4. Código boilerplate
Prepare-se para escrever mais código boilerplate para operações comuns que o Anchor abstrai
5. Curva de aprendizado
Embora possa ser mais eficiente, a programação nativa tem uma curva de aprendizado mais acentuada e exige conhecimento mais aprofundado da arquitetura da Solana
Resumindo
O maior fator limitante ao migrar do Anchor para código nativo é lidar com serialização e desserialização. Em nosso caso, isso foi relativamente simples. Porém, ficaria cada vez mais complicado à medida que o gerenciamento de estado se tornasse mais complexo.
Por outro lado, também é verdade que o Borsh usado pelo Anchor tem um custo computacional muito alto, então o esforço vale a pena.
Nossa jornada de otimização não termina aqui. Na próxima seção, levaremos os limites ainda mais longe usando syscalls diretas e evitando a biblioteca padrão do Rust.
Essa abordagem é desafiadora, mas prometo que oferecerá insights interessantes sobre o funcionamento interno do runtime da Solana.
Levando o desempenho ao limite com Rust unsafe e syscalls diretas
Para levar o desempenho do nosso programa de contador ao limite, agora exploraremos o uso de Rust unsafe e syscalls diretas. O Rust unsafe permite que os desenvolvedores contornem verificações de segurança padrão, possibilitando a manipulação direta da memória e otimizações de baixo nível. As syscalls, por sua vez, oferecem interfaces diretas com o runtime da Solana.
Embora seja complexa e exija um desenvolvimento meticuloso, essa abordagem pode gerar economias significativas de CUs. No entanto, ela também exige um entendimento mais profundo da arquitetura da Solana e muita atenção à segurança do programa. Os possíveis ganhos de desempenho são substanciais, mas vêm acompanhados de maior responsabilidade.
Vamos analisar uma versão altamente otimizada do nosso programa de contador que aproveita essas técnicas avançadas:
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(())
}Principais diferenças e otimizações
Vamos analisar as otimizações:
1. Ambiente no-std
Estamos usando solana_nostd_entrypoint, que fornece um ambiente no-std. Isso elimina o overhead da biblioteca padrão do Rust, reduzindo o tamanho do programa e possivelmente melhorando o desempenho. Créditos a cavemanloverboy e ao seu repositório no GitHub sobre entrypoint no-std para programas Solana.
2. Funções inline
As funções críticas são marcadas com #[inline(always)]. Inlining é uma otimização do compilador em que o corpo da função é inserido no local da chamada, eliminando o overhead da chamada de função. Isso pode acelerar a execução, especialmente em funções pequenas chamadas com frequência.
3. Manipulação de bits para parsing de instruções
Usamos manipulação de bitsinstruction_data[0] & 1 para determinar o tipo da instrução, o que pode ser mais eficiente do que outros métodos de parsing:
// Use the least significant bit to determine the instruction
match instruction_data[0] & 1 {
0 => initialize(accounts),
1 => increment(accounts),
_ => unreachable!(),
}4. Gerenciamento de memória sem custo e tratamento mínimo de pânico
As macros noalloc_allocator! e basic_panic_impl! implementam gerenciamento de memória sem overhead e tratamento mínimo de pânico:
Noalloc_allocator! define um alocador personalizado que entra em pânico em qualquer tentativa de alocação e não faz nada durante a desalocação. Defini-lo como alocador global para programas Solana impede efetivamente qualquer alocação dinâmica de memória durante o runtime:
#[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;
}
};
}Isso é crucial porque:
- Elimina o overhead das operações de alocação e desalocação de memória
- Obriga os desenvolvedores a usar memória baseada na stack ou estática, que geralmente é mais rápida e tem desempenho mais previsível
- Reduz o consumo de memória do programa
basic_panic_impl! fornece um handler de pânico mínimo que apenas registra a mensagem “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. Preparação eficiente de CPI
A struct InstructionC e as funções to_meta_c e to_info_c oferecem uma maneira eficiente e de baixo nível de preparar dados para CPIs:
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()
];Essas funções criam estruturas compatíveis com C que podem ser passadas diretamente para a syscall sol_invoke_signed_c. Ao evitar o overhead das abstrações de alto nível do Rust e trabalhar diretamente com ponteiros brutos e estruturas compatíveis com C, essas funções minimizam o custo computacional da preparação para CPIs.
Essa abordagem economiza CUs ao reduzir as alocações, cópias e conversões de memória que normalmente ocorreriam ao usar tipos Rust mais abstratos.
Por exemplo, o método to_info_c constrói de forma eficiente uma struct AccountInfoC usando aritmética direta de ponteiros:
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 …
}
}Essa manipulação direta dos layouts de memória permite criar com extrema eficiência as estruturas necessárias para CPIs, reduzindo assim o custo em CUs dessas operações.
6. Syscalls diretas e Rust unsafe
Essa abordagem ignora as abstrações habituais do Rust e interage diretamente com o runtime da Solana, oferecendo benefícios significativos de desempenho. No entanto, ela também introduz complexidade e exige cuidado ao lidar com Rust unsafe:
// 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. Compilação condicional:
O atributo #[cfg(target_os = “solana”)] garante que esse código seja compilado somente quando o destino for o runtime da Solana, o que é necessário porque essas syscalls só estão disponíveis nesse ambiente.
Possíveis problemas com Rust unsafe
Embora seja poderoso, o Rust unsafe pode causar problemas graves se não for usado corretamente:
- Vazamentos e corrupção de memória
- Comportamento indefinido
- Condições de corrida
Para reduzir os riscos ao usar Rust unsafe:
- Use blocos unsafe com moderação e somente quando necessário
- Documente todas as premissas e invariantes de segurança
- Use ferramentas como Miri e os sanitizers integrados do Rust para testes
- Considere técnicas de verificação formal para seções críticas
- Faça revisões de código completas com foco nos blocos unsafe
Resumindo
Embora tudo isso seja fascinante, é difícil justificar o uso dessa abordagem hiperotimizada em um programa pronto para produção que protege dinheiro real, devido à maior complexidade, ao potencial de erros e aos desafios de manutenção. Para a maioria das aplicações, o risco de introduzir bugs críticos costuma superar os benefícios de desempenho.
Usar essa abordagem provavelmente levará você à armadilha das otimizações prematuras.
No entanto, algumas medidas são fáceis de replicar:
- Usar
nostd_entrypointem vez do inchadoentrypointdesolana_program - Usar funções inline sempre que possível
- Minimizar alocações dinâmicas e priorizar estruturas de dados baseadas na stack
Conclusão
Este artigo explorou vários níveis de otimização para programas Solana, desde o desenvolvimento de alto nível com Anchor até Rust unsafe de baixo nível com syscalls diretas. Vimos como cada abordagem oferece diferentes trade-offs entre facilidade de uso, segurança e desempenho.
Principais conclusões:
- O Anchor oferece um framework acessível com algum overhead de desempenho
- A desserialização zero-copy pode melhorar significativamente a eficiência de estruturas de dados grandes
- O Rust nativo oferece mais controle e potencial de otimização
- Rust unsafe e syscalls diretas oferecem desempenho máximo, mas aumentam a complexidade e o risco
A escolha do nível de otimização depende do seu caso de uso específico, dos requisitos de desempenho e da sua tolerância a riscos. Sempre meça o impacto das otimizações e considere as implicações de manutenção das suas escolhas no longo prazo.
Se você leu até aqui, agradeço, anon! Insira seu endereço de e-mail abaixo para nunca perder uma atualização sobre as novidades da Solana. Quer se aprofundar? Explore os artigos mais recentes no blog da Helius e continue hoje mesmo sua jornada na Solana.
Recursos adicionais
Artigos relacionados
Assine a Helius
Acompanhe as novidades mais recentes do desenvolvimento Solana e receba atualizações quando publicarmos


