Performance Tuning¶
Optimize Zig EVM for maximum throughput and efficiency.
Baseline Performance¶
| Metric | Value |
|---|---|
| Single execution | ~1M gas/second |
| Parallel throughput | 5-6x improvement (8 threads) |
| Memory overhead | ~100KB per EVM instance |
| Startup time | <1ms for instance creation |
Parallel Execution Optimization¶
Thread Count Selection¶
| Threads | Typical Speedup |
|---|---|
| 1 | 1.0x (baseline) |
| 2 | 1.8-2.0x |
| 4 | 3.2-3.8x |
| 8 | 5.0-6.5x |
| 16 | 6.0-8.0x (diminishing returns) |
Recommendation: Use number_of_cpu_cores - 1 for best results.
Batch Size Optimization¶
| Batch Size | Throughput | Overhead |
|---|---|---|
| 10 | Low | High (setup dominates) |
| 100 | Medium | Moderate |
| 1000 | High | Low |
| 10000 | Very High | Very Low |
| 100000+ | Maximum | Minimal |
Recommendation: 1000-10000 transactions per batch.
Workload Characteristics¶
High Parallelism (5-6x speedup):
- Independent transfers
- Different senders/receivers
- No shared storage
Low Parallelism (1-2x speedup):
- Same sender (nonce ordering)
- Shared contract state
- DEX trades on same pair
Configuration Tuning¶
from zigevm import BatchExecutor, BatchConfig
# High-throughput configuration
config = BatchConfig(
max_threads=8, # Match CPU cores
enable_parallel=True,
enable_speculation=True, # For low-conflict workloads
block_gas_limit=30000000,
)
Memory Optimization¶
Instance Pooling¶
Reuse EVM instances instead of creating new ones:
class EVMPool:
def __init__(self, size=8):
self.pool = [EVM() for _ in range(size)]
self.available = list(range(size))
def acquire(self):
if self.available:
idx = self.available.pop()
self.pool[idx].reset()
return idx, self.pool[idx]
return None, None
def release(self, idx):
self.available.append(idx)
Memory Limits¶
Set appropriate memory limits to prevent excessive allocation:
Arena Allocators¶
In Zig, use arena allocators for temporary operations:
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
var evm = try EVM.init(arena.allocator());
// All memory freed when arena is deinitialized
Gas Optimization¶
Efficient Gas Estimation¶
def estimate_gas(evm, code, calldata):
# Use binary search for accurate estimation
low, high = 21000, 10_000_000
while low < high:
mid = (low + high) // 2
evm.set_gas_limit(mid)
evm.reset()
result = evm.execute(code, calldata)
if result.success:
high = mid
else:
low = mid + 1
return low * 1.1 # Add 10% buffer
Avoid Expensive Operations¶
| Operation | Cost | Alternative |
|---|---|---|
| SSTORE | 5000-20000 | Batch writes |
| CREATE | 32000 | CREATE2 for predictable addresses |
| Memory expansion | Quadratic | Fixed-size buffers |
| LOG4 | 1875+ | Fewer topics |
Benchmarking¶
Running Benchmarks¶
# Full benchmark suite
zig build bench-full
# Parallel execution benchmarks
zig build benchmark
# Simple benchmarks
zig build bench
Profiling¶
# Build with profiling
zig build -Doptimize=ReleaseFast
# Run with perf
perf record -g ./zig-out/bin/zig-evm
perf report
Metrics to Track¶
| Metric | Target |
|---|---|
| Transactions/second | >10,000 |
| Gas/second | >1M |
| Parallelism ratio | >0.7 |
| Memory per TX | <10KB |
Optimization Techniques¶
1. Pre-sort Transactions¶
Sort by sender to improve nonce handling:
2. Group Similar Transactions¶
Batch transactions with similar characteristics:
# Separate simple transfers from contract calls
simple = [tx for tx in txs if tx.gas_limit < 25000]
complex = [tx for tx in txs if tx.gas_limit >= 25000]
# Execute in separate batches
executor.execute(simple)
executor.execute(complex)
3. Pre-analyze Dependencies¶
For known contracts, pre-compute access patterns:
def analyze_erc20_transfer(tx):
return {
'reads': [tx.from_addr, tx.to_addr],
'writes': [tx.from_addr, tx.to_addr],
}
4. Use Speculation Wisely¶
Enable speculation only for low-conflict workloads:
5. Optimize Calldata¶
Minimize calldata size:
Production Deployment¶
Resource Allocation¶
| Component | Recommendation |
|---|---|
| CPU | 8+ cores dedicated |
| Memory | 16GB+ |
| Storage | SSD for state access |
Monitoring¶
Track these metrics:
- Transaction throughput
- Gas utilization
- Parallel wave count
- Rollback rate (if speculation enabled)
- Memory usage
Error Handling¶
def execute_with_retry(executor, transactions, max_retries=3):
for attempt in range(max_retries):
try:
stats = executor.execute(transactions)
if stats.failed_transactions == 0:
return stats
# Handle failures
failed = get_failed_transactions(executor)
transactions = retry_failed(failed)
except Exception as e:
if attempt == max_retries - 1:
raise
time.sleep(0.1 * (2 ** attempt))
Scaling¶
For very high throughput:
- Horizontal scaling: Multiple executor instances
- Sharding: Partition transactions by sender
- Pipelining: Overlap dependency analysis with execution
Performance Comparison¶
vs. Other EVM Implementations¶
| Implementation | Throughput | Parallelism |
|---|---|---|
| geth | ~1000 tx/s | Sequential |
| reth | ~2000 tx/s | Limited |
| zig-evm | ~10000 tx/s | Full parallel |
Benchmark Results¶
Zig EVM Parallel Execution Benchmark
====================================
Batch size: 10000 transactions
Threads: 8
Sequential execution:
Time: 2450 ms
Throughput: 4,081 tx/s
Parallel execution:
Time: 410 ms
Throughput: 24,390 tx/s
Waves: 127
Max parallelism: 8
Speedup: 5.97x
Dependency analysis:
Time: 12 ms
Overhead: 2.9%
Troubleshooting Performance Issues¶
Low Throughput¶
Symptoms: <5000 tx/s
Check:
- Thread count configuration
- Batch size (increase if <100)
- Workload conflicts
High Memory Usage¶
Symptoms: >1GB for 10000 tx
Check:
- Return data accumulation
- Log storage
- Memory leaks in custom code
Inconsistent Performance¶
Symptoms: Variable tx/s
Check:
- System load
- GC pauses (in binding languages)
- I/O contention