Эх сурвалжийг харах

sdk: Add `deserialize_into` to entrypoint to avoid heap allocations (#2618)

* sdk: Add `deserialize_into` to entrypoint

#### Problem

The main entrypoint for Solana programs allocates a vector on the heap
and pushes AccountInfos to it. Allocation is expensive.

#### Summary of changes

Add a new version of `deserialize` called `deserialize_into`, which
expects a slice of `MaybeUninit<AccountInfo>`. The entrypoint can
allocate a maximum array of AccountInfos on the stack and then pass it
in. This new version of the entrypoint saves roughly 30 CUs per unique
account passed to the program.

In an earlier version, I had the new function return the array itself,
but this used slightly more CUs, and didn't work for an array with 64
elements. Let me know how it looks!

* Call instruction processor in non-inlined function

* Add test for max supported accounts in a transaction

* Refactor extracting account infos and instruction data

* Changes required from rebase

* Add clippy allow

* Add panic message if too many accounts provided

* Add `entrypoint_no_alloc!` and revert behavior in entrypoint!

* Use entrypoint_no_alloc! everywhere except noop

* Comment why noop program works the way it does

* Add limit in doc-string

* CHANGELOG: Add entry for entrypoint
Jon C 1 жил өмнө
parent
commit
71eb1d86c6
36 өөрчлөгдсөн 289 нэмэгдсэн , 96 устгасан
  1. 3 1
      CHANGELOG.md
  2. 52 1
      program-test/tests/bpf.rs
  3. 1 1
      programs/sbf/rust/call_args/src/lib.rs
  4. 1 1
      programs/sbf/rust/caller_access/src/lib.rs
  5. 1 1
      programs/sbf/rust/custom_heap/src/lib.rs
  6. 1 1
      programs/sbf/rust/dup_accounts/src/lib.rs
  7. 1 1
      programs/sbf/rust/error_handling/src/lib.rs
  8. 1 1
      programs/sbf/rust/external_spend/src/lib.rs
  9. 1 1
      programs/sbf/rust/finalize/src/lib.rs
  10. 1 1
      programs/sbf/rust/get_minimum_delegation/src/lib.rs
  11. 1 1
      programs/sbf/rust/instruction_introspection/src/lib.rs
  12. 1 1
      programs/sbf/rust/invoke/src/lib.rs
  13. 1 1
      programs/sbf/rust/invoke_and_error/src/lib.rs
  14. 1 1
      programs/sbf/rust/invoke_and_ok/src/lib.rs
  15. 1 1
      programs/sbf/rust/invoke_and_return/src/lib.rs
  16. 1 1
      programs/sbf/rust/invoked/src/lib.rs
  17. 1 1
      programs/sbf/rust/log_data/src/lib.rs
  18. 1 1
      programs/sbf/rust/mem/src/lib.rs
  19. 2 0
      programs/sbf/rust/noop/src/lib.rs
  20. 1 1
      programs/sbf/rust/panic/src/lib.rs
  21. 1 1
      programs/sbf/rust/rand/src/lib.rs
  22. 1 1
      programs/sbf/rust/realloc/src/lib.rs
  23. 1 1
      programs/sbf/rust/realloc_invoke/src/lib.rs
  24. 1 1
      programs/sbf/rust/remaining_compute_units/src/lib.rs
  25. 1 1
      programs/sbf/rust/ro_account_modify/src/lib.rs
  26. 1 1
      programs/sbf/rust/ro_modify/src/lib.rs
  27. 1 1
      programs/sbf/rust/sanity/src/lib.rs
  28. 1 1
      programs/sbf/rust/sibling_inner_instructions/src/lib.rs
  29. 1 1
      programs/sbf/rust/sibling_instructions/src/lib.rs
  30. 1 1
      programs/sbf/rust/simulation/src/lib.rs
  31. 1 1
      programs/sbf/rust/spoof1/src/lib.rs
  32. 1 1
      programs/sbf/rust/spoof1_system/src/lib.rs
  33. 1 1
      programs/sbf/rust/sysvar/src/lib.rs
  34. 1 1
      programs/sbf/rust/upgradeable/src/lib.rs
  35. 1 1
      programs/sbf/rust/upgraded/src/lib.rs
  36. 200 62
      sdk/program/src/entrypoint.rs

+ 3 - 1
CHANGELOG.md

@@ -23,7 +23,9 @@ Release channels have their own copy of this changelog:
   * Banks-client:
     * relax functions to use `&self` instead of `&mut self` (#2591)
 * Changes
-  * SDK: removed the `respan` macro. This was marked as "internal use only" and was no longer used internally.
+  * SDK:
+    * removed the `respan` macro. This was marked as "internal use only" and was no longer used internally.
+    * add `entrypoint_no_alloc!`, a more performant program entrypoint that avoids allocations, saving 20-30 CUs per unique account
   * `agave-validator`: Update PoH speed check to compare against current hash rate from a Bank (#2447)
   * `solana-test-validator`: Add `--clone-feature-set` flag to mimic features from a target cluster (#2480)
   * `solana-genesis`: the `--cluster-type` parameter now clones the feature set from the target cluster (#2587)

+ 52 - 1
program-test/tests/bpf.rs

@@ -1,9 +1,13 @@
 use {
     solana_program_test::ProgramTest,
     solana_sdk::{
-        bpf_loader, instruction::Instruction, pubkey::Pubkey, signature::Signer,
+        bpf_loader, feature_set,
+        instruction::{AccountMeta, Instruction},
+        pubkey::Pubkey,
+        signature::Signer,
         transaction::Transaction,
     },
+    test_case::test_case,
 };
 
 #[tokio::test]
@@ -39,3 +43,50 @@ async fn test_add_bpf_program() {
         .await
         .unwrap();
 }
+
+#[test_case(64, true, true; "success with 64 accounts and without feature")]
+#[test_case(65, true, false; "failure with 65 accounts and without feature")]
+#[test_case(128, false, true; "success with 128 accounts and with feature")]
+#[test_case(129, false, false; "failure with 129 accounts and with feature")]
+#[tokio::test]
+async fn test_max_accounts(num_accounts: u8, deactivate_feature: bool, expect_success: bool) {
+    let program_id = Pubkey::new_unique();
+
+    let mut program_test = ProgramTest::default();
+
+    program_test.prefer_bpf(true);
+    program_test.add_program("noop_program", program_id, None);
+    if deactivate_feature {
+        program_test.deactivate_feature(feature_set::increase_tx_account_lock_limit::id());
+    }
+
+    let context = program_test.start_with_context().await;
+
+    // Subtract 2 to account for the program and fee payer
+    let num_extra_accounts = num_accounts.checked_sub(2).unwrap();
+    let account_metas = (0..num_extra_accounts)
+        .map(|_| AccountMeta::new_readonly(Pubkey::new_unique(), false))
+        .collect::<Vec<_>>();
+    let instruction = Instruction::new_with_bytes(program_id, &[], account_metas);
+    let transaction = Transaction::new_signed_with_payer(
+        &[instruction],
+        Some(&context.payer.pubkey()),
+        &[&context.payer],
+        context.last_blockhash,
+    );
+
+    // Invoke the program.
+    if expect_success {
+        context
+            .banks_client
+            .process_transaction(transaction)
+            .await
+            .unwrap();
+    } else {
+        context
+            .banks_client
+            .process_transaction(transaction)
+            .await
+            .unwrap_err();
+    }
+}

+ 1 - 1
programs/sbf/rust/call_args/src/lib.rs

@@ -33,7 +33,7 @@ struct OutputData {
     many_args_2: i64,
 }
 
-solana_program::entrypoint!(entry);
+solana_program::entrypoint_no_alloc!(entry);
 
 pub fn entry(_program_id: &Pubkey, _accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
     // This code is supposed to occupy stack space. The purpose of this test is to make sure

+ 1 - 1
programs/sbf/rust/caller_access/src/lib.rs

@@ -10,7 +10,7 @@ use {
     std::convert::TryInto,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/custom_heap/src/lib.rs

@@ -55,7 +55,7 @@ unsafe impl std::alloc::GlobalAlloc for BumpAllocator {
 #[global_allocator]
 static A: BumpAllocator = BumpAllocator;
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 pub fn process_instruction(
     _program_id: &Pubkey,
     _accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/dup_accounts/src/lib.rs

@@ -13,7 +13,7 @@ use solana_program::{
     pubkey::Pubkey,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/error_handling/src/lib.rs

@@ -45,7 +45,7 @@ impl PrintProgramError for MyError {
     }
 }
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/external_spend/src/lib.rs

@@ -5,7 +5,7 @@
 extern crate solana_program;
 use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/finalize/src/lib.rs

@@ -8,7 +8,7 @@ use solana_program::{
     program::invoke, pubkey::Pubkey,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/get_minimum_delegation/src/lib.rs

@@ -7,7 +7,7 @@ use solana_program::{
     account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey, stake,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     _accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/instruction_introspection/src/lib.rs

@@ -12,7 +12,7 @@ use solana_program::{
     sysvar::instructions,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/invoke/src/lib.rs

@@ -65,7 +65,7 @@ fn do_nested_invokes(num_nested_invokes: u64, accounts: &[AccountInfo]) -> Progr
     Ok(())
 }
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction<'a>(
     program_id: &Pubkey,
     accounts: &[AccountInfo<'a>],

+ 1 - 1
programs/sbf/rust/invoke_and_error/src/lib.rs

@@ -9,7 +9,7 @@ use solana_program::{
     pubkey::Pubkey,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/invoke_and_ok/src/lib.rs

@@ -9,7 +9,7 @@ use solana_program::{
     pubkey::Pubkey,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/invoke_and_return/src/lib.rs

@@ -9,7 +9,7 @@ use solana_program::{
     pubkey::Pubkey,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/invoked/src/lib.rs

@@ -17,7 +17,7 @@ use {
     solana_sbf_rust_invoked_dep::*,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 #[allow(clippy::cognitive_complexity)]
 fn process_instruction(
     program_id: &Pubkey,

+ 1 - 1
programs/sbf/rust/log_data/src/lib.rs

@@ -5,7 +5,7 @@ use solana_program::{
     program::set_return_data, pubkey::Pubkey,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 #[allow(clippy::cognitive_complexity)]
 fn process_instruction(
     _program_id: &Pubkey,

+ 1 - 1
programs/sbf/rust/mem/src/lib.rs

@@ -10,7 +10,7 @@ use {
     solana_sbf_rust_mem_dep::{run_mem_tests, MemOps},
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 pub fn process_instruction(
     _program_id: &Pubkey,
     _accounts: &[AccountInfo],

+ 2 - 0
programs/sbf/rust/noop/src/lib.rs

@@ -3,6 +3,8 @@
 extern crate solana_program;
 use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
 
+// This program intentionally uses `entrypoint!` instead of `entrypoint_no_alloc!`
+// to handle any number of accounts.
 solana_program::entrypoint!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,

+ 1 - 1
programs/sbf/rust/panic/src/lib.rs

@@ -11,7 +11,7 @@ fn custom_panic(info: &core::panic::PanicInfo<'_>) {
 extern crate solana_program;
 use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     _accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/rand/src/lib.rs

@@ -5,7 +5,7 @@
 extern crate solana_program;
 use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey};
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     _accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/realloc/src/lib.rs

@@ -16,7 +16,7 @@ use {
     std::{convert::TryInto, mem},
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/realloc_invoke/src/lib.rs

@@ -16,7 +16,7 @@ use {
     std::convert::TryInto,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/remaining_compute_units/src/lib.rs

@@ -5,7 +5,7 @@ use solana_program::{
     account_info::AccountInfo, compute_units::sol_remaining_compute_units,
     entrypoint::ProgramResult, msg, pubkey::Pubkey,
 };
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 pub fn process_instruction(
     _program_id: &Pubkey,
     _accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/ro_account_modify/src/lib.rs

@@ -14,7 +14,7 @@ const INSTRUCTION_INVOKE_MODIFY: u8 = 1;
 const INSTRUCTION_MODIFY_INVOKE: u8 = 2;
 const INSTRUCTION_VERIFY_MODIFIED: u8 = 3;
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/ro_modify/src/lib.rs

@@ -108,7 +108,7 @@ fn check_preconditions(
     Ok(())
 }
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/sanity/src/lib.rs

@@ -36,7 +36,7 @@ fn return_sstruct() -> SStruct {
     SStruct { x: 1, y: 2, z: 3 }
 }
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 pub fn process_instruction(
     program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/sibling_inner_instructions/src/lib.rs

@@ -13,7 +13,7 @@ use solana_program::{
     pubkey::Pubkey,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/sibling_instructions/src/lib.rs

@@ -12,7 +12,7 @@ use solana_program::{
     pubkey::Pubkey,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/simulation/src/lib.rs

@@ -15,7 +15,7 @@ use {
 
 declare_id!("Sim1jD5C35odT8mzctm8BWnjic8xW5xgeb5MbcbErTo");
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 
 pub fn process_instruction(
     _program_id: &Pubkey,

+ 1 - 1
programs/sbf/rust/spoof1/src/lib.rs

@@ -9,7 +9,7 @@ use solana_program::{
     system_program,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/spoof1_system/src/lib.rs

@@ -2,7 +2,7 @@
 
 use solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, pubkey::Pubkey};
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/sysvar/src/lib.rs

@@ -17,7 +17,7 @@ use solana_program::{
     },
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 pub fn process_instruction(
     program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/upgradeable/src/lib.rs

@@ -5,7 +5,7 @@ use solana_program::{
     account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey, sysvar::clock,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 1 - 1
programs/sbf/rust/upgraded/src/lib.rs

@@ -5,7 +5,7 @@ use solana_program::{
     account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey, sysvar::clock,
 };
 
-solana_program::entrypoint!(process_instruction);
+solana_program::entrypoint_no_alloc!(process_instruction);
 fn process_instruction(
     _program_id: &Pubkey,
     accounts: &[AccountInfo],

+ 200 - 62
sdk/program/src/entrypoint.rs

@@ -11,7 +11,7 @@ use {
     std::{
         alloc::Layout,
         cell::RefCell,
-        mem::size_of,
+        mem::{size_of, MaybeUninit},
         ptr::null_mut,
         rc::Rc,
         result::Result as ResultGeneric,
@@ -139,6 +139,59 @@ macro_rules! entrypoint {
     };
 }
 
+/// Declare the program entrypoint and set up global handlers.
+///
+/// This is similar to the `entrypoint!` macro, except that it does not perform
+/// any dynamic allocations, and instead writes the input accounts into a pre-
+/// allocated array.
+///
+/// This version reduces compute unit usage by 20-30 compute units per unique
+/// account in the instruction. It may become the default option in a future
+/// release.
+///
+/// For more information about how the program entrypoint behaves and what it
+/// does, please see the documentation for [`entrypoint!`].
+///
+/// NOTE: This entrypoint has a hard-coded limit of 64 input accounts.
+#[macro_export]
+macro_rules! entrypoint_no_alloc {
+    ($process_instruction:ident) => {
+        /// # Safety
+        #[no_mangle]
+        pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
+            use std::mem::MaybeUninit;
+            // Clippy complains about this because a `const` with interior
+            // mutability `RefCell` should use `static` instead to make it
+            // clear that it can change.
+            // In our case, however, we want to create an array of `AccountInfo`s,
+            // and the only way to do it is through a `const` expression, and
+            // we don't expect to mutate the internals of this `const` type.
+            #[allow(clippy::declare_interior_mutable_const)]
+            const UNINIT_ACCOUNT_INFO: MaybeUninit<AccountInfo> =
+                MaybeUninit::<AccountInfo>::uninit();
+            const MAX_ACCOUNT_INFOS: usize = 64;
+            let mut accounts = [UNINIT_ACCOUNT_INFO; MAX_ACCOUNT_INFOS];
+            let (program_id, num_accounts, instruction_data) =
+                unsafe { $crate::entrypoint::deserialize_into(input, &mut accounts) };
+            // Use `slice_assume_init_ref` once it's stabilized
+            let accounts = &*(&accounts[..num_accounts] as *const [MaybeUninit<AccountInfo<'_>>]
+                as *const [AccountInfo<'_>]);
+
+            #[inline(never)]
+            fn call_program(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> u64 {
+                match $process_instruction(program_id, accounts, data) {
+                    Ok(()) => $crate::entrypoint::SUCCESS,
+                    Err(error) => error.into(),
+                }
+            }
+
+            call_program(&program_id, accounts, &instruction_data)
+        }
+        $crate::custom_heap_default!();
+        $crate::custom_panic_default!();
+    };
+}
+
 /// Define the default global allocator.
 ///
 /// The default global allocator is enabled only if the calling crate has not
@@ -265,6 +318,86 @@ pub const MAX_PERMITTED_DATA_INCREASE: usize = 1_024 * 10;
 /// `assert_eq(std::mem::align_of::<u128>(), 8)` is true for BPF but not for some host machines
 pub const BPF_ALIGN_OF_U128: usize = 8;
 
+#[allow(clippy::arithmetic_side_effects)]
+#[inline(always)] // this reduces CU usage
+unsafe fn deserialize_instruction_data<'a>(input: *mut u8, mut offset: usize) -> (&'a [u8], usize) {
+    #[allow(clippy::cast_ptr_alignment)]
+    let instruction_data_len = *(input.add(offset) as *const u64) as usize;
+    offset += size_of::<u64>();
+
+    let instruction_data = { from_raw_parts(input.add(offset), instruction_data_len) };
+    offset += instruction_data_len;
+
+    (instruction_data, offset)
+}
+
+#[allow(clippy::arithmetic_side_effects)]
+#[inline(always)] // this reduces CU usage by half!
+unsafe fn deserialize_account_info<'a>(
+    input: *mut u8,
+    mut offset: usize,
+) -> (AccountInfo<'a>, usize) {
+    #[allow(clippy::cast_ptr_alignment)]
+    let is_signer = *(input.add(offset) as *const u8) != 0;
+    offset += size_of::<u8>();
+
+    #[allow(clippy::cast_ptr_alignment)]
+    let is_writable = *(input.add(offset) as *const u8) != 0;
+    offset += size_of::<u8>();
+
+    #[allow(clippy::cast_ptr_alignment)]
+    let executable = *(input.add(offset) as *const u8) != 0;
+    offset += size_of::<u8>();
+
+    // The original data length is stored here because these 4 bytes were
+    // originally only used for padding and served as a good location to
+    // track the original size of the account data in a compatible way.
+    let original_data_len_offset = offset;
+    offset += size_of::<u32>();
+
+    let key: &Pubkey = &*(input.add(offset) as *const Pubkey);
+    offset += size_of::<Pubkey>();
+
+    let owner: &Pubkey = &*(input.add(offset) as *const Pubkey);
+    offset += size_of::<Pubkey>();
+
+    #[allow(clippy::cast_ptr_alignment)]
+    let lamports = Rc::new(RefCell::new(&mut *(input.add(offset) as *mut u64)));
+    offset += size_of::<u64>();
+
+    #[allow(clippy::cast_ptr_alignment)]
+    let data_len = *(input.add(offset) as *const u64) as usize;
+    offset += size_of::<u64>();
+
+    // Store the original data length for detecting invalid reallocations and
+    // requires that MAX_PERMITTED_DATA_LENGTH fits in a u32
+    *(input.add(original_data_len_offset) as *mut u32) = data_len as u32;
+
+    let data = Rc::new(RefCell::new({
+        from_raw_parts_mut(input.add(offset), data_len)
+    }));
+    offset += data_len + MAX_PERMITTED_DATA_INCREASE;
+    offset += (offset as *const u8).align_offset(BPF_ALIGN_OF_U128); // padding
+
+    #[allow(clippy::cast_ptr_alignment)]
+    let rent_epoch = *(input.add(offset) as *const u64);
+    offset += size_of::<u64>();
+
+    (
+        AccountInfo {
+            key,
+            is_signer,
+            is_writable,
+            lamports,
+            data,
+            owner,
+            executable,
+            rent_epoch,
+        },
+        offset,
+    )
+}
+
 /// Deserialize the input arguments
 ///
 /// The integer arithmetic in this method is safe when called on a buffer that was
@@ -273,7 +406,6 @@ pub const BPF_ALIGN_OF_U128: usize = 8;
 ///
 /// # Safety
 #[allow(clippy::arithmetic_side_effects)]
-#[allow(clippy::type_complexity)]
 pub unsafe fn deserialize<'a>(input: *mut u8) -> (&'a Pubkey, Vec<AccountInfo<'a>>, &'a [u8]) {
     let mut offset: usize = 0;
 
@@ -290,62 +422,9 @@ pub unsafe fn deserialize<'a>(input: *mut u8) -> (&'a Pubkey, Vec<AccountInfo<'a
         let dup_info = *(input.add(offset) as *const u8);
         offset += size_of::<u8>();
         if dup_info == NON_DUP_MARKER {
-            #[allow(clippy::cast_ptr_alignment)]
-            let is_signer = *(input.add(offset) as *const u8) != 0;
-            offset += size_of::<u8>();
-
-            #[allow(clippy::cast_ptr_alignment)]
-            let is_writable = *(input.add(offset) as *const u8) != 0;
-            offset += size_of::<u8>();
-
-            #[allow(clippy::cast_ptr_alignment)]
-            let executable = *(input.add(offset) as *const u8) != 0;
-            offset += size_of::<u8>();
-
-            // The original data length is stored here because these 4 bytes were
-            // originally only used for padding and served as a good location to
-            // track the original size of the account data in a compatible way.
-            let original_data_len_offset = offset;
-            offset += size_of::<u32>();
-
-            let key: &Pubkey = &*(input.add(offset) as *const Pubkey);
-            offset += size_of::<Pubkey>();
-
-            let owner: &Pubkey = &*(input.add(offset) as *const Pubkey);
-            offset += size_of::<Pubkey>();
-
-            #[allow(clippy::cast_ptr_alignment)]
-            let lamports = Rc::new(RefCell::new(&mut *(input.add(offset) as *mut u64)));
-            offset += size_of::<u64>();
-
-            #[allow(clippy::cast_ptr_alignment)]
-            let data_len = *(input.add(offset) as *const u64) as usize;
-            offset += size_of::<u64>();
-
-            // Store the original data length for detecting invalid reallocations and
-            // requires that MAX_PERMITTED_DATA_LENGTH fits in a u32
-            *(input.add(original_data_len_offset) as *mut u32) = data_len as u32;
-
-            let data = Rc::new(RefCell::new({
-                from_raw_parts_mut(input.add(offset), data_len)
-            }));
-            offset += data_len + MAX_PERMITTED_DATA_INCREASE;
-            offset += (offset as *const u8).align_offset(BPF_ALIGN_OF_U128); // padding
-
-            #[allow(clippy::cast_ptr_alignment)]
-            let rent_epoch = *(input.add(offset) as *const u64);
-            offset += size_of::<u64>();
-
-            accounts.push(AccountInfo {
-                key,
-                is_signer,
-                is_writable,
-                lamports,
-                data,
-                owner,
-                executable,
-                rent_epoch,
-            });
+            let (account_info, new_offset) = deserialize_account_info(input, offset);
+            offset = new_offset;
+            accounts.push(account_info);
         } else {
             offset += 7; // padding
 
@@ -356,18 +435,77 @@ pub unsafe fn deserialize<'a>(input: *mut u8) -> (&'a Pubkey, Vec<AccountInfo<'a
 
     // Instruction data
 
+    let (instruction_data, new_offset) = deserialize_instruction_data(input, offset);
+    offset = new_offset;
+
+    // Program Id
+
+    let program_id: &Pubkey = &*(input.add(offset) as *const Pubkey);
+
+    (program_id, accounts, instruction_data)
+}
+
+/// Deserialize the input arguments
+///
+/// Differs from `deserialize` by writing the account infos into an uninitialized
+/// slice, which provides better performance, roughly 30 CUs per unique account
+/// provided to the instruction.
+///
+/// Panics if the input slice is not large enough.
+///
+/// The integer arithmetic in this method is safe when called on a buffer that was
+/// serialized by runtime. Use with buffers serialized otherwise is unsupported and
+/// done at one's own risk.
+///
+/// # Safety
+#[allow(clippy::arithmetic_side_effects)]
+pub unsafe fn deserialize_into<'a>(
+    input: *mut u8,
+    accounts: &mut [MaybeUninit<AccountInfo<'a>>],
+) -> (&'a Pubkey, usize, &'a [u8]) {
+    let mut offset: usize = 0;
+
+    // Number of accounts present
+
     #[allow(clippy::cast_ptr_alignment)]
-    let instruction_data_len = *(input.add(offset) as *const u64) as usize;
+    let num_accounts = *(input.add(offset) as *const u64) as usize;
     offset += size_of::<u64>();
 
-    let instruction_data = { from_raw_parts(input.add(offset), instruction_data_len) };
-    offset += instruction_data_len;
+    if num_accounts > accounts.len() {
+        panic!(
+            "{} accounts provided, but only {} are supported",
+            num_accounts,
+            accounts.len()
+        );
+    }
+
+    // Account Infos
+
+    for i in 0..num_accounts {
+        let dup_info = *(input.add(offset) as *const u8);
+        offset += size_of::<u8>();
+        if dup_info == NON_DUP_MARKER {
+            let (account_info, new_offset) = deserialize_account_info(input, offset);
+            offset = new_offset;
+            accounts[i].write(account_info);
+        } else {
+            offset += 7; // padding
+
+            // Duplicate account, clone the original
+            accounts[i].write(accounts[dup_info as usize].assume_init_ref().clone());
+        }
+    }
+
+    // Instruction data
+
+    let (instruction_data, new_offset) = deserialize_instruction_data(input, offset);
+    offset = new_offset;
 
     // Program Id
 
     let program_id: &Pubkey = &*(input.add(offset) as *const Pubkey);
 
-    (program_id, accounts, instruction_data)
+    (program_id, num_accounts, instruction_data)
 }
 
 #[cfg(test)]