Data Structure Fee Research
TL;DR
- Tree outer (avl.Tree): O(log N) gas growth — +45% at N=1,000 (TreeMap 1.1M → 2.2M)
- Map outer (map): O(N) gas growth — +1,734% at N=1,000 (MapMap 498K → 9.1M)
- Inner struct ≈ inner map: gas difference < 1%, struct ~7% cheaper on storage
- Inner tree: gas 2x, storage 5.6x — inefficient
- Optimal: TreeStruct (gas 2.2M + storage 333K = total 2.5M ugnot at N=1,000)
- vs current TreeMap: TreeStruct is gas -0.2%, storage -6.6% — practically equivalent
1) Background and Goal
Personal World currently stores world data as avl.Tree → map[string]string. This experiment measures total fees (gas + storage deposit) for 6 alternative outer/inner data structure combinations to identify the optimal storage pattern.
6 combinations tested:
| Name | Outer | Inner |
|---|---|---|
| TreeTree | avl.Tree | *avl.Tree |
| TreeMap | avl.Tree | map[string]string |
| TreeStruct | avl.Tree | *WorldData |
| MapTree | map[string]*avl.Tree | — |
| MapMap | map[string]map[string]string | — |
| MapStruct | map[string]*WorldData | — |
Hypothesis:
- Tree outer = O(log N) gas growth, map outer = O(N) gas growth (full map re-serialization)
- Inner overhead: struct < map < tree
Goals:
- Quantify gas and storage fee differences across combinations
- Determine if map outer is viable at scale or becomes prohibitive
- Identify the most cost-efficient combination for production use
Contract (gno.land/r/akkadia/ds_fee):
package ds_fee
import "gno.land/p/nt/avl"
type WorldData struct {
Owner string
Name string
Slug string
SizeID string
Size string
Seed string
Biome string
IsVisible string
Options string
}
var (
treeTree = avl.NewTree() // avl.Tree → *avl.Tree
treeMap = avl.NewTree() // avl.Tree → map[string]string
treeStruct = avl.NewTree() // avl.Tree → *WorldData
mapTree = make(map[string]*avl.Tree) // map → *avl.Tree
mapMap = make(map[string]map[string]string) // map → map[string]string
mapStruct = make(map[string]*WorldData) // map → *WorldData
)
// 6 setters — all share the same signature:
// func Set<Name>(cur realm, key, owner, name, slug, sizeID, size, seed, biome, isVisible, options string)
//
// TreeTree/MapTree: inner avl.Tree with 9 field Set calls
// TreeMap/MapMap: map[string]string literal with 9 fields
// TreeStruct/MapStruct: &WorldData{...} with 9 fields
func GetSize() int { return treeTree.Size() }2) Test Environment
- Date: 2026-02-09
- Execution: gnodev (local machine)
- Gno RPC:
localhost:26657 - Chain ID:
dev - Scripts:
bin/gno/research/ds_fee.sh all - Milestones: 1, 500, 1,000
- Repeat: 3
3) Results
TreeTree
| N (entries) | Gas (avg) | Gas Q1 | Gas Q3 | Storage (avg) | Storage Q1 | Storage Q3 |
|---|---|---|---|---|---|---|
| 1 | 2,322,382 | -2.8% | +2.4% | 1,959,200 | -0.0% | +0.0% |
| 500 | 3,252,450 | -0.0% | +0.0% | 1,985,066 | -0.0% | +0.0% |
| 1,000 | 3,358,444 | -0.0% | +0.0% | 1,996,700 | +0.0% | +0.0% |
TreeMap
| N (entries) | Gas (avg) | Gas Q1 | Gas Q3 | Storage (avg) | Storage Q1 | Storage Q3 |
|---|---|---|---|---|---|---|
| 1 | 1,146,272 | -5.6% | +4.9% | 350,100 | -0.0% | +0.0% |
| 500 | 2,072,640 | -0.0% | +0.0% | 354,966 | -0.2% | +0.1% |
| 1,000 | 2,177,018 | -0.0% | +0.0% | 356,100 | +0.0% | +0.0% |
TreeStruct
| N (entries) | Gas (avg) | Gas Q1 | Gas Q3 | Storage (avg) | Storage Q1 | Storage Q3 |
|---|---|---|---|---|---|---|
| 1 | 1,142,187 | -5.6% | +4.9% | 325,600 | -0.0% | +0.0% |
| 500 | 2,068,933 | -0.0% | +0.0% | 331,066 | -0.2% | +0.1% |
| 1,000 | 2,173,226 | -0.0% | +0.0% | 332,500 | +0.0% | +0.0% |
MapTree
| N (entries) | Gas (avg) | Gas Q1 | Gas Q3 | Storage (avg) | Storage Q1 | Storage Q3 |
|---|---|---|---|---|---|---|
| 1 | 2,155,085 | -0.2% | +0.2% | 1,773,600 | +0.0% | +0.0% |
| 500 | 6,701,551 | -0.1% | +0.1% | 1,795,200 | +0.0% | +0.0% |
| 1,000 | 11,265,455 | -0.0% | +0.0% | 1,806,000 | +0.0% | +0.0% |
MapMap
| N (entries) | Gas (avg) | Gas Q1 | Gas Q3 | Storage (avg) | Storage Q1 | Storage Q3 |
|---|---|---|---|---|---|---|
| 1 | 497,777 | -0.9% | +0.9% | 164,500 | +0.0% | +0.0% |
| 500 | 4,799,973 | -0.1% | +0.1% | 165,100 | +0.0% | +0.0% |
| 1,000 | 9,120,757 | -0.0% | +0.0% | 165,400 | +0.0% | +0.0% |
MapStruct
| N (entries) | Gas (avg) | Gas Q1 | Gas Q3 | Storage (avg) | Storage Q1 | Storage Q3 |
|---|---|---|---|---|---|---|
| 1 | 494,572 | -1.0% | +1.0% | 140,000 | +0.0% | +0.0% |
| 500 | 5,246,656 | -0.1% | +0.1% | 141,200 | +0.0% | +0.0% |
| 1,000 | 10,018,176 | -0.0% | +0.0% | 141,800 | +0.0% | +0.0% |
4) Analysis
Outer: Tree vs Map
Comparing equivalent inner types to isolate outer structure impact:
| Metric | Tree (TreeMap) | Map (MapMap) | Observation |
|---|---|---|---|
| Gas at N=1 | 1,146,272 | 497,777 | Map 2.3x cheaper at N=1 |
| Gas at N=1,000 | 2,177,018 | 9,120,757 | Tree 4.2x cheaper at N=1,000 |
| Gas growth 1→1,000 | +90% | +1,733% | Tree = O(log N), Map = O(N) |
| Storage at N=1,000 | 356,100 | 165,400 | Map 2.2x cheaper (constant) |
- Tree gas grows sub-linearly (O(log N)): only +90% from N=1 to N=1,000
- Map gas grows linearly (O(N)): +1,733% from N=1 to N=1,000 — entire map re-serialized on every write
- Crossover point: Map is cheaper below ~100 entries, Tree wins above that
- Storage: Map is consistently ~2x cheaper (no AVL node overhead)
Inner: Tree vs Map vs Struct
Comparing inner types within Tree outer to isolate inner structure impact:
| Metric | Tree (TreeTree) | Map (TreeMap) | Struct (TreeStruct) | Observation |
|---|---|---|---|---|
| Gas at N=1 | 2,322,382 | 1,146,272 | 1,142,187 | Struct ≈ Map, Tree 2x more |
| Gas at N=1,000 | 3,358,444 | 2,177,018 | 2,173,226 | Same pattern at scale |
| Storage at N=1,000 | 1,996,700 | 356,100 | 332,500 | Tree 6x more storage |
- Struct vs Map: Gas nearly identical (< 0.2% difference), storage struct is ~6.6% cheaper
- Tree inner: 2x gas, 5.6x storage — each inner avl.Tree stores 9 AVL nodes vs 1 flat object
- Inner tree overhead is dominated by node allocation, not lookup
Total Fee Ranking (at N=1,000)
| Rank | Combination | Gas | Storage | Total (gas fee + storage) | Notes |
|---|---|---|---|---|---|
| 1 | TreeStruct | 2,173,226 | 332,500 | 2,505,726 | Optimal |
| 2 | TreeMap | 2,177,018 | 356,100 | 2,533,118 | Current production pattern |
| 3 | TreeTree | 3,358,444 | 1,996,700 | 5,355,144 | Inner tree overhead |
| 4 | MapMap | 9,120,757 | 165,400 | 9,286,157 | O(N) gas kills it |
| 5 | MapStruct | 10,018,176 | 141,800 | 10,159,976 | O(N) gas, cheap storage |
| 6 | MapTree | 11,265,455 | 1,806,000 | 13,071,455 | Worst of both worlds |
5) Conclusions
- Outer structure:
avl.Treeis clearly superior at scale. Map outer’s O(N) re-serialization makes it 4-5x more expensive at N=1,000 and will only worsen. Map is only viable for very small, bounded collections (< 100 entries). - Inner structure:
*WorldData(struct) andmap[string]stringare practically equivalent in gas (< 0.2% difference). Struct saves ~7% on storage. Inner*avl.Treeis 2x gas and 6x storage — avoid. - Optimal combination: TreeStruct (
avl.Tree → *WorldData) — lowest total fee at scale. - Production impact: Current TreeMap is only ~1.1% more expensive than TreeStruct in total fees. Migration is not urgent but worth considering for new contracts.
- Hypothesis confirmed: Tree outer = O(log N) ✓, Map outer = O(N) ✓, Inner struct ≈ map < tree ✓
Last updated on