Performance
Si de nombreux développeurs se tournent vers Pinocchio pour son contrôle précis des champs des compte, sa véritable force réside dans sa capacité à offrir des performances maximales.
Dans cette section, nous explorerons des stratégies pratiques pour optimiser l'efficacité de vos programmes Solana.
Vérifications Superflues
Les développeurs ajoutent souvent des contraintes supplémentaires aux comptes pour des raisons de sécurité mais celles-ci peuvent entraîner une charge inutile. Il est important de faire la distinction entre les vérifications essentielles et les vérifications redondantes.
Par exemple, lors de la lecture d'un Token Account ou d'un Mint, la désérialisation et la validation sont nécessaires. Mais si ces mêmes comptes sont ensuite utilisés dans un CPI (Invocation de Programme Croisé), toute incompatibilité ou erreur entraînera l'échec de l'instruction à ce stade. Ainsi, les contrôles préventifs peuvent s'avérer redondants.
De même, vérifier le "propriétaire" d'un Compte de Jetons est souvent superflu, en particulier si le compte est contrôlé par une Adresse Dérivée de Programme (PDA). Si le propriétaire est incorrect, le CPI échouera en raison de seeds invalides. Dans les cas où le transfert n'est pas effectué par un PDA, vous devez vous concentrer sur la validation du destinataire, en particulier lors d'un dépôt sur un compte contrôlé par un PDA car les attentes de l'expéditeur correspondent à celles du programme.
Prenons l'exemple d'un Escrow:
...
Programme de Jetons Associés
Associated Token Accounts (Comptes de Jetons Associés ou ATAs) sont pratiques mais ont un coût en termes de performances. Évitez d'imposer leur utilisation sauf en cas d'absolue nécessité et n'exigez jamais leur création dans votre logique d'instruction. Dans la plupart des cas, init-if-needed ajoute une complexité et une utilisation des ressources qui pourraient être évitées (comme dans les instructions d'Amm qui sont composées par un routeur tel que Jupiter).
Si votre programme utilise des ATAs, assurez-vous qu'ils sont créés en externe. Dans votre programme, vérifiez leur exactitude en dérivant directement l'adresse attendue comme ceci :
let (associated_token_account, _) = find_program_address(
&[
self.accounts.owner.key(),
self.accounts.token_program.key(),
self.accounts.mint.key(),
],
&pinocchio_associated_token_account::ID,
);En minimisant les vérifications inutiles et les exigences liées aux comptes, vous réduisez les coûts de calcul et rationalisez l'exécution de votre programme, libérant ainsi tout le potentiel de performance du développement natif sur Solana.
Drapeau Perf
Les drapeaux de fonctionnalité (Feature flags) de Rust constituent un moyen puissant de compiler du code de manière conditionnelle, vous permettant d'activer ou de désactiver des fonctionnalités pour différents profils de compilation, tels que le développement, les tests ou les performances maximales en production.
Cela est particulièrement utile dans les programmes Solana où chaque unité de calcul compte.
Configuration des Drapeaux de Fonctionnalité
Les drapeaux de fonctionnalités sont définis dans votre fichier Cargo.toml sous la section [features]. Par exemple, vous pouvez avoir envie d'un drapeau perf qui active les optimisations de performance en désactivant la journalisation et les vérifications supplémentaires :
[features]
default = ["perf"]
perf = []Ici, la fonctionnalité perf est activée par défaut, mais vous pouvez la désactiver lors de la compilation ou des tests.
Utilisation des Drapeaux de Fonctionnalité dans le Code
Vous pouvez utiliser les attributs de compilation conditionnelle de Rust pour inclure ou exclure du code en fonction de la fonctionnalité active. Par exemple :
pub fn process(ctx: Context<'info>) -> ProgramResult {
#[cfg(not(feature = "perf"))]
sol_log("Create Class");
Self::try_from(ctx)?.execute()
}La plupart des programmes renvoient le nom de l'instruction sous forme de log afin de faciliter le débogage et de s'assurer que la bonne instruction est appelée.
Cependant, cela coûte cher et n'est pas vraiment nécessaire, sauf pour rendre l'explorateur plus lisible et améliorer le débogage.
#[cfg(not(feature = "perf"))]
if name.len() > MAX_NAME_LEN {
return Err(ProgramError::InvalidArgument);
}Un autre exemple est celui des contrôles superflus évoqués précédemment.
Si nous savons que notre instruction est sûre sans ces vérifications, nous ne devrions pas les définir comme paramètres par défaut, mais plutôt les masquer derrière un drapeau.
Compilation avec Différents Drapeaux
Pour compiler votre programme avec ou sans la fonctionnalité perf, utilisez :
Avec optimisations des performances (par défaut):
cargo build-bpfAvec des vérifications supplémentaires et la journalisation :
cargo build-bpf --no-default-featuresCette approche vous permet de conserver une base de code unique qui peut être optimisée pour la sécurité du développement ou la vitesse de production simplement en activant ou désactivant un drapeau de fonctionnalité.
Opérations bit à bit
Lorsqu'on parle d'opérations efficaces, les booléens sont parmi les plus gaspilleurs. Considérez ceci : ils occupent 1 octet pour représenter seulement deux valeurs possibles : 0 ou 1.
Si vous avez plusieurs booléens dans votre code, vous pouvez les stocker de manière beaucoup plus efficace en utilisant la manipulation de bits. Avec les opérations bit à bit, vous pouvez stocker jusqu'à 8 valeurs booléennes différentes dans un seul octet.
Définition des drapeaux
Définissez les drapeaux comme des positions de bits en utilisant des opérations de décalage à gauche :
const FLAG_ACTIVE: u8 = 1 << 0; // 0000_0001
const FLAG_VERIFIED: u8 = 1 << 1; // 0000_0010
const FLAG_PREMIUM: u8 = 1 << 2; // 0000_0100
const FLAG_LOCKED: u8 = 1 << 3; // 0000_1000Pour définir un drapeau, nous pouvons simplement faire :
let mut flags = 0u8; // flags = 0000_0000
flags |= FLAG_ACTIVE; // flags = 0000_0001
flags |= FLAG_VERIFIED; // flags = 0000_0011
flags |= FLAG_PREMIUM | FLAG_LOCKED; // flags = 0000_1111L'opérateur | (OU) est parfait pour "activer" des bits spécifiques sans affecter les autres, car lorsque vous faites un OU avec 0, le bit original est préservé et lorsque vous faites un OU avec 1, le résultat est toujours 1 (le drapeau est activé)
Si nous voulons vérifier si un drapeau est actif, nous pouvons faire quelque chose comme ceci :
let flags = 0b0000_0101u8; // Has ACTIVE and PREMIUM flags set
// Check if a single flag is set
if flags & FLAG_ACTIVE != 0 {
println!("Account is active");
}
// Check if multiple flags are set
if (flags & (FLAG_ACTIVE | FLAG_PREMIUM)) == (FLAG_ACTIVE | FLAG_PREMIUM) {
println!("Account is both active and premium");
}
// Check if any of multiple flags are set
if flags & (FLAG_VERIFIED | FLAG_PREMIUM) != 0 {
println!("Account is either verified or premium (or both)");
}L'opérateur & (ET) est parfait pour "masquer" des bits spécifiques afin de vérifier leurs valeurs, car lorsque vous faites un ET avec 0, le résultat est toujours 0 et lorsque vous faites un ET avec 1, le bit original est préservé
Pour effacer ou basculer des drapeaux, nous pouvons faire quelque chose comme ceci :
let mut flags = 0b0000_1111u8; // All flags set
// Clear a single flag
flags &= !FLAG_ACTIVE; // flags = 0000_1110
// Clear multiple flags at once
flags &= !(FLAG_VERIFIED | FLAG_PREMIUM); // flags = 0000_1000
// Toggle a single flag
flags ^= FLAG_ACTIVE; // flags = 0000_1001 (now has VERIFIED)
flags ^= FLAG_LOCKED; // flags = 0000_0001 (LOCKED now cleared)
// Toggle multiple flags at once
flags ^= FLAG_PREMIUM | FLAG_LOCKED; // flags = 0000_1011Mémoire sur Solana
La Machine Virtuelle Solana (SVM) utilise une architecture de mémoire à trois niveaux qui sépare strictement la mémoire de pile (variables locales), la mémoire du tas (structures dynamiques) et l'espace de compte (stockage persistant).
Comprendre cette architecture est crucial pour écrire des programmes Solana haute performance.
Les programmes fonctionnent dans des espaces d'adressage virtuel fixes avec un mappage mémoire prévisible :
Code du programme :
0x100000000- Où réside votre bytecode de programme compiléDonnées de pile :
0x200000000- Variables locales et cadres d'appel de fonctionDonnées de tas :
0x300000000- Allocations dynamiques (coûteuses !)
Cette disposition déterministe permet des optimisations puissantes mais crée également des contraintes strictes de performance.
L'avantage de la zéro-allocation
La principale avancée en performance de Pinocchio vient de l'utilisation de références au lieu d'allocations de tas pour tout.
Cette approche s'appuie sur une idée clé : la SVM charge déjà toutes vos entrées de programme en mémoire, donc copier ces données dans de nouvelles allocations de tas est un pur gaspillage.
L'allocation de tas n'est pas intrinsèquement mauvaise, mais sur Solana elle est coûteuse et complexe car chaque allocation consomme de précieuses unités de calcul : chaque allocation fragmente l'espace de tas limité et les opérations de nettoyage consomment des unités de calcul supplémentaires.
Techniques de zéro-allocation
Structures de données basées sur les références - Transformer les données possédées en références empruntées :
rust// HEAP ALLOCATION: struct AccountInfo { key: Pubkey, // Owned data - copied to heap data: Vec<u8>, // Vector - definitely heap allocated } // ZERO ALLOCATION: struct AccountInfo<'a> { key: &'a Pubkey, // Reference - no allocation data: &'a [u8], // Slice reference - no allocation }Accès aux données sans copie - Accéder aux données sur place sans désérialisation :
rust// Instead of deserializing, access data in-place: pub fn process_transfer(accounts: &[u8], instruction_data: &[u8]) { // Parse accounts directly from byte slice - NO HEAP ALLOCATION let source_account = &accounts[0..36]; // Just slice references let dest_account = &accounts[36..72]; // Access fields through pointer arithmetic - NO ALLOCATION let amount = u64::from_le_bytes(instruction_data[0..8].try_into().unwrap()); }Contraintes No-Std - Empêcher l'utilisation accidentelle du tas au moment de la compilation :
rust// Enforces no-std to prevents accidental heap usage: #![no_std]
Allocateur de mémoire Pinocchio
Si vous voulez vous assurer de ne pas allouer de tas, Pinocchio fournit une macro spécialisée pour garantir cela : no_allocator!()
En dehors de cela, vous pouvez toujours écrire un meilleur allocateur de tas qui sait comment se nettoyer après utilisation. Si cette approche vous intéresse, voici un exemple.