Skip to content

Commit 8e671cc

Browse files
committed
Add benchmarking info
1 parent 753461e commit 8e671cc

File tree

3 files changed

+292
-0
lines changed

3 files changed

+292
-0
lines changed

BENCHMARK.rst

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
============
2+
Benchmarking
3+
============
4+
5+
As of Mar 2026, there are 3 alternative implementations of SonyFlake in
6+
Python:
7+
8+
* `hjpotter92/sonyflake-py <https://github.com/hjpotter92/sonyflake-py>`_
9+
* `iyad-f/sonyflake <https://github.com/iyad-f/sonyflake>`_
10+
* `pavelprokhorenko/snowflake-id-toolkit <https://github.com/pavelprokhorenko/snowflake-id-toolkit>`_
11+
12+
None of them has batch generation feature, neither they use extension modules
13+
for speed ups. So we'll be comparing against pure-Python, single-id generation
14+
mode in ``sonyflake-turbo``:
15+
16+
.. code-block:: python
17+
18+
sf = SonyFlake(*range(0xFFFF))
19+
asf = AsyncSonyFlake(sf)
20+
next(sf) # for sync benchmarks
21+
await asf # for async benchmarks
22+
23+
Since Sonyflake algorithm at its core has limited throughput, we'll be using
24+
as much machine ids as possible. For ``sonyflake-turbo`` it is as trivial
25+
as:
26+
27+
.. code-block:: python
28+
29+
sf = SonyFlake(*range(0xFFFF))
30+
31+
for other implementation we'll be using something equivalent to:
32+
33+
.. code-block:: python
34+
35+
sfs = [
36+
SonyFlake(machine_id=machine_id).next_id
37+
for machine_id in range(0xFFFF)
38+
]
39+
max_i = len(sfs)
40+
i = 0
41+
42+
def next_id() -> int:
43+
nonlocal i
44+
id_ = sfs[i]()
45+
i = (i + 1) % max_i
46+
return id_
47+
48+
49+
In order to shave-off some time for ``getattr``-ing of ``next_id`` we do it
50+
in initialization step. Same goes for ``len``-s. We're not using generators,
51+
since ``yield``-s are also tanking performance.
52+
53+
This setup is closest to "running multiple generators in parallel".
54+
55+
Unfortunately, both ``hjpotter92/sonyflake-py`` and ``iyad-f/sonyflake``
56+
share the same package name ``sonyflake``, so we cannot benchmark everything
57+
in one go. Results has to be merged later.
58+
59+
Full source code of benchmark is located in ``benchmark.py`` file in the root
60+
of the repo.
61+
62+
Naming breakdown:
63+
64+
* ``hjpotter92_sonyflake`` - ``hjpotter92/sonyflake-py``
65+
* ``iyad_f_sonyflake*`` - ``iyad-f/sonyflake``
66+
* ``snowflake_id_toolkit`` - ``pavelprokhorenko/snowflake-id-toolkit``
67+
* ``turbo_native*`` - running against ``sonyflake_turbo._sonyflake.SonyFlake``
68+
* ``turbo_pure*`` - running against ``sonyflake_turbo.pure.SonyFlake``
69+
* ``turbo_*_solo`` - solo mode (``next(sf)``, ``await asf``)
70+
* ``turbo_*_batch`` - batch mode (``sf(1000)``, ``await asf(1000)``)
71+
72+
Run:
73+
74+
.. code-block:: sh
75+
76+
python benchmark.py
77+
78+
Results
79+
=======
80+
81+
For CPython 3.12.3 on the Intel Xeon E3-1275, results are following (lower %
82+
= better):
83+
84+
.. csv-table:: Sync
85+
:header: "Name", "Time", "%"
86+
87+
turbo_native_batch,0.03s,3.03%
88+
turbo_native_solo,0.13s,13.13%
89+
turbo_pure_batch,0.35s,35.35%
90+
turbo_pure_solo,0.99s,100.00%
91+
hjpotter92_sonyflake,1.35s,136.36%
92+
iyad_f_sonyflake,2.48s,250.51%
93+
snowflake_id_toolkit,1.14s,115.15%
94+
95+
.. csv-table:: Async
96+
:header: "Name", "Time", "%"
97+
98+
turbo_native_batch_async,0.05s,0.41%
99+
turbo_native_solo_async,10.16s,83.07%
100+
turbo_pure_batch_async,0.31s,2.53%
101+
turbo_pure_solo_async,12.23s,100.00%
102+
iyad_f_sonyflake_async,14.07s,115.04%

