Skip to content

Commit 4b8fdca

Browse files
committed
[mypyc] Add librt.random module (#21433)
The stdlib `random` module is fairly often used in performance critical code, and it's not super efficient. Add `librt.random` with a subset of the stdlib module interface that is optimized for performance when compiled. Use ChaCha8 as the algorithm. Based on some research, this is a modern, high-quality PRNG algorithm. It's used by Go `math/rand/v2`, among others. This is a non-cryptographic PRNG, similar to the stdlib `random` module (but this uses a different algorithm). I used Claude Code and Codex to write all the code, but I iterated on it quite a lot and did a bunch of manual validation and code review. I also asked Codex to explicitly check that the ChaCha8 implementation is correct by comparing it to a reference implementation. Use thread-local RNG state for module-level functions to enable good scaling in free-threaded builds. There's some extra complexity from having to free the state at thread exit. I asked a LLM to generate and run a benchmark, and here are the results on 3.14: ``` │ Function │ vs stdlib(compiled) │ vs stdlib(interpreted) │ │ random() │ 3.2x │ 4.8x │ │ randint() │ 16.6x │ 18.0x │ │ randrange() │ 14.5x │ 16.2x │ │ choice() │ 12.9x │ 10.3x │ ``` `choice()` was replaced with `randrange` when using `librt`, since we don't provide it as part of this fairly minimal API.
1 parent 675b6bf commit 4b8fdca

16 files changed

Lines changed: 1470 additions & 1 deletion

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import final, overload
2+
3+
from mypy_extensions import i64
4+
5+
def random() -> float: ...
6+
def randint(a: i64, b: i64) -> i64: ...
7+
@overload
8+
def randrange(stop: i64, /) -> i64: ...
9+
@overload
10+
def randrange(start: i64, stop: i64, /) -> i64: ...
11+
def seed(n: i64, /) -> None: ...
12+
13+
@final
14+
class Random:
15+
def __init__(self, seed: i64 | None = None) -> None: ...
16+
def randint(self, a: i64, b: i64) -> i64: ...
17+
@overload
18+
def randrange(self, stop: i64, /) -> i64: ...
19+
@overload
20+
def randrange(self, start: i64, stop: i64, /) -> i64: ...
21+
def random(self) -> float: ...
22+
def seed(self, n: i64, /) -> None: ...

mypyc/build.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class ModDesc(NamedTuple):
121121
["vecs"],
122122
),
123123
ModDesc("librt.time", ["time/librt_time.c"], ["time/librt_time.h"], []),
124+
ModDesc("librt.random", ["random/librt_random.c"], ["random/librt_random.h"], ["random"]),
124125
]
125126

126127
try:
@@ -631,6 +632,9 @@ def get_cflags(
631632
# Disables C Preprocessor (cpp) warnings
632633
# See https://github.com/mypyc/mypyc/issues/956
633634
"-Wno-cpp",
635+
"-Wno-array-bounds",
636+
"-Wno-stringop-overread",
637+
"-Wno-stringop-overflow",
634638
]
635639
if log_trace:
636640
cflags.append("-DMYPYC_LOG_TRACE")

mypyc/codegen/emitmodule.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
from mypyc.errors import Errors
6060
from mypyc.ir.deps import (
6161
LIBRT_BASE64,
62+
LIBRT_RANDOM,
6263
LIBRT_STRINGS,
6364
LIBRT_TIME,
6465
LIBRT_VECS,
@@ -1224,6 +1225,10 @@ def emit_module_exec_func(
12241225
emitter.emit_line("if (import_librt_vecs() < 0) {")
12251226
emitter.emit_line("return -1;")
12261227
emitter.emit_line("}")
1228+
if LIBRT_RANDOM in module.dependencies:
1229+
emitter.emit_line("if (import_librt_random() < 0) {")
1230+
emitter.emit_line("return -1;")
1231+
emitter.emit_line("}")
12271232
emitter.emit_line("PyObject* modname = NULL;")
12281233
if self.multi_phase_init:
12291234
emitter.emit_line(f"{module_static} = module;")

mypyc/ir/deps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def get_header(self) -> str:
109109
LIBRT_BASE64: Final = Capsule("librt.base64")
110110
LIBRT_VECS: Final = Capsule("librt.vecs")
111111
LIBRT_TIME: Final = Capsule("librt.time")
112+
LIBRT_RANDOM: Final = Capsule("librt.random")
112113

113114
BYTES_EXTRA_OPS: Final = SourceDep("bytes_extra_ops.c")
114115
BYTES_WRITER_EXTRA_OPS: Final = SourceDep("byteswriter_extra_ops.c")

mypyc/ir/rtypes.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class to enable the new behavior. In rare cases, adding a new
4141
from typing import TYPE_CHECKING, ClassVar, Final, Generic, TypeGuard, TypeVar, Union, final
4242

4343
from mypyc.common import HAVE_IMMORTAL, IS_32_BIT_PLATFORM, PLATFORM_SIZE, JsonDict, short_name
44-
from mypyc.ir.deps import LIBRT_STRINGS, LIBRT_VECS, Dependency
44+
from mypyc.ir.deps import LIBRT_RANDOM, LIBRT_STRINGS, LIBRT_VECS, Dependency
4545
from mypyc.namegen import NameGenerator
4646

4747
if TYPE_CHECKING:
@@ -544,10 +544,15 @@ def __hash__(self) -> int:
544544
("librt.strings.BytesWriter", (LIBRT_STRINGS,)),
545545
("librt.strings.StringWriter", (LIBRT_STRINGS,)),
546546
]
547+
} | {
548+
"librt.random.Random": RPrimitive(
549+
"librt.random.Random", is_unboxed=False, is_refcounted=True, dependencies=(LIBRT_RANDOM,)
550+
)
547551
}
548552

549553
bytes_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.BytesWriter"]
550554
string_writer_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.strings.StringWriter"]
555+
random_rprimitive: Final = KNOWN_NATIVE_TYPES["librt.random.Random"]
551556

552557

553558
def is_native_rprimitive(rtype: RType) -> bool:

0 commit comments

Comments
 (0)