This repository was archived by the owner on Jan 8, 2025. It is now read-only.
Replies: 2 comments 1 reply
-
|
Thanks for performing and sharing this great analysis @enitrat. I was able to shed a notable amount of gas from Loot Survivor using this knowledge: BibliothecaDAO/loot-survivor@d9be998 I appreciate you 🙏 |
Beta Was this translation helpful? Give feedback.
0 replies
-
|
I'm wondering in the sierra code if |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
How the compiler optimizes structs modifications
Introduction
This report highlights how the compiler optimizes structs modified in function in order to minimize the amount of cells written to each time a struct field is updated. This work was conducted using scarb 0.6.2 and cairo: 2.1.1
Methodology
We define a file with a multi-field struct
MyStruct, a main function that creates an instance ofMyStructand calls a functionupdate_onethat updates a single field of the struct, and a functionupdate_manythat updates multiple fields of the struct. We then compile this file to sierra and analyze the Sierra code to see how the compiler optimizes the struct modifications.In the middle of the
calling_barandcalling_bar_inlinedfunctions, we add a call to a function that takesMyStructas parameter to show how function inlining affects the optimization.The following file is the source file used for this insvestigation:
Results
Let's start by analyzing the behavior of each function one by one, looking at Sierra code snippets
update_one:Nothing particular here, but let's highlight the important parts of this code to make it easier to understand what's happenning in the next snippets.
[1]is dropped, and a new variable with id[6]is created, whose value is1[6]for the first member, keeping the previous value fort the other membersupdate_manycalling_barbarwhich takesMyStructas an parameter, we need to reconstruct the struct before calling bar, and then deconstruct it again after the call tobarto perform the following modifications. This is because the functionbarcould modify the struct, and we need to make sure that the struct is in a consistent state after the call tobar. This is why we have the following instructions:calling_bar_inlinedbar_inlinedis inlined, the compiler still needs to reconstruct the struct before the call tobar_inlinedand deconstruct it after the call tobar_inlined. This could probably be optimized by the compiler - because we're just callingstruct_constructto callstruct_deconstructright after - but it's not the case for now.store_temp(which supposedly stores the struct in memory prior to the function call) returns the same variable ID, meaning that this is actually a no-op.Overall, the compiler optimizes structs modifications as we would expect a performant compiler to - and it's a feature that is very welcomed compared to the previous way of doing things in Cairo 0, where we had to explicitly define functions to update structs that would manually reconstruct the struct after each update.
What if we looked at Cairo Assembly?
Sierra is an intermediate representation of Cairo code, and it's not the code that is actually executed by the VM. The VM executes Cairo Assembly, which is a lower-level representation of Cairo code. Let's look at the Cairo Assembly generated by the compiler for the same file as above.
Remember that Cairo only has three registers: ap, fp, and pc, and a single memory. Structs are a high-level representation of packed data, but they have no representation in Cairo Assembly. Instead, structs data are represented as a sequence of cells in memory, and the compiler keeps track of the offset of each field of the struct in memory.
Inlined functions
While not the primary objective of this study, it's interesting to see how the compiler optimizes the code when the function is inlined. Let's look at the Cairo Assembly generated for both
calling_barcalling_bar_inlined:calling_barcalling_bar_inlinedThe first noticeable thing here is that inlined function only runs one extra instruction, the
call rel 11, but otherwise both run the same amount of instructions. In the inlined version, all the fields ofmy_structare written to memory before the execution of the inlined bar function. The normal version is similar, except that it uses acallinstruction to execute the non-inlined version of thebarfunction. Both versions execute the same amount of instructionsBack to understanding how structs are handled
Let's look at the Cairo Assembly generated for the
update_manyandcalling_barfunctions. The compiled code forcalling_baris available above.update_manyThis one is pretty straightforward - we just write the values of the struct fields to memory, and since no other operation is performed in between, we can just return them as is (as our function returns
my_structpassed byref).calling_barThe main difference here is that we need to store the struct in memory before calling
bar, and then store it in memory again it after the call tobarto return it from thecalling_barfunction.Basically, what happens here is that we write the values of the struct fields to memory (in order to call the function
bar), and oncebarreturns we still have to update the last two fields. Becausemy_structis passed asref, it is returned from the function - therefore, the last instructions in our function are to write all the fields ofmy_structto memory. Given that we already updated the values ofa,b, we just refer to the values stored in memory previously for these fields, and we write the new values fordandeto memory.Why you should minimize the amount of functions called
Every time you make a function call, you need to write the arguments to memory and if that functions returns something, you need to push it back to memory before returning.
To illustrate this more accurately, let's focus on this specific case: using multiple setters instead of updating the struct directly. Let's look at the following code:
Let's see how the following compiles to CASM:
Look at how the following is way more optimized:
Good programming practices would expect you to use getters and setters to modify/access struct members, as it makes refactoring easier. But if you're looking into optimizations, it might not be the best idea...
One last thing: What if our getters/setters were inlined?
Let's look at the following code:
Surely, we would expect the compiler to behave the same way as what we noticed before, right? Well, not exactly. Let's look at the CASM generated for this code:
It's strictly equivalent to the code generated when we directly update the struct members. So the solution might actually be to use inlined methods!
One weird edge case: inlining free functions
We would expect it to be the same result as with methods, but it's not the case! However, the difference is actually not that big: When we look at the main function (the last one in this block, what the program really executes), it's the same - the only difference is that the CASM codes contain the code of the inlined functions, but they're not called in this specific case.
Here's the output
Conclusion
This study gave us more insights into how the compiler optimizes struct modifications. We can draw one very valuable conclusion: what is actually expensive is to have multiple functions call and pass around the struct as an argument, which requires writing the struct to memory before each function call and reading it back from memory after each function call.
We highlighted the fact that methods and free functions are not treated the same way by the compiler and that inlining free functions can actually be a good idea if you're looking for performance.
If you want to combine best programming practices and good optimizations, then consider defining methods on your type and inlining them. This way, you can still refactor your code easily, and you can benefit from the optimizations of the compiler.
Beta Was this translation helpful? Give feedback.
All reactions