Skip to content

[aiohttp] - use lcg as random generator #9942

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion frameworks/Python/aiohttp/aiohttp.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ ADD ./ /aiohttp

WORKDIR /aiohttp

RUN pip3 install -r /aiohttp/requirements-cpython.txt
RUN pip3 install -r /aiohttp/requirements-cpython.txt && \
pip3 install cython==3.1.2 setuptools==80.9.0 && \
Copy link
Contributor

@Dreamsorcerer Dreamsorcerer Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do use this, we atleast want dependencies in the requirements files, so they can be updated with Dependabot.

python3 setup.py build_ext --inplace

ENV CONNECTION=RAW

Expand Down
2 changes: 1 addition & 1 deletion frameworks/Python/aiohttp/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def pg_dsn(dialect=None) -> str:
url = URL.create(
database='hello_world',
password=os.getenv('PGPASS', 'benchmarkdbpass'),
host='tfb-database',
host=os.getenv('PGHOST','tfb-database'),
port='5432',
username=os.getenv('PGUSER', 'benchmarkdbuser'),
drivername='postgresql+{}'.format(dialect) if dialect else 'postgresql',
Expand Down
33 changes: 33 additions & 0 deletions frameworks/Python/aiohttp/app/random_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from random import randint

# Constants
# https://statmath.wu.ac.at/software/src/prng-3.0.2/doc/prng.html/Table_LCG.html
MAX_VALUE = 10000
MODULOS = 134217689
LCG_MULTIPLIER = 3162696
RANDOM_THRESHOLD = MODULOS - (MODULOS % (MAX_VALUE))

seed = randint(0, MAX_VALUE)

def random_id():
"""Generate a pseudo-random number based on a linear congruential generator"""
global seed

# Regenerate if we're above the threshold to avoid distribution bias
while True:
seed = (LCG_MULTIPLIER * seed) % MODULOS
if seed < RANDOM_THRESHOLD:
break

return 1 + seed % MAX_VALUE

def random_unique_ids(n):
"""Generate n unique random IDs"""
generated_ids = {random_id() for _ in range(n)}

while len(generated_ids) < n:
id_ = random_id()
if id_ not in generated_ids:
generated_ids.add(id_)

return list(generated_ids)
63 changes: 63 additions & 0 deletions frameworks/Python/aiohttp/app/random_utils.pyx
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of feels like cheating, this seems beyond benchmarking a realistic framework.

This feels more like a potential contribution to cpython to provide faster built-in random functions, thus benefiting everyone.

Copy link
Contributor Author

@Reskov Reskov Jun 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of feels like cheating, this seems beyond benchmarking a realistic framework.

Agreed, but like many other things, module-level caching, execute queries without read actual results.

This looks more like a potential contribution to cpython to provide faster built-in random functions, which would benefit everyone.

This is certainly not intended as a contribution to Python itself, since LCGs are inferior to existing random number generators. They have significantly shorter periods, and their speed advantage comes only from the small space size and the simplicity of the algorithm. In our case, the short period and predictability of the sequence don’t matter—we’re not aiming for cryptographic or truly random values—but overall, LCGs are still a poor choice.

We are going back to the roots here 😀 Even Python 1.0 had improved version of LCG
https://github.com/nagayev/old-python/blob/a93727bb9eb40818ecaafb50e1e942ab75b3b6d3/Lib/whrandom.py#L73

Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# cython: boundscheck=False
# cython: wraparound=False
# cython: initializedcheck=False
# cython: cdivision=True

from libc.stdlib cimport calloc, free

import random

# Constants
cdef const unsigned short MAX_VALUE = 10000
# https://statmath.wu.ac.at/software/src/prng-3.0.2/doc/prng.html/Table_LCG.html
cdef const unsigned int MODULOS = 134217689
cdef const unsigned long LCG_MULTIPLIER = 3162696

cdef unsigned int RANDOM_THRESHOLD = MODULOS - (MODULOS % MAX_VALUE)
cdef char* seen = <char*>calloc(MAX_VALUE + 1, sizeof(char)) # Bit array simulation

cdef unsigned int seed = random.randint(1, MODULOS-1)

cdef inline unsigned int _next_random() noexcept nogil:
"""Generate a pseudo-random number based on a linear congruential generator"""
global seed

cdef unsigned int next_val = (LCG_MULTIPLIER * seed) % MODULOS

while next_val >= RANDOM_THRESHOLD:
next_val = (LCG_MULTIPLIER * next_val) % MODULOS

seed = next_val
return seed

cpdef unsigned short random_id() noexcept nogil:
"""Generate a pseudo-random number in range [1, MAX_VALUE]"""
return 1 + (_next_random() % MAX_VALUE)

cpdef list[unsigned short] random_unique_ids(unsigned short n):
"""Generate n unique random IDs in range[1, 10001]"""
cdef list[unsigned short] result = [0] * n
cdef unsigned short candidate, count = 0

try:
while count < n:
candidate = 1 + (_next_random() % MAX_VALUE)

if seen[candidate] == 0: # Not seen before
seen[candidate] = 1
result[count] = candidate
count += 1
finally:
for i in result:
seen[i] = 0

return result

def _cleanup():
global seen
if seen != NULL:
free(seen)
seen = NULL

import atexit
atexit.register(_cleanup)
33 changes: 19 additions & 14 deletions frameworks/Python/aiohttp/app/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import platform
from operator import attrgetter, itemgetter
from pathlib import Path
from random import randint, sample

import jinja2
from aiohttp.web import Response
from sqlalchemy import bindparam, select
from sqlalchemy.orm.attributes import flag_modified

from .models import Fortune, World
from .random_utils import random_id, random_unique_ids

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

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

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

def get_num_queries(request):
try:
Expand All @@ -51,14 +53,14 @@ async def json(request):
"""
Test 1
"""
return json_response({'message': 'Hello, World!'})
return json_response(JSON_MESSAGE)


async def single_database_query_orm(request):
"""
Test 2 ORM
"""
id_ = randint(1, 10000)
id_ = random_id()
async with request.app['db_session']() as sess:
num = await sess.scalar(READ_SELECT_ORM, {"id": id_})
return json_response({'id': id_, 'randomNumber': num})
Expand All @@ -68,7 +70,7 @@ async def single_database_query_raw(request):
"""
Test 2 RAW
"""
id_ = randint(1, 10000)
id_ = random_id()

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

ids = [randint(1, 10000) for _ in range(num_queries)]
ids = random_unique_ids(num_queries)

result = []
async with request.app['db_session']() as sess:
Expand All @@ -96,12 +98,13 @@ async def multiple_database_queries_raw(request):
Test 3 RAW
"""
num_queries = get_num_queries(request)

ids = [(randint(1, 10000), ) for _ in range(num_queries)]
ids = random_unique_ids(num_queries)
id_params = [db_query_id_params[id_] for id_ in ids]

async with request.app['pg'].acquire() as conn:
rows = await conn.fetchmany(READ_ROW_SQL, ids)
result = [{'id': id_[0], 'randomNumber': row[0]} for id_, row in zip(ids, rows)]
rows = await conn.fetchmany(READ_ROW_SQL, id_params)

result = [{'id': id_, 'randomNumber': row[0]} for id_, row in zip(ids, rows)]
return json_response(result)


Expand Down Expand Up @@ -135,9 +138,10 @@ async def updates(request):
Test 5 ORM
"""
num_queries = get_num_queries(request)
update_ids = sample(range(1, 10001), num_queries)
update_ids = random_unique_ids(num_queries)
update_ids.sort()
updates = tuple(zip(update_ids, sample(range(1, 10001), num_queries)))

updates = [*zip(update_ids, random_unique_ids(num_queries))]
worlds = [{'id': row_id, 'randomNumber': number} for row_id, number in updates]

async with request.app['db_session'].begin() as sess:
Expand All @@ -154,13 +158,14 @@ async def updates_raw(request):
Test 5 RAW
"""
num_queries = get_num_queries(request)
update_ids = sample(range(1, 10001), num_queries)
update_ids = random_unique_ids(num_queries)
update_ids.sort()

numbers = sample(range(1, 10001), num_queries)
fetch_params = [(i,) for i in update_ids]
numbers = random_unique_ids(num_queries)
row_updates = [*zip(update_ids, numbers)]

fetch_params = [db_query_id_params[i] for i in update_ids]

async with request.app['pg'].acquire() as conn:
# the result of this is the int previous random number which we don't actually use
await conn.executemany(READ_ROW_SQL, fetch_params)
Expand Down
12 changes: 12 additions & 0 deletions frameworks/Python/aiohttp/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from setuptools import setup, Extension
from Cython.Build import cythonize

setup(
ext_modules=cythonize([
Extension(
"app.random_utils",
["app/random_utils.pyx"],
extra_compile_args=["-O3"]
),
])
)
Loading