Skip to content

Commit 93694e5

Browse files
committed
added benchmarks
1 parent ce2f89f commit 93694e5

4 files changed

Lines changed: 235 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ keywords = ["string", "runtime", "format", "template"]
1010
license = "MIT OR Apache-2.0"
1111
readme = "README.md"
1212
repository = "https://github.com/clitic/formatx"
13+
exclude = ["images"]
1314

1415
[dependencies]

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
`formatx` lets you format strings at runtime using the same syntax as [`std::fmt`] (`{}`, `{:?}`, `{name}`, etc.), but with runtime template strings instead of compile-time literals with **zero** dependencies.
1111

12+
<div align="center">
13+
<img src="https://raw.githubusercontent.com/clitic/formatx/refs/heads/main/docs/images/benchmark.svg" width="700px">
14+
</div>
15+
1216
## Getting Started
1317

1418
Add this to your `Cargo.toml` file.
@@ -40,6 +44,9 @@ let result = formatx!(template, "Alice", "maths", score = 95.678).unwrap();
4044
assert_eq!(result, "Alice scored 95.7% in maths");
4145
```
4246

47+
> [!NOTE]
48+
> Extra arguments that aren't referenced by any placeholder are silently ignored in both `formatx!` and `formatxl!`.
49+
4350
### Template Reuse
4451

4552
Parse once, render many times with [`Template`](https://docs.rs/formatx/latest/formatx/struct.Template.html).

examples/bench.rs

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
use std::{fmt::Write, fs, time::Instant};
2+
3+
const ITERATIONS: u32 = 1_000_000;
4+
5+
fn generate_svg(results: &[BenchResult]) -> String {
6+
let max_ns = results
7+
.iter()
8+
.flat_map(|r| [r.format_ns, r.formatx_ns])
9+
.fold(0.0_f64, f64::max);
10+
11+
let chart_h = 240;
12+
let top = 90;
13+
let bottom = top + chart_h;
14+
let group_w: usize = 140;
15+
let bar_w: usize = 45;
16+
let gap: usize = 8;
17+
let left: usize = 70;
18+
let total_w = left + group_w * results.len() + 20;
19+
let total_h = bottom + 60;
20+
21+
let mut svg = String::new();
22+
let _ = writeln!(
23+
svg,
24+
"<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 {total_w} {total_h}\" font-family=\"'Segoe UI', system-ui, sans-serif\">"
25+
);
26+
let _ = writeln!(
27+
svg,
28+
"<rect width=\"{total_w}\" height=\"{total_h}\" fill=\"#1e293b\"/>"
29+
);
30+
31+
// Title
32+
let cx = total_w / 2;
33+
let _ = writeln!(
34+
svg,
35+
"<text x=\"{cx}\" y=\"28\" text-anchor=\"middle\" fill=\"#f1f5f9\" font-size=\"16\" font-weight=\"700\">formatx! vs format!</text>"
36+
);
37+
let _ = writeln!(
38+
svg,
39+
"<text x=\"{cx}\" y=\"44\" text-anchor=\"middle\" fill=\"#94a3b8\" font-size=\"11\">{ITERATIONS} iterations \u{00b7} release mode \u{00b7} lower is better</text>"
40+
);
41+
42+
// Legend
43+
let lx = cx - 70;
44+
let _ = writeln!(
45+
svg,
46+
"<rect x=\"{lx}\" y=\"52\" width=\"12\" height=\"12\" fill=\"#818cf8\"/><text x=\"{}\" y=\"63\" fill=\"#c7d2fe\" font-size=\"11\">format!</text>",
47+
lx + 17
48+
);
49+
let rx = cx + 20;
50+
let _ = writeln!(
51+
svg,
52+
"<rect x=\"{rx}\" y=\"52\" width=\"12\" height=\"12\" fill=\"#fb923c\"/><text x=\"{}\" y=\"63\" fill=\"#fed7aa\" font-size=\"11\">formatx!</text>",
53+
rx + 17
54+
);
55+
56+
// Y-axis label
57+
let mid_y = top + chart_h / 2;
58+
let _ = writeln!(
59+
svg,
60+
"<text x=\"14\" y=\"{mid_y}\" text-anchor=\"middle\" fill=\"#64748b\" font-size=\"10\" transform=\"rotate(-90, 14, {mid_y})\">(ns/iter)</text>"
61+
);
62+
63+
// Y-axis gridlines
64+
for i in 0..=4 {
65+
let scale = max_ns * 1.15;
66+
let ns = scale * i as f64 / 4.0;
67+
let y = bottom - (chart_h as f64 * i as f64 / 4.0) as usize;
68+
let _ = writeln!(
69+
svg,
70+
"<line x1=\"{left}\" y1=\"{y}\" x2=\"{}\" y2=\"{y}\" stroke=\"#334155\" stroke-width=\"1\"/>",
71+
left + group_w * results.len()
72+
);
73+
let _ = writeln!(
74+
svg,
75+
"<text x=\"{}\" y=\"{}\" text-anchor=\"end\" fill=\"#64748b\" font-size=\"10\">{:.0}</text>",
76+
left - 6,
77+
y + 4,
78+
ns
79+
);
80+
}
81+
82+
// Bars
83+
for (i, r) in results.iter().enumerate() {
84+
let gx = left + i * group_w + (group_w - bar_w * 2 - gap) / 2;
85+
86+
let scale = max_ns * 1.15;
87+
let h1 = (r.format_ns / scale * chart_h as f64).max(2.0) as usize;
88+
let h2 = (r.formatx_ns / scale * chart_h as f64).max(2.0) as usize;
89+
90+
let x1 = gx;
91+
let x2 = gx + bar_w + gap;
92+
let y1 = bottom - h1;
93+
let y2 = bottom - h2;
94+
95+
// format! bar + label
96+
let _ = writeln!(
97+
svg,
98+
"<rect x=\"{x1}\" y=\"{y1}\" width=\"{bar_w}\" height=\"{h1}\" fill=\"#818cf8\"/>"
99+
);
100+
let _ = writeln!(
101+
svg,
102+
"<text x=\"{}\" y=\"{}\" text-anchor=\"middle\" fill=\"#c7d2fe\" font-size=\"10\">{:.0} ns</text>",
103+
x1 + bar_w / 2,
104+
y1 - 6,
105+
r.format_ns
106+
);
107+
108+
// formatx! bar + label
109+
let _ = writeln!(
110+
svg,
111+
"<rect x=\"{x2}\" y=\"{y2}\" width=\"{bar_w}\" height=\"{h2}\" fill=\"#fb923c\"/>"
112+
);
113+
let _ = writeln!(
114+
svg,
115+
"<text x=\"{}\" y=\"{}\" text-anchor=\"middle\" fill=\"#fed7aa\" font-size=\"10\">{:.0} ns</text>",
116+
x2 + bar_w / 2,
117+
y2 - 6,
118+
r.formatx_ns
119+
);
120+
121+
// X-axis label
122+
let center_x = gx + (2 * bar_w + gap) / 2;
123+
let _ = writeln!(
124+
svg,
125+
"<text x=\"{center_x}\" y=\"{}\" text-anchor=\"middle\" fill=\"#e2e8f0\" font-size=\"11\">{}</text>",
126+
bottom + 16,
127+
r.label
128+
);
129+
}
130+
131+
// Footer
132+
let _ = writeln!(
133+
svg,
134+
"<text x=\"{cx}\" y=\"{}\" text-anchor=\"middle\" fill=\"#64748b\" font-size=\"10\" font-style=\"italic\">Expected overhead - formatx! parses and resolves format strings at runtime.</text>",
135+
total_h - 10
136+
);
137+
138+
let _ = writeln!(svg, "</svg>");
139+
svg
140+
}
141+
142+
struct BenchResult {
143+
label: &'static str,
144+
format_ns: f64,
145+
formatx_ns: f64,
146+
}
147+
148+
fn bench<F: Fn() -> String>(f: F) -> f64 {
149+
for _ in 0..1000 {
150+
std::hint::black_box(f());
151+
}
152+
let start = Instant::now();
153+
for _ in 0..ITERATIONS {
154+
std::hint::black_box(f());
155+
}
156+
start.elapsed().as_nanos() as f64 / ITERATIONS as f64
157+
}
158+
159+
fn main() {
160+
let results = vec![
161+
BenchResult {
162+
label: "{} + {} = {}",
163+
format_ns: bench(|| format!("{} + {} = {}", 1, 2, 3)),
164+
formatx_ns: bench(|| formatx::formatx!("{} + {} = {}", 1, 2, 3).unwrap()),
165+
},
166+
BenchResult {
167+
label: "{name} scored {score}%",
168+
format_ns: bench(|| format!("{name} scored {score}%", name = "Alice", score = 95)),
169+
formatx_ns: bench(|| {
170+
formatx::formatx!("{name} scored {score}%", name = "Alice", score = 95).unwrap()
171+
}),
172+
},
173+
BenchResult {
174+
label: "{:+08.2}",
175+
format_ns: bench(|| format!("{:+08.2}", 3.14159)),
176+
formatx_ns: bench(|| formatx::formatx!("{:+08.2}", 3.14159).unwrap()),
177+
},
178+
BenchResult {
179+
label: "{:-^20}",
180+
format_ns: bench(|| format!("{:-^20}", "center")),
181+
formatx_ns: bench(|| formatx::formatx!("{:-^20}", "center").unwrap()),
182+
},
183+
];
184+
185+
let svg = generate_svg(&results);
186+
fs::create_dir_all("images").ok();
187+
fs::write("images/benchmark.svg", &svg).unwrap();
188+
}

images/benchmark.svg

Lines changed: 39 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)