forked from chowdhurya/rust-unidecode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspeed_benchmark.py
More file actions
179 lines (148 loc) · 6.4 KB
/
Copy pathspeed_benchmark.py
File metadata and controls
179 lines (148 loc) · 6.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
from __future__ import annotations
import csv
import random
import time
from functools import lru_cache
from pathlib import Path
from statistics import mean, stdev
from typing import Any, Callable
from fast_unidecode import unidecode as fast_unidecode
from unidecode import unidecode
# It's a good practice to use a cache to simulate real-world usage
# where the same strings might be processed multiple times.
@lru_cache(maxsize=8)
def py_unidecode(text: str) -> str:
"""Cached version of the original Python unidecode."""
return unidecode(text)
@lru_cache(maxsize=8)
def rust_unidecode(text: str) -> str:
"""Cached version of the fast_unidecode (Rust implementation)."""
return fast_unidecode(text)
def run_benchmark(
func: Callable[[str], str], text: str, iterations: int
) -> list[float]:
"""
Benchmark a given unidecode function.
Args:
func: The unidecode function to benchmark.
text: The string to process.
iterations: The number of times to run the benchmark.
Returns:
A list of execution times in seconds.
"""
timings = []
for _ in range(iterations):
# Shuffle the string to prevent lower-level caching and ensure the
# function's performance is tested on varied (but same-character) inputs.
shuffled_text = "".join(random.sample(text, len(text)))
start_time = time.perf_counter()
func(shuffled_text)
end_time = time.perf_counter()
timings.append(end_time - start_time)
return timings
def get_mocked_data() -> dict[str, str]:
"""Provides a dictionary of strings for benchmarking."""
return {
"a": "看看谁的脚丫子更大,就踩上去",
"b": "回去以后",
"c": 14 * "回去以后",
"num": "100,23",
"cur": "100,23 CZK",
"d": " げんまい茶 ᔕᓇᓇ",
"e": "csonbhxehj",
"f": "KNFOTVDFLQ",
"g": "kRAaicQQMzSFtOkZPeyUumzAJoRmjDXJ",
"h": 5 * "299mm73d28rg6x7m8qe",
"i": "8LhNr31RxtmUrtWponbl",
"j": 100 * "8LhNr31RxtmUrtWponbl",
"k": "}<[,[{<#[/=-'@-%(*&~",
"l": (
int(5e4)
* "Æneid 1234 098 @#$!@)() -0/'ˇmkdaslllsmdlamdlkas げんまい茶 ᔕᓇᓇ 北亰 étude 四千年前有一个姑娘叫姜嫄,她有一天觉得很空虚,就到郊外玩,看见一只巨人脚印,也许是外星人留下的,她想上去比一比,看看谁的脚丫子更大,就踩上去。踩上去就发现肚子里乱动,跟怀了孕似的。回去以后,肚子里的小孩,又老不出来,过了十二个月才生下来。"
),
"m": ".H<ncWi&dY_Wf)`'bNR=P@)G\8EkVEdmTZdMVO]gM2v m!",
"n": 14 * "aDc4__uu3I_jq/68=YK(=Z'/3u5@{cu5_6{ v]U9q nZ_#X&ZbXBv~tFmb@p}Z",
}
def main() -> None:
"""Main function to run the benchmark and save results."""
test_data = get_mocked_data()
all_results: list[dict[str, Any]] = []
iteration_counts = [1, 5, 10, 20, 100, 500]
num_rounds = 10
print("Starting benchmark...")
for iterations in iteration_counts:
for round_num in range(num_rounds):
print(
f"\n--- Running: {iterations} iterations, "
f"round {round_num + 1}/{num_rounds} ---"
)
round_results: dict[str, Any] = {
"iterations": iterations,
"round": round_num + 1,
}
for key, text in test_data.items():
print(f"Benchmarking case: '{key}'")
# Adjust iterations for the very long string 'l'
current_iters = min(3, iterations) if key == "l" else iterations
if current_iters == 0:
print("Skipping due to 0 iterations.")
round_results[key] = 1.0
continue
py_timings = run_benchmark(py_unidecode, text, current_iters)
rust_timings = run_benchmark(rust_unidecode, text, current_iters)
py_mean = mean(py_timings)
py_std = stdev(py_timings) if len(py_timings) > 1 else 0
rust_mean = mean(rust_timings)
rust_std = stdev(rust_timings) if len(rust_timings) > 1 else 0
print(f" Python: {py_mean * 1000:.3f} ± {py_std * 1000:.3f} ms")
print(f" Rust: {rust_mean * 1000:.3f} ± {rust_std * 1000:.3f} ms")
if rust_mean > 0:
speedup = py_mean / rust_mean
if speedup > 1:
print(f" Speedup: {speedup:.2f}x")
else:
print(f" Slowdown: {1 / speedup:.2f}x")
else:
speedup = float("inf") if py_mean > 0 else 1.0
print(
" Rust version was instantaneous, speedup is effectively infinite."
)
round_results[key] = round(speedup, 4)
print("-" * 20)
py_unidecode.cache_clear()
rust_unidecode.cache_clear()
all_results.append(round_results)
# --- Save results to CSV ---
output_file = Path("result.tsv")
fieldnames = ["iterations", "round"] + list(test_data.keys())
with open(output_file, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter="\t")
writer.writeheader()
writer.writerows(all_results)
print(f"\nBenchmark results saved to {output_file}")
# --- Final Summary ---
total_comparisons = 0
rust_was_faster = 0
total_speedup = 0.0
for res in all_results:
for key in test_data:
speedup = res.get(key, 1.0)
if speedup != 1.0: # Exclude cases with no difference
total_comparisons += 1
if speedup > 1.0:
rust_was_faster += 1
total_speedup += speedup
if total_comparisons > 0:
outperformance_pct = 100 * rust_was_faster / total_comparisons
average_speedup = total_speedup / total_comparisons
print("\n--- Summary ---")
print(
f"Rust implementation was faster in {outperformance_pct:.2f}% "
"of comparisons."
)
print(f"Average speedup across all comparisons: {average_speedup:.2f}x.")
else:
print("\n--- Summary ---")
print("No conclusive performance difference observed.")
if __name__ == "__main__":
main()