# Gas Golf on the Aptos Course Optimizing gas on Aptos will become increasingly important, especially because real estate appreciates exponentially with scarcity, according to the [gas curve](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/doc/storage_gas.md#0x1_storage_gas_base_8192_exponential_curve). As developers and citizens of Aptos, we want to develop novel, innovative, yet secure and gas-efficient contracts, because these costs are ultimately passed onto our users. [Skip to practical pointers.](pointers) Gas fees are broken down into three parts in Aptos: - **Intrinsic or payload fees:** This is based on [transaction size](https://github.com/aptos-labs/aptos-core/blob/63e5c543742f17e0f09b98ea39f40744c7cf4abf/aptos-move/aptos-gas/src/transaction.rs#L112), and for most transactions is a flat fee when under a cutoff. - **Storage fees:** These fees are based on what and [how many bytes are stored](https://github.com/aptos-labs/aptos-core/blob/214a952edd19a28e39969ee9c434eba9c2a69f42/aptos-move/aptos-gas/src/transaction.rs#L18) in the global state, and are the *most important consideration.* - **Instruction fees**: These fees are based on computation, what [instructions](https://github.com/aptos-labs/aptos-core/blob/63e5c543742f17e0f09b98ea39f40744c7cf4abf/aptos-move/aptos-gas/src/instr.rs) are being called. Since storage fees make up the bulk of most transactions, here is the breakdown of [storage costs](https://github.com/aptos-labs/aptos-core/blob/5adf30082b287681c1e7e1bbd6b35fe392ceac9b/aptos-move/framework/aptos-framework/sources/storage_gas.move#L420): | Operation | Minimum internal gas | Operation | Minimum internal gas | | --- | --- | --- | --- | | Per item Read | 300,000 | Per byte Read |300 | | Per item Create | 5,000,000 | Per byte Create |5,000 | | Per item Write |300,000 | Per byte Write |5,000 | [Source](https://aptos.dev/concepts/base-gas/#storage-gas-1) A key tenet is that we must read to write. Aptos actually gives us 1 kilobyte (1024 bytes) of write and create for [free](https://github.com/aptos-labs/aptos-core/blob/214a952edd19a28e39969ee9c434eba9c2a69f42/aptos-move/aptos-gas/src/transaction.rs#L96) **per** item, likely because we have already paid the read price. Let’s break down what counts as an item according to this table, and what counts as a read, write, and create. ## What constitutes an item? The delta from a transaction is broken into [four distinct items](https://github.com/aptos-labs/aptos-core/blob/a1d5e038aa2f303c476d88fa8e9ac8f59d0b2ce8/aptos-move/aptos-vm/src/move_vm_ext/session.rs#L171): - **Modules** (contracts), stored under an account address. - **Resources** (structs), stored under an account address. - **Table** entries, specific key-value pairs within a table. - **Aggregator** changes, use of a parallel integer. ## What constitutes a create and write? Creates happen on initialization and writes happen on items previously created in prior transactions. **Writesets** aggregate the create and writes after a script, entry function, or module publish is called. This change set is [squashed](https://github.com/move-language/move/blob/add615b64390ea36e377e2a575f8cb91c9466844/language/move-vm/types/src/values/values_impl.rs#L2457) on-the-fly , which means every item described above belongs to only one class, and you can only be charged for that one respective class: - **Fresh** (Create) - **Cached** (Write) - **Deleted** and **None** ```rust match self { Self::Fresh { .. } | Self::Cached { .. } => { return Err(( PartialVMError::new(StatusCode::RESOURCE_ALREADY_EXISTS), val, )) } Self::None => *self = Self::fresh(val)?, Self::Deleted => *self = Self::cached(val, GlobalDataStatus::Dirty)?, } ``` For example, creating, deleting, and creating an item again in the same transaction is still only one create (**Fresh**). As we can see, creates are the most expensive and should have the most conservative use. Note that deletes do not cost anything, but also currently do not refund users. ## What is a read? Reads are slightly separate because they are not part of the writeset. However, they can be equally important as a per-item read cost the same as a write! A read occurs when a resource is loaded, via the global storage operators `borrow_global`, `exists`, `move_from` or `move_to` , and when a [table is accessed](https://github.com/aptos-labs/aptos-core/blob/a1d5e038aa2f303c476d88fa8e9ac8f59d0b2ce8/aptos-move/aptos-vm/src/move_vm_ext/session.rs#L171). Within a VM session (effectively a transaction), you are only charged for one read per item and the byte-size of item [at most](https://github.com/move-language/move/blob/f20499851934cd51f81b390954a292ca1bd419b8/language/extensions/move-table-extension/src/lib.rs#L329). Caching enforces global storage read to charge each item only once, along with its number of bytes a single time. An important [exception](https://github.com/move-language/move/blob/f20499851934cd51f81b390954a292ca1bd419b8/language/extensions/move-table-extension/src/lib.rs#L329) to this is table: each table operation can charge an item multiple times, but its number of bytes only once. ## Practical Pointers Now with a good conceptual idea of how gas is charged, here are some broad recommendations to think about. Every application has its own nuances, and these can only be practical pointers, not prescriptions. ### Minimize per-item creates Because creates are the most gas intensive, it often makes sense to integrate data structures to save a single create. For example: ```rust struct A has key { message: string::String, } struct B has key { message: string::String } struct AB has key { message_a: string::String, message_b: string::String, } ``` Using struct `A` and `B` means paying for two create items on initialization, and paying for two write items instead of one with `AB` on mutation. However, as we see in our next point, if our struct grows too large we exceed our “free” budget and there is a trade-off. Importantly, you might think using Tables is a way to minimize per-item creates, but every entry is classified as an item. However, one way to minimize the per item of writes and creates with respect to `Table` is to re-write entries, as opposed to deleting them. This requires more careful management on the development side. ### Per Item vs. Per Byte Optimization We mentioned earlier that practically speaking, charging for write and create per byte counts are often not in play because we get 1 kilobyte for free per item per transaction. Counter to minimizing per-item creates, if a struct overflows through the 1 kb budget, it may be worth it to consider creating new items/resources. Let’s suppose we can either create: **A) one item with 2 kilobytes** **B) two items of 1 kilobyte each** On creation of two items, we pay for an extra create per item (5,000,000 gas) but basically break even because we save on 5000 x 1024 bytes of creation per-byte cost. On every future write, which includes read, we pay for one more write item and one more read item (300,000 gas each) but save on 5000 x 1024 bytes of write per-byte cost. The caveat is on every future read, we will have to pay 300,000 more gas with two items. The read per-byte cost is the same. | # Items | # Create Items | # Create Bytes | # Write Items | # Write Bytes | # Read Items | # Read Bytes | | --- | --- | --- | --- | --- | --- | --- | | 1 | 1 | 1024 | 1 | 1024 | 1 | 2048 | | 2 | 2 | 0 | 2 | 0 | 2 | 2048 | We compare the cost of one write in both cases to see when an item is large enough to split. ![](https://i.imgur.com/P3tszEq.png) **600,000 gas saved from write and read of one item ≤ (120+) bytes past 1KB x 5000 of bytes written** If a very large struct cannot be split, another idea might be to partially write to it in several transactions rather than one (trading off the intrinsic transaction gas for the per-byte gas). ### Storage vs. Instruction Trade-offs While a general rule of thumb that **storage >> instructions** in cost makes sense broadly, we need to delve into the details of the specific needs of a contract and DApp. If gas should model computation well, picking the right data structures for the job should save gas generally. For example, if we want to use an associative container data type, we may think `Table` is the best and only option. However, tables are expensive because each entry is per-item, and most of its operations cost more than basic vector operations. With a small amount of items or less access, it actually makes sense to use a [`SimpleMap`]() which uses a vector to implement a map. Despite having to read the full bytes of a vector (one item read, full bytes-per-read), and loop through every item on access, with a small enough set this is more efficient despite a high instruction gas cost and more read bytes. The tradeoff here is between bytes-read, item-writes and instructions against item-reads and item-creations. ## Closing Strokes As we can see, development can be demanding dance. There is a tradeoff between code readability, maintainability, and security, but also gas-efficiency. If you need help optimizing your gas costs, or securing your livelihood, give us a holler. In this post, we specifically focus on Aptos, but in upcoming posts we will delve more into Move, Sui, and more golfing tips. Eventually Aptos will likely have a rent refund model when storage is deleted, but gas optimization will remain important because of the rent being held.