This page gives brief descriptions of optimizations performed.
It is fairly low-level and not required for using Tolk in production.
Constant folding
Tolk compiler evaluates constant variables and conditions at compile-time:if’s condition is guaranteed to be false, only else body is left.
If an assert is proven statically, only the corresponding throw remains.
IF at all (both body and condition evaluation), because it can never be reached.
While calculating compile-time values, all mathematical operators are emulated as they would have run at runtime.
Additional flags like “this value is even / non-positive” are also tracked, leading to more aggressive code elimination.
It works not only for plain variables, but also for struct fields, tensor items, across inlining, etc.
(because it happens after transforming a high-level syntax tree to low-level intermediate representation).
Merge constant builder.storeInt
When building cells manually, there is no need to group constantstoreUint into a single number.
builder.storeInt are merged automatically:
Auto-inline functions
Tolk inlines functions at the compiler level:How does auto-inline work?
- simple, small functions are always inlined
- functions called only once are always inlined
- if
weight < THRESHOLD, the function is always inlined - if
usages == 1, the function is always inlined - otherwise, an empirical formula determines inlining
How to control inlining manually?
@inlineforces inlining even for large functions@noinlineprevents from being inlined@inline_refpreserves an inline reference, suitable for rarely executed paths
What can NOT be auto-inlined?
A function is NOT inlined, even if marked with@inline, if:
- contains
returnin the middle; multiple return points are unsupported - participates in a recursive call chain
f -> g -> f - is used as a non-call; e.g., as a reference
val callback = f
return in the middle:
Peephole and stack optimizations
After the code has been analyzed and transformed to IR, the compiler repeatedly replaces some assembler combinations to equal ones, but cheaper. Some examples are:- stack permutations: DUP + DUP => 2DUP, SWAP + OVER => TUCK, etc.
- N LDU + NIP => N PLDU
- SWAP + N STU => N STUR, SWAP + STSLICE => STSLICER, etc.
- SWAP + EQUAL => EQUAL and other symmetric like MUL, OR, etc.
- 0 EQINT + N THROWIF => N THROWIFNOT and vice versa
- N EQINT + NOT => N NEQINT and other xxx + NOT
- …
- replace a ternary operator to
CONDSEL - evaluate arguments of
asmfunctions in a desired stack order - evaluate struct fields of a shuffled object literal to fit stack order
Lazy loading
The magiclazy keyword loads only required fields from a cell/slice:
Suggestions for manual optimizations
Although the compiler performs substantial work in the background, there are still cases when a developer can gain a few gas units. The primary aspect is changing evaluation order to target fewer stack manipulations. The compiler does not reorder blocks of code unless they are constant expressions or pure calls. But a developer knows the context better. Generally, it looks like this:(v1 v2 v3).
But v1 is used at first, so the stack must be shuffled with SWAP / ROT / XCPU / etc.
If to rearrange assignments or usages — say, move assert(v3) upper — it will naturally pop the topmost element.
Of course, automatic reordering is unsafe and prohibited, but in exact cases business logic might be still valid.
Another option is using bitwise & | instead of logical && ||.
Logical operators are short-circuit: the right operand is evaluated only if required to.
It’s implemented via conditional branches at runtime.
But in some cases, evaluating both operands is less expensive than a dynamic IF.
The last possibility is using low-level Fift code for certain independent tasks that cannot be expressed imperatively.
Usage of exotic TVM instructions like NULLROTRIFNOT / IFBITJMP / etc.
Overriding how top-level Fift dictionary works for routing method_id. And similar.
Old residents call it “deep fifting”.
Anyway, it’s applicable only to a very limited set of goals, mostly as exercises, not as real-world usage.
Do not micro-optimize. Lots of sleepless nights will result in 2-3% gas reducing at best,
producing unreadable code. Just use Tolk as intended.
How to explore Fift assembler
Tolk compiler outputs Fift assembler. The bytecode (bag of cells) is generated by Fift, actually. Projects built on blueprint rely ontolk-js under the hood, which invokes Tolk and then Fift.
As a result:
- for command-line users, fift assembler is the compiler’s output
- for blueprint users, it’s an intermediate result, but can easily be found
npm build or blueprint build in a project.
After successful compilation, a directory build/ is created, and a folder build/ContractName/
contains a .fif file.