Performance Characteristics
This document explains the performance characteristics of mojo-toml v0.9.1 and the copying behaviour inherent in Mojo’s ownership model.
Benchmark Results (v0.9.1)
Performance measurements on Apple M1 Max (macOS). See benchmarks/benchmark_parser.mojo for Mojo benchmarks and benchmarks/compare_python.py for Python baselines.
Parser Performance (mojo-toml)
| Operation | Throughput | Average Time | Notes |
|---|---|---|---|
| Simple parse (5 keys) | 40,546 parses/sec | 24 μs | Very fast for small configs |
| Nested tables (7 keys) | 4,834 parses/sec | 206 μs | Good for structured configs |
| Arrays (~40 elements) | 7,025 parses/sec | 142 μs | Efficient array handling |
| Dotted keys (9 keys) | 5,176 parses/sec | 193 μs | Comparable to table headers |
| Large document (40+ keys) | 331 parses/sec | 3 ms | Still fast for real-world files |
| Real pixi.toml | 368 parses/sec | 2 ms | Production-ready performance |
| Table access | 99,592 accesses/sec | 10 μs | Negligible overhead |
Comparison with Python (tomllib)
| Operation | mojo-toml | Python tomllib | Speedup |
|---|---|---|---|
| Simple document (5 keys) | 24 μs | 11 μs | 0.5x (slower) |
| Nested tables (6 keys) | ~200 μs* | 22 μs | 0.1x (slower) |
| Array of tables (2 products) | ~180 μs* | 14 μs | 0.1x (slower) |
| Alternative number bases (3 numbers) | ~20 μs* | 8 μs | 0.4x (slower) |
*Estimated from similar complexity benchmarks
Writer Performance Comparison
| Operation | mojo-toml | Python tomli_w | Speedup |
|---|---|---|---|
| Simple document (5 keys) | Not yet benchmarked | 5 μs | TBD |
| Nested tables (6 keys) | Not yet benchmarked | 9 μs | TBD |
Key Findings
- Parsing is fast: Even large documents parse in milliseconds
- Table access overhead is minimal: 10 μs per nested
as_table()call - Real-world performance is excellent: pixi.toml parses in ~2ms
- Python is faster for now: CPython’s
tomllibis currently 2-10x faster due to optimised C implementation - The copying overhead is acceptable for typical use cases
Copying Behavior
Why Copying Happens
mojo-toml uses value semantics throughout, which means data is copied rather than shared. This is a deliberate design choice driven by Mojo’s ownership model:
- Safety: Prevents aliasing issues and use-after-free bugs
- Simplicity: No lifetime annotations or borrow checker complexity
- Predictability: Clear ownership semantics
Where Copying Occurs
1. as_table() and as_array() Methods
fn as_table(self) raises -> Dict[String, TomlValue]:
# Returns a COPY of the table
var result = Dict[String, TomlValue]()
for entry in self.table_value.items():
result[entry.key] = entry.value.copy() # Deep copy
return result^
Why: Mojo doesn’t currently support borrowed returns from struct methods. The copy ensures safe ownership transfer.
Impact: 10 μs per call (negligible for typical usage)
2. Parser Internal Functions
Functions like ensure_table_path() and set_in_table_path() use functional-style copying:
fn ensure_table_path(mut self, result: Dict[String, TomlValue], path: List[String]) raises -> Dict[String, TomlValue]:
# Creates new Dict with modifications
var new_result = Dict[String, TomlValue]()
for entry in result.items():
new_result[entry.key] = entry.value.copy()
# ... modifications ...
return new_result^
Why: Mojo’s ownership prevents in-place mutation of borrowed parameters in some contexts.
Impact: O(depth × keys) during parsing, but amortized over the entire parse operation (still 2-3ms for large files)
3. KeyValuePair Extraction
var parsed_key = pair.key
var parsed_value = pair.value.copy()
Why: Mojo prevents partial destruction - can’t move pair.value while pair.key is still needed.
Impact: One copy per key-value pair during parsing (included in overall parse time)
Performance Recommendations
✅ Do This (Efficient)
// Cache table references if accessing multiple times
var db = config["database"].as_table()
var host = db["host"].as_string()
var port = db["port"].as_int()
var user = db["user"].as_string() // ~26 μs total
❌ Avoid This (Wasteful)
// Don't call as_table() repeatedly
var host = config["database"].as_table()["host"].as_string()
var port = config["database"].as_table()["port"].as_int()
var user = config["database"].as_table()["user"].as_string() // ~30 μs total
The difference is small (4 μs) but caching is better practice.
Reuse Parsers
// Efficient: Reuse parser instance
var parser = Parser(tokens1^)
var data1 = parser.parse()
parser.reset(tokens2^)
var data2 = parser.parse()
This avoids allocating new Parser objects.
Future Optimizations
Potential Improvements
Borrowed References: If Mojo adds support for borrowed struct method returns:
fn as_table_ref(borrowed self) -> borrowed Dict[String, TomlValue]: return self.table_value // Zero-copy!In-Place Mutation: If Mojo’s ownership allows more flexible mutation:
fn ensure_table_path_mut(mut self, mut result: Dict[String, TomlValue], ...) // Mutate result directly instead of copyingArena Allocation: Use a memory arena for parser-internal structures
Why Not Now?
- Mojo limitations: Current language doesn’t support borrowed returns from methods
- Minimal impact: Benchmark shows copying overhead is negligible (10 μs)
- Correctness first: Value semantics ensure safety and correctness
- Future-proof: Can optimize when Mojo adds features without API changes
Is Performance Acceptable?
Yes, for typical use cases:
- Configuration files parse in milliseconds: Real pixi.toml parses in ~2ms
- Table access is fast: Negligible 10 μs overhead per
as_table()call - Throughput is good: 40K+ parses/sec for simple configs, 300+ for large documents
- Value semantics provide safety: No aliasing bugs or lifetime complexity
Python is currently faster (2-10x) because CPython’s tomllib is implemented in optimised C. However: - Pure Python parsers like tomli are slower (5-10ms vs 2ms for pixi.toml) - Mojo is still early-stage; compiler optimisations will improve - The difference is negligible for config file use cases (microseconds)
Conclusion
The copying behaviour in mojo-toml is:
- Necessary: Due to Mojo’s current ownership model
- Fast enough: 10 μs per table access is negligible for config files
- Acceptable: Real-world files parse in 2-3ms - fast enough for any config loading scenario
- Improvable: Future Mojo features could eliminate copies and match C performance
For 99% of use cases (reading configuration files), the current performance is excellent. The value semantics provide safety and simplicity without meaningful performance cost.
Running Benchmarks
Both benchmarks generate markdown reports with full system information:
# Mojo benchmark (with report generation)
pixi run benchmark-mojo
# Python baseline (tomllib/tomli_w comparison)
pixi run benchmark-pythonReports are saved to benchmarks/reports/ with complete system information. You can also run the raw Mojo benchmark:
mojo -I src benchmarks/benchmark_parser.mojoIf you have specific performance requirements or large-scale parsing needs, please file an issue with your use case.