Skip to content

Commit 33af604

Browse files
committed
[aiohttp] - use lcg as random generator
1 parent 568f4f8 commit 33af604

File tree

6 files changed

+146
-16
lines changed

6 files changed

+146
-16
lines changed

frameworks/Python/aiohttp/aiohttp.dockerfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ ADD ./ /aiohttp
44

55
WORKDIR /aiohttp
66

7-
RUN pip3 install -r /aiohttp/requirements-cpython.txt
7+
RUN pip3 install -r /aiohttp/requirements-cpython.txt && \
8+
pip3 install cython==3.1.2 setuptools==80.9.0 && \
9+
python3 setup.py build_ext --inplace
810

911
ENV CONNECTION=RAW
1012

frameworks/Python/aiohttp/app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def pg_dsn(dialect=None) -> str:
3939
url = URL.create(
4040
database='hello_world',
4141
password=os.getenv('PGPASS', 'benchmarkdbpass'),
42-
host='tfb-database',
42+
host=os.getenv('PGHOST','tfb-database'),
4343
port='5432',
4444
username=os.getenv('PGUSER', 'benchmarkdbuser'),
4545
drivername='postgresql+{}'.format(dialect) if dialect else 'postgresql',
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from random import randint
2+
3+
# Constants
4+
# https://statmath.wu.ac.at/software/src/prng-3.0.2/doc/prng.html/Table_LCG.html
5+
MAX_VALUE = 10000
6+
MODULOS = 134217689
7+
LCG_MULTIPLIER = 3162696
8+
RANDOM_THRESHOLD = MODULOS - (MODULOS % (MAX_VALUE))
9+
10+
seed = randint(0, MAX_VALUE)
11+
12+
def random_id():
13+
"""Generate a pseudo-random number based on a linear congruential generator"""
14+
global seed
15+
16+
# Regenerate if we're above the threshold to avoid distribution bias
17+
while True:
18+
seed = (LCG_MULTIPLIER * seed) % MODULOS
19+
if seed < RANDOM_THRESHOLD:
20+
break
21+
22+
return 1 + seed % MAX_VALUE
23+
24+
def random_unique_ids(n):
25+
"""Generate n unique random IDs"""
26+
generated_ids = {random_id() for _ in range(n)}
27+
28+
while len(generated_ids) < n:
29+
id_ = random_id()
30+
if id_ not in generated_ids:
31+
generated_ids.add(id_)
32+
33+
return list(generated_ids)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# cython: boundscheck=False
2+
# cython: wraparound=False
3+
# cython: initializedcheck=False
4+
5+
from libc.stdlib cimport rand, srand, calloc, free
6+
from libc.time cimport time
7+
8+
import cython
9+
10+
# Constants
11+
cdef unsigned int MAX_VALUE = 10000
12+
# https://statmath.wu.ac.at/software/src/prng-3.0.2/doc/prng.html/Table_LCG.html
13+
cdef unsigned int MODULOS = 134217689
14+
cdef unsigned int RANDOM_THRESHOLD = MODULOS - (MODULOS % MAX_VALUE)
15+
cdef unsigned int LCG_MULTIPLIER = 3162696
16+
cdef bint* seen = <bint*>calloc(MAX_VALUE + 1, sizeof(bint)) # Bit array simulation
17+
18+
cdef unsigned long seed = 0
19+
20+
cdef void _init_seed():
21+
global seed
22+
srand(<unsigned long>time(NULL))
23+
seed = rand() % MAX_VALUE
24+
25+
if seed == 0:
26+
seed = 1 # Ensure seed is never zero to avoid low number cycle
27+
28+
_init_seed()
29+
30+
@cython.boundscheck(False)
31+
@cython.wraparound(False)
32+
cdef inline unsigned int _next_random() noexcept nogil:
33+
"""Generate a pseudo-random number based on a linear congruential generator"""
34+
global seed
35+
36+
cdef unsigned long next_val = (<unsigned long>LCG_MULTIPLIER * seed) % MODULOS
37+
38+
while next_val >= RANDOM_THRESHOLD:
39+
next_val = (next_val * LCG_MULTIPLIER) % MODULOS
40+
41+
seed = next_val
42+
return seed
43+
44+
@cython.boundscheck(False)
45+
@cython.wraparound(False)
46+
cpdef unsigned int random_id() noexcept nogil:
47+
"""Generate a pseudo-random number in range [1, MAX_VALUE]"""
48+
return 1 + (_next_random() % MAX_VALUE)
49+
50+
@cython.boundscheck(False)
51+
@cython.wraparound(False)
52+
cpdef list[unsigned int] random_unique_ids(int n):
53+
"""Generate n unique random IDs in range[1, 10001]"""
54+
cdef list[int] result = [0] * n
55+
cdef int candidate, count = 0
56+
57+
try:
58+
while count < n:
59+
candidate = random_id()
60+
61+
if seen[candidate] == 0: # Not seen before
62+
seen[candidate] = 1
63+
result[count] = candidate
64+
count += 1
65+
finally:
66+
for i in result:
67+
seen[i] = 0
68+
69+
return result
70+
71+
def _cleanup():
72+
global seen
73+
if seen != NULL:
74+
free(seen)
75+
seen = NULL
76+
77+
import atexit
78+
atexit.register(_cleanup)

frameworks/Python/aiohttp/app/views.py

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import platform
22
from operator import attrgetter, itemgetter
33
from pathlib import Path
4-
from random import randint, sample
54

65
import jinja2
76
from aiohttp.web import Response
87
from sqlalchemy import bindparam, select
98
from sqlalchemy.orm.attributes import flag_modified
109

1110
from .models import Fortune, World
11+
from .random_utils import random_id, random_unique_ids
1212

1313
if platform.python_implementation() == "PyPy":
1414
from aiohttp.web import json_response
@@ -23,6 +23,7 @@ def json_response(payload):
2323

2424
ADDITIONAL_FORTUNE_ORM = Fortune(id=0, message='Additional fortune added at request time.')
2525
ADDITIONAL_FORTUNE_ROW = {'id': 0, 'message': 'Additional fortune added at request time.'}
26+
JSON_MESSAGE = {'message': 'Hello, World!'}
2627
READ_ROW_SQL = 'SELECT "randomnumber", "id" FROM "world" WHERE id = $1'
2728
READ_SELECT_ORM = select(World.randomnumber).where(World.id == bindparam("id"))
2829
READ_FORTUNES_ORM = select(Fortune.id, Fortune.message)
@@ -34,6 +35,7 @@ def json_response(payload):
3435
sort_fortunes_orm = attrgetter('message')
3536
sort_fortunes_raw = itemgetter('message')
3637

38+
db_query_id_params = [(i,) for i in range(0, 10002)]
3739

3840
def get_num_queries(request):
3941
try:
@@ -51,14 +53,14 @@ async def json(request):
5153
"""
5254
Test 1
5355
"""
54-
return json_response({'message': 'Hello, World!'})
56+
return json_response(JSON_MESSAGE)
5557

5658

5759
async def single_database_query_orm(request):
5860
"""
5961
Test 2 ORM
6062
"""
61-
id_ = randint(1, 10000)
63+
id_ = random_id()
6264
async with request.app['db_session']() as sess:
6365
num = await sess.scalar(READ_SELECT_ORM, {"id": id_})
6466
return json_response({'id': id_, 'randomNumber': num})
@@ -68,7 +70,7 @@ async def single_database_query_raw(request):
6870
"""
6971
Test 2 RAW
7072
"""
71-
id_ = randint(1, 10000)
73+
id_ = random_id()
7274

7375
async with request.app['pg'].acquire() as conn:
7476
r = await conn.fetchval(READ_ROW_SQL, id_)
@@ -81,7 +83,7 @@ async def multiple_database_queries_orm(request):
8183
"""
8284
num_queries = get_num_queries(request)
8385

84-
ids = [randint(1, 10000) for _ in range(num_queries)]
86+
ids = random_unique_ids(num_queries)
8587

8688
result = []
8789
async with request.app['db_session']() as sess:
@@ -96,12 +98,13 @@ async def multiple_database_queries_raw(request):
9698
Test 3 RAW
9799
"""
98100
num_queries = get_num_queries(request)
99-
100-
ids = [(randint(1, 10000), ) for _ in range(num_queries)]
101+
ids = random_unique_ids(num_queries)
102+
id_params = [db_query_id_params[id_] for id_ in ids]
101103

102104
async with request.app['pg'].acquire() as conn:
103-
rows = await conn.fetchmany(READ_ROW_SQL, ids)
104-
result = [{'id': id_[0], 'randomNumber': row[0]} for id_, row in zip(ids, rows)]
105+
rows = await conn.fetchmany(READ_ROW_SQL, id_params)
106+
107+
result = [{'id': id_, 'randomNumber': row[0]} for id_, row in zip(ids, rows)]
105108
return json_response(result)
106109

107110

@@ -135,9 +138,10 @@ async def updates(request):
135138
Test 5 ORM
136139
"""
137140
num_queries = get_num_queries(request)
138-
update_ids = sample(range(1, 10001), num_queries)
141+
update_ids = random_unique_ids(num_queries)
139142
update_ids.sort()
140-
updates = tuple(zip(update_ids, sample(range(1, 10001), num_queries)))
143+
144+
updates = [*zip(update_ids, random_unique_ids(num_queries))]
141145
worlds = [{'id': row_id, 'randomNumber': number} for row_id, number in updates]
142146

143147
async with request.app['db_session'].begin() as sess:
@@ -154,13 +158,14 @@ async def updates_raw(request):
154158
Test 5 RAW
155159
"""
156160
num_queries = get_num_queries(request)
157-
update_ids = sample(range(1, 10001), num_queries)
161+
update_ids = random_unique_ids(num_queries)
158162
update_ids.sort()
159163

160-
numbers = sample(range(1, 10001), num_queries)
161-
fetch_params = [(i,) for i in update_ids]
164+
numbers = random_unique_ids(num_queries)
162165
row_updates = [*zip(update_ids, numbers)]
163166

167+
fetch_params = [db_query_id_params[i] for i in update_ids]
168+
164169
async with request.app['pg'].acquire() as conn:
165170
# the result of this is the int previous random number which we don't actually use
166171
await conn.executemany(READ_ROW_SQL, fetch_params)

frameworks/Python/aiohttp/setup.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from setuptools import setup, Extension
2+
from Cython.Build import cythonize
3+
4+
setup(
5+
ext_modules=cythonize([
6+
Extension(
7+
"app.random_utils",
8+
["app/random_utils.pyx"],
9+
extra_compile_args=["-O3"]
10+
),
11+
])
12+
)

0 commit comments

Comments
 (0)