README.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,11 @@ Otherwise you might want to use it for one of the following reasons:
247247
.. _UUIDv7: https://www.rfc-editor.org/rfc/rfc9562.html#name-uuid-version-7
248248
.. _IDOR: https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html
249249

250+
Benchmarking
251+
~~~~~~~~~~~~
252+
253+
See the `benchmarks <BENCHMARK.rst>`_ for performance details.
254+
250255
Development
251256
===========
252257

benchmark.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
#!/usr/bin/env python3
2+
import platform
3+
import sys
4+
from asyncio import new_event_loop, set_event_loop
5+
from contextlib import suppress
6+
from datetime import datetime, timezone
7+
from timeit import timeit
8+
9+
from sonyflake_turbo import AsyncSonyFlake
10+
11+
EPOCH = 1749081600
12+
EPOCH_DT = datetime.fromtimestamp(EPOCH, tz=timezone.utc)
13+
ONE_THOUSAND = 1000
14+
ONE_MILLION = ONE_THOUSAND * ONE_THOUSAND
15+
# Benchmarking a single id generator is not useful since it'll be bottlenecked
16+
# by SonyFlake's own throughtput. Use all possible machine ids to alleviate
17+
# stray sleeps.
18+
MACHINE_IDS = range(0xFFFF)
19+
# `asyncio.run()` creates new event loop on every invocation, which is no-go
20+
# for benchmarking. Reuse the same event loop for all benchmarks.
21+
loop = new_event_loop()
22+
set_event_loop(loop)
23+
24+
25+
def _turbo(name, sf):
26+
t_batch = timeit(lambda: sf(ONE_THOUSAND), number=ONE_THOUSAND)
27+
t_solo = timeit(lambda: next(sf), number=ONE_MILLION)
28+
29+
print(f"turbo_{name}_batch,{t_batch:.2f}")
30+
print(f"turbo_{name}_solo,{t_solo:.2f}")
31+
32+
33+
def _turbo_async(name, asf):
34+
def batch():
35+
loop.run_until_complete(asf(ONE_THOUSAND))
36+
37+
def solo():
38+
loop.run_until_complete(asf)
39+
40+
t_batch = timeit(batch, number=ONE_THOUSAND)
41+
t_solo = timeit(solo, number=ONE_MILLION)
42+
43+
print(f"turbo_{name}_batch_async,{t_batch:.2f}")
44+
print(f"turbo_{name}_solo_async,{t_solo:.2f}")
45+
46+
47+
def _benchmark(name, sfs, aio=False):
48+
max_i = len(sfs)
49+
i = 0
50+
51+
if aio:
52+
name += "_async"
53+
54+
def next_id() -> int:
55+
nonlocal i
56+
id_ = loop.run_until_complete(sfs[i]())
57+
i = (i + 1) % max_i
58+
return id_
59+
60+
else:
61+
62+
def next_id() -> int:
63+
nonlocal i
64+
id_ = sfs[i]()
65+
i = (i + 1) % max_i
66+
return id_
67+
68+
t = timeit(next_id, number=ONE_MILLION)
69+
70+
print(f"{name},{t:.2f}")
71+
72+
73+
def turbo_native():
74+
from sonyflake_turbo._sonyflake import SonyFlake
75+
76+
_turbo("native", SonyFlake(*MACHINE_IDS, start_time=EPOCH))
77+
78+
79+
def turbo_pure():
80+
from sonyflake_turbo.pure import SonyFlake
81+
82+
_turbo("pure", SonyFlake(*MACHINE_IDS, start_time=EPOCH))
83+
84+
85+
def turbo_async_native():
86+
from sonyflake_turbo._sonyflake import SonyFlake
87+
88+
asf = AsyncSonyFlake(SonyFlake(*MACHINE_IDS, start_time=EPOCH))
89+
90+
_turbo_async("native", asf)
91+
92+
93+
def turbo_async_pure():
94+
from sonyflake_turbo.pure import SonyFlake
95+
96+
asf = AsyncSonyFlake(SonyFlake(*MACHINE_IDS, start_time=EPOCH))
97+
98+
_turbo_async("pure", asf)
99+
100+
101+
def hjpotter92_sonyflake():
102+
"""pip install sonyflake-py"""
103+
104+
from sonyflake import SonyFlake
105+
106+
sfs = [
107+
SonyFlake(start_time=EPOCH_DT, machine_id=lambda: machine_id).next_id
108+
for machine_id in MACHINE_IDS
109+
]
110+
111+
_benchmark("hjpotter92_sonyflake", sfs)
112+
113+
114+
def iyad_f_sonyflake():
115+
"""pip install sonyflake"""
116+
117+
from sonyflake import Sonyflake
118+
119+
sfs = [
120+
Sonyflake(start_time=EPOCH_DT, machine_id=machine_id).next_id
121+
for machine_id in MACHINE_IDS
122+
]
123+
124+
_benchmark("iyad_f_sonyflake", sfs)
125+
126+
127+
def iyad_f_sonyflake_async():
128+
"""pip install sonyflake"""
129+
130+
from sonyflake import Sonyflake
131+
132+
sfs = [
133+
Sonyflake(start_time=EPOCH_DT, machine_id=machine_id).next_id_async
134+
for machine_id in MACHINE_IDS
135+
]
136+
137+
_benchmark("iyad_f_sonyflake", sfs, True)
138+
139+
140+
def snowflake_id_toolkit():
141+
from snowflake_id_toolkit.sony import SonyflakeIDGenerator
142+
143+
# https://github.com/pavelprokhorenko/snowflake-id-toolkit/issues/19
144+
object.__setattr__(SonyflakeIDGenerator._config, "node_id_bits", 16)
145+
object.__setattr__(SonyflakeIDGenerator._config, "sequence_bits", 8)
146+
147+
sfs = [
148+
SonyflakeIDGenerator(epoch=EPOCH, node_id=machine_id).generate_next_id
149+
for machine_id in MACHINE_IDS
150+
]
151+
152+
_benchmark("snowflake_id_toolkit", sfs)
153+
154+
155+
def main():
156+
print(platform.python_implementation(), platform.python_version(), file=sys.stderr)
157+
for f in [
158+
turbo_native,
159+
turbo_pure,
160+
turbo_async_native,
161+
turbo_async_pure,
162+
hjpotter92_sonyflake,
163+
iyad_f_sonyflake,
164+
iyad_f_sonyflake_async,
165+
snowflake_id_toolkit,
166+
]:
167+
with suppress(ImportError):
168+
f()
169+
print("done", file=sys.stderr)
170+
171+
172+
if __name__ == "__main__":
173+
main()
174+
# CPython 3.12.3
175+
# turbo_native_batch,0.03
176+
# turbo_native_solo,0.13
177+
# turbo_pure_batch,0.35
178+
# turbo_pure_solo,0.99
179+
# turbo_native_batch_async,0.05
180+
# turbo_native_solo_async,10.16
181+
# turbo_pure_batch_async,0.31
182+
# turbo_pure_solo_async,12.23
183+
# sonyflake_py,1.35
184+
# sonyflake_alt,2.48
185+
# sonyflake_alt_async,14.07

0 commit comments

Comments
 (0)