Оптимізація compute units (CU) у програмі на Solana — це не разова дія, а системний процес вимірювання, локалізації вузьких місць та усунення зайвих витрат. Нижче наведено перевірену послідовність кроків: від профілювання до конкретних інженерних прийомів, які знижують споживання CU без зміни бізнес-логіки програми.
Зменшення алокацій та копіювань пам'яті
Кожен виклик alloc або realloc у програмі на Solana оплачується compute units. Чим більше алокацій відбувається в критичному шляху виконання, тим вищий бюджет CU. Головне правило: якщо дані вже існують в пам'яті, не виділяйте новий блок — працюйте з наявним.
Уникайте проміжних Vec і String у hot path
Конструювання Vec<u8> або String для тимчасових результатів змушує алокатор виділяти пам'ять на купі (heap). Якщо результат передається далі як зріз (slice), працюйте безпосередньо з &[u8]. Наприклад, замість збирання фрагментів у вектор із подальшою конкатенацією, використовуйте ітератори по зрізах вихідних даних.
Попередньо виділяйте пам'ять із запасом
Якщо алокація неминуча, вкажіть очікуваний розмір одразу через Vec::with_capacity. Динамічне зростання вектора викликає повторні алокації з копіюванням існуючих даних у новий блок — кожне таке перерозподілення оплачується окремо.
Мінімізуйте клонування та копіювання структур
У Rust клонування великих структур (особливо тих, що містять вектори або вкладені об'єкти) створює глибоку копію. Передавайте посилання (&T, &mut T) або, якщо структура мала і реалізує Copy, покладайтеся на семантику копіювання стеку. Для серіалізації та десеріалізації використовуйте zero-copy формати (наприклад, borsh із borsh::from_slice читає дані безпосередньо з буфера входу).
Перевірка
Після внесення змін запустіть тести з увімкненим лічильником CU (див. розділ про бенчмаркінг) і порівняйте кількість алокацій до та після. Якщо різниця в CU менша за очікувану, перевірте, чи не відбувається неявне клонування через макроси Anchor (наприклад, автоматична десеріалізація акаунтів).
Оптимізація циклів та обчислень
Цикли — найчастіше джерело непропорційно великих витрат CU, особливо коли кількість ітерацій залежить від зовнішніх даних (розмір масиву, кількість акаунтів).
Обмежте ітерації константою або перевіреною межею
Ніколи не допускайте циклів, де кількість ітерацій визначається виключно користувацьким вводом без жорсткої верхньої межі. Навіть якщо кожна ітерація дешева, 10 000 ітерацій здатні вичерпати весь бюджет. Встановіть розумну константу (наприклад, 256 елементів) і повертайте помилку при перевищенні.
Ранній вихід із циклу
Якщо цикл виконує пошук, перевірку або фільтрацію, зупиняйте ітерацію одразу після знаходження результату через break. Це тривіальний прийом, але на практиці його часто пропускають, особливо в коді, що пройшов кілька ітерацій рефакторингу.
Виносьте інваріанти з циклу
Обчислення, результат яких не змінюється між ітераціями, має бути виконане до циклу. Типовий приклад: конвертація адреси PDA у байтовий масив або обчислення хешу для перевірки, яка повторюється в кожній ітерації, хоча залежить лише від фіксованих параметрів.
Замініть лінійний пошук хеш-таблицею
Якщо в циклі відбувається пошук елемента за ключем у відсортованому масиві або перебір списку, замініть це на HashMap або BTreeMap. Операція пошуку в хеш-таблиці має середню складність O(1) проти O(n) у лінійному пошуку. Врахуйте, однак, що створення самої хеш-таблиці також коштує CU — це має сенс лише при багаторазовому пошуку в межах одного виклику.
Математичні оптимізації
Заміна ділення на множення з оберненим значенням (де це коректно), використання бітових операцій замість цілочисельного ділення на степені двійки, попереднє обчислення констант — ці прийоми знижують вартість окремих інструкцій. Ефект помітний лише в дуже щільних циклах із тисячами ітерацій; у разових обчисленнях різниця в CU мінімальна.
Використання syscalls замість дорогих операцій
Solana надає набір системних викликів (syscalls), які реалізовані на рівні runtime і виконуються значно дешевше, ніж еквівалентний код на Rust у межах програми.
sol_memcpy_ та sol_memset_
Системний виклик sol_memcpy_ копіює блоки пам'яті ефективніше, ніж цикл з поклементним присвоєнням або стандартний copy_from_slice для великих буферів. Аналогічно sol_memset_ заповнює пам'ять значенням дешевше, ніж ручний цикл. Ці виклики мають сенс для буферів розміром від кількох сотень байтів і більше.
Криптографічні syscalls
Хешування SHA-256, перевірка підписів Ed25519 та інші криптографічні операції мають відповідні syscalls (sol_sha256, sol_verify_ed25519). Їхня вартість у CU значно нижча, ніж реалізація тих самих алгоритмів на чистому Rust у межах програми. Ніколи не імплементуйте криптографічні примітиви самостійно — це і дорожче в CU, і небезпечно з точки зору безпеки.
sol_log_ та діагностика
Виклик sol_log_ споживає CU. У production-коді видаліть або обгорніть діагностичні логи умовою компіляції (#[cfg(feature = "debug")]), щоб вони не впливали на бюджет у робочому середовищі.
Коли syscalls не дають переваги
Для малих буферів (до ~64 байтів) накладні витрати на перехід до syscall можуть перевищити виграш від оптимізованої реалізації. Виміряйте обидва варіанти перед тим, як замінювати стандартні операції на syscalls.
Бенчмаркінг до та після оптимізації
Будь-яка оптимізація без вимірювання — це гадання. Єдиний надійний спосіб дізнатися реальну вартість інструкції в CU — запустити її в контрольованому середовищі з увімкненим лічильником.
Середовище та передумови
- Solana CLI останньої стабільної версії (перевірте актуальність у офіційному репозиторії перед запуском)
- solana-program-test для локального запуску програми з вимірюванням CU
- Тестовий випадок, який ізольовано викликає саме ту інструкцію, що оптимізується
Метод вимірювання
У solana-program-test після виклику інструкції через process_instruction можна отримати значення спожитих CU з контексту транзакції. Альтернативно, використовуйте solana test-validator із прапорцем --compute-unit-price та аналізуйте логи, де вказано фактичне споживання.
Базовий підхід у тестах:
- Підготуйте тестові акаунти та дані в фіксованому стані.
- Викличте інструкцію один раз, зафіксуйте CU (базове значення).
- Внесіть одну оптимізацію.
- Викличте ту саму інструкцію з тими самими даними, зафіксуйте CU.
- Зафіксуйте різницю.
Інтерпретація результатів
Звертайте увагу на дисперсію: запустіть кожен вимір не менше 5–10 разів і використовуйте медіану. Деякі операції (зокрема пов'язані з доступом до пам'яті) можуть мати варіацію через особливості роботи алокатора в різних викликах.
Якщо оптимізація дала зниження менше ніж на 100–200 CU, оцініть, чи варта вона складності, яку вона додає до коду. Мікрооптимізації з нестабільним ефектом ускладнюють аудит і подальшу підтримку.
Регресійний контроль
Додайте перевірку CU до CI-пайплайну: якщо інструкція починає споживати більше CU, ніж зафіксований поріг, збірка має падати. Це захищає від непомітної регресії під час рефакторингу або оновлення залежностей.
Межі застосування та відкат
Результати бенчмаркінгу в solana-program-test близькі до production, але не ідентичні: реальний runtime може мати інші характеристики через наявність інших транзакцій у слоті. Тому зафіксований в тестах бюджет CU слід вважати нижньою межею, а для production закладати запас 10–15%. Якщо після оптимізації інструкція працює нестабільно, відкотіть зміни через git і поверніться до попередньої стабільної версії — всі оптимізації мають бути оборотними.
Наступний логічний крок після зниження CU — перевірка, чи не можна відповідно зменшити розмір облікового запису, що використовується програмою. Детальніше про це — у матеріалі про зменшення розміру облікового запису в Solana.