Skip to content

Commit 2f9d807

Browse files
authored
[mypyc] Add write_i16_le and read_i16_le to librt.strings (#20745)
These are experimental optimized primitives for reading/writing 16-bit binary integers. I'll later add support for other integer sizes and floats, and big endian formats. The main optimization here is to inline the hot paths of these functions. Also the api is optimized for performance and not really ergonomics, though I don't think the ergonomics are particularly bad. When reading, the caller needs to keep track of the index to the bytes object, and negative indexes aren't supported, but the latter doesn't seem really useful for these kinds of primitives. In a microbenchmark, these can be dozens of times faster than using stdlib `struct` (when compiled). It may still be possible to further optimize performance, but these seem already pretty good. I used coding agent assist but through small human reviewed increments.
1 parent 87f98db commit 2f9d807

File tree

9 files changed

+328
-13
lines changed

9 files changed

+328
-13
lines changed

mypy/typeshed/stubs/librt/librt/strings.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import final
22

3-
from mypy_extensions import i64, i32, u8
3+
from mypy_extensions import i64, i32, i16, u8
44

55
@final
66
class BytesWriter:
@@ -19,3 +19,6 @@ class StringWriter:
1919
def getvalue(self) -> str: ...
2020
def __len__(self) -> i64: ...
2121
def __getitem__(self, i: i64, /) -> i32: ...
22+
23+
def write_i16_le(b: BytesWriter, n: i16, /) -> None: ...
24+
def read_i16_le(b: bytes, index: i64, /) -> i16: ...

mypyc/lib-rt/byteswriter_extra_ops.c

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,15 @@ char CPyBytesWriter_Write(PyObject *obj, PyObject *value) {
3232
return CPY_NONE;
3333
}
3434

35+
int16_t CPyBytes_ReadError(int64_t index, Py_ssize_t size) {
36+
if (index < 0) {
37+
PyErr_SetString(PyExc_ValueError, "index must be non-negative");
38+
} else {
39+
PyErr_Format(PyExc_IndexError,
40+
"index %lld out of range for bytes of length %zd",
41+
(long long)index, size);
42+
}
43+
return CPY_LL_INT_ERROR;
44+
}
45+
3546
#endif // MYPYC_EXPERIMENTAL

mypyc/lib-rt/byteswriter_extra_ops.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <Python.h>
88

99
#include "strings/librt_strings.h"
10+
#include "strings/librt_strings_common.h"
1011

1112
static inline CPyTagged
1213
CPyBytesWriter_Len(PyObject *obj) {
@@ -34,8 +35,20 @@ CPyBytesWriter_Append(PyObject *obj, uint8_t value) {
3435
return CPY_NONE;
3536
}
3637

38+
static inline char
39+
CPyBytesWriter_WriteI16LE(PyObject *obj, int16_t value) {
40+
BytesWriterObject *self = (BytesWriterObject *)obj;
41+
if (!CPyBytesWriter_EnsureSize(self, 2))
42+
return CPY_NONE_ERROR;
43+
BytesWriter_write_i16_le_unchecked(self, value);
44+
return CPY_NONE;
45+
}
46+
3747
char CPyBytesWriter_Write(PyObject *obj, PyObject *value);
3848

49+
// Helper function for bytes read error handling (negative index or out of range)
50+
int16_t CPyBytes_ReadError(int64_t index, Py_ssize_t size);
51+
3952
// If index is negative, convert to non-negative index (no range checking)
4053
static inline int64_t CPyBytesWriter_AdjustIndex(PyObject *obj, int64_t index) {
4154
if (index < 0) {
@@ -56,6 +69,18 @@ static inline void CPyBytesWriter_SetItem(PyObject *obj, int64_t index, uint8_t
5669
(((BytesWriterObject *)obj)->buf)[index] = x;
5770
}
5871

72+
static inline int16_t
73+
CPyBytes_ReadI16LE(PyObject *bytes_obj, int64_t index) {
74+
// bytes_obj type is enforced by mypyc
75+
Py_ssize_t size = PyBytes_GET_SIZE(bytes_obj);
76+
if (unlikely(index < 0 || index > size - 2)) {
77+
CPyBytes_ReadError(index, size);
78+
return CPY_LL_INT_ERROR;
79+
}
80+
const unsigned char *data = (const unsigned char *)PyBytes_AS_STRING(bytes_obj);
81+
return read_i16_le_unchecked(data + index);
82+
}
83+
5984
#endif // MYPYC_EXPERIMENTAL
6085

6186
#endif

mypyc/lib-rt/strings/librt_strings.c

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,9 +831,76 @@ StringWriter_len_internal(PyObject *self) {
831831

832832
// End of StringWriter
833833

834+
static PyObject*
835+
write_i16_le(PyObject *module, PyObject *const *args, size_t nargs) {
836+
if (unlikely(nargs != 2)) {
837+
PyErr_Format(PyExc_TypeError,
838+
"write_i16_le() takes exactly 2 arguments (%zu given)", nargs);
839+
return NULL;
840+
}
841+
PyObject *writer = args[0];
842+
if (!check_bytes_writer(writer)) {
843+
return NULL;
844+
}
845+
PyObject *value = args[1];
846+
int16_t unboxed = CPyLong_AsInt16(value);
847+
if (unlikely(unboxed == CPY_LL_INT_ERROR && PyErr_Occurred())) {
848+
// Error already set by CPyLong_AsInt16 (ValueError for overflow, TypeError for wrong type)
849+
return NULL;
850+
}
851+
BytesWriterObject *bw = (BytesWriterObject *)writer;
852+
if (unlikely(!ensure_bytes_writer_size(bw, 2))) {
853+
return NULL;
854+
}
855+
BytesWriter_write_i16_le_unchecked(bw, unboxed);
856+
Py_INCREF(Py_None);
857+
return Py_None;
858+
}
859+
860+
static PyObject*
861+
read_i16_le(PyObject *module, PyObject *const *args, size_t nargs) {
862+
if (unlikely(nargs != 2)) {
863+
PyErr_Format(PyExc_TypeError,
864+
"read_i16_le() takes exactly 2 arguments (%zu given)", nargs);
865+
return NULL;
866+
}
867+
PyObject *bytes_obj = args[0];
868+
if (unlikely(!PyBytes_Check(bytes_obj))) {
869+
PyErr_SetString(PyExc_TypeError, "read_i16_le() argument 1 must be bytes");
870+
return NULL;
871+
}
872+
PyObject *index_obj = args[1];
873+
int64_t index = CPyLong_AsInt64(index_obj);
874+
if (unlikely(index == CPY_LL_INT_ERROR && PyErr_Occurred())) {
875+
return NULL;
876+
}
877+
if (unlikely(index < 0)) {
878+
PyErr_SetString(PyExc_ValueError, "index must be non-negative");
879+
return NULL;
880+
}
881+
Py_ssize_t size = PyBytes_GET_SIZE(bytes_obj);
882+
if (unlikely(index > size - 2)) {
883+
PyErr_Format(PyExc_IndexError,
884+
"index %lld out of range for bytes of length %zd",
885+
(long long)index, size);
886+
return NULL;
887+
}
888+
const unsigned char *data = (const unsigned char *)PyBytes_AS_STRING(bytes_obj);
889+
int16_t value = read_i16_le_unchecked(data + index);
890+
return PyLong_FromLong(value);
891+
}
892+
834893
#endif
835894

836895
static PyMethodDef librt_strings_module_methods[] = {
896+
#ifdef MYPYC_EXPERIMENTAL
897+
{"write_i16_le", (PyCFunction) write_i16_le, METH_FASTCALL,
898+
PyDoc_STR("Write a 16-bit signed integer to BytesWriter in little-endian format")
899+
},
900+
{"read_i16_le", (PyCFunction) read_i16_le, METH_FASTCALL,
901+
PyDoc_STR("Read a 16-bit signed integer from bytes in little-endian format")
902+
},
903+
#endif
837904
{NULL, NULL, 0, NULL}
838905
};
839906

mypyc/lib-rt/strings/librt_strings.h

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import_librt_strings(void)
1313
#else // MYPYC_EXPERIMENTAL
1414

1515
#include <Python.h>
16+
#include "librt_strings_common.h"
1617

1718
// ABI version -- only an exact match is compatible. This will only be changed in
1819
// very exceptional cases (likely never) due to strict backward compatibility
@@ -30,17 +31,6 @@ import_librt_strings(void)
3031

3132
static void *LibRTStrings_API[LIBRT_STRINGS_API_LEN];
3233

33-
// Length of the default buffer embedded directly in a BytesWriter object
34-
#define WRITER_EMBEDDED_BUF_LEN 256
35-
36-
typedef struct {
37-
PyObject_HEAD
38-
char *buf; // Beginning of the buffer
39-
Py_ssize_t len; // Current length (number of bytes written)
40-
Py_ssize_t capacity; // Total capacity of the buffer
41-
char data[WRITER_EMBEDDED_BUF_LEN]; // Default buffer
42-
} BytesWriterObject;
43-
4434
typedef struct {
4535
PyObject_HEAD
4636
char *buf; // Beginning of the buffer
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#ifndef LIBRT_STRINGS_COMMON_H
2+
#define LIBRT_STRINGS_COMMON_H
3+
4+
#include <Python.h>
5+
#include <stdint.h>
6+
#include <string.h>
7+
8+
// Length of the default buffer embedded directly in a BytesWriter object
9+
#define WRITER_EMBEDDED_BUF_LEN 256
10+
11+
typedef struct {
12+
PyObject_HEAD
13+
char *buf; // Beginning of the buffer
14+
Py_ssize_t len; // Current length (number of bytes written)
15+
Py_ssize_t capacity; // Total capacity of the buffer
16+
char data[WRITER_EMBEDDED_BUF_LEN]; // Default buffer
17+
} BytesWriterObject;
18+
19+
// Write a 16-bit signed integer in little-endian format to BytesWriter.
20+
// NOTE: This does NOT check buffer capacity - caller must ensure space is available.
21+
static inline void
22+
BytesWriter_write_i16_le_unchecked(BytesWriterObject *self, int16_t value) {
23+
// Store len in local to help optimizer reduce struct member accesses
24+
Py_ssize_t len = self->len;
25+
unsigned char *p = (unsigned char *)(self->buf + len);
26+
uint16_t uval = (uint16_t)value;
27+
28+
// Write in little-endian format
29+
// Modern compilers optimize this pattern well, often to a single store on LE systems
30+
p[0] = (unsigned char)uval;
31+
p[1] = (unsigned char)(uval >> 8);
32+
33+
self->len = len + 2;
34+
}
35+
36+
// Read a 16-bit signed integer in little-endian format from bytes.
37+
// NOTE: This does NOT check bounds - caller must ensure valid index.
38+
static inline int16_t
39+
read_i16_le_unchecked(const unsigned char *data) {
40+
// Read in little-endian format
41+
// Modern compilers optimize this pattern well, often to a single load on LE systems
42+
uint16_t uval = (uint16_t)data[0] | ((uint16_t)data[1] << 8);
43+
return (int16_t)uval;
44+
}
45+
46+
#endif // LIBRT_STRINGS_COMMON_H

mypyc/primitives/librt_strings_ops.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
bytearray_rprimitive,
66
bytes_rprimitive,
77
bytes_writer_rprimitive,
8+
int16_rprimitive,
89
int32_rprimitive,
910
int64_rprimitive,
1011
none_rprimitive,
@@ -76,6 +77,26 @@
7677
dependencies=[LIBRT_STRINGS],
7778
)
7879

80+
function_op(
81+
name="librt.strings.write_i16_le",
82+
arg_types=[bytes_writer_rprimitive, int16_rprimitive],
83+
return_type=none_rprimitive,
84+
c_function_name="CPyBytesWriter_WriteI16LE",
85+
error_kind=ERR_MAGIC,
86+
experimental=True,
87+
dependencies=[LIBRT_STRINGS, BYTES_WRITER_EXTRA_OPS],
88+
)
89+
90+
function_op(
91+
name="librt.strings.read_i16_le",
92+
arg_types=[bytes_rprimitive, int64_rprimitive],
93+
return_type=int16_rprimitive,
94+
c_function_name="CPyBytes_ReadI16LE",
95+
error_kind=ERR_MAGIC,
96+
experimental=True,
97+
dependencies=[LIBRT_STRINGS, BYTES_WRITER_EXTRA_OPS],
98+
)
99+
79100
function_op(
80101
name="builtins.len",
81102
arg_types=[bytes_writer_rprimitive],

mypyc/test-data/irbuild-librt-strings.test

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,52 @@ L2:
9494
CPyBytesWriter_SetItem(b, r0, x)
9595
return 1
9696

97+
[case testLibrtStrings_write_i16_le_experimental_64bit]
98+
from librt.strings import BytesWriter, write_i16_le
99+
from mypy_extensions import i16
100+
101+
def test_write_i16_le(b: BytesWriter, n: i16) -> None:
102+
write_i16_le(b, n)
103+
def test_write_i16_le_literal(b: BytesWriter) -> None:
104+
write_i16_le(b, 1234)
105+
[out]
106+
def test_write_i16_le(b, n):
107+
b :: librt.strings.BytesWriter
108+
n :: i16
109+
r0 :: None
110+
L0:
111+
r0 = CPyBytesWriter_WriteI16LE(b, n)
112+
return 1
113+
def test_write_i16_le_literal(b):
114+
b :: librt.strings.BytesWriter
115+
r0 :: None
116+
L0:
117+
r0 = CPyBytesWriter_WriteI16LE(b, 1234)
118+
return 1
119+
120+
[case testLibrtStrings_read_i16_le_experimental_64bit]
121+
from librt.strings import read_i16_le
122+
from mypy_extensions import i16, i64
123+
124+
def test_read_i16_le(b: bytes, i: i64) -> i16:
125+
return read_i16_le(b, i)
126+
def test_read_i16_le_literal(b: bytes) -> i16:
127+
return read_i16_le(b, 0)
128+
[out]
129+
def test_read_i16_le(b, i):
130+
b :: bytes
131+
i :: i64
132+
r0 :: i16
133+
L0:
134+
r0 = CPyBytes_ReadI16LE(b, i)
135+
return r0
136+
def test_read_i16_le_literal(b):
137+
b :: bytes
138+
r0 :: i16
139+
L0:
140+
r0 = CPyBytes_ReadI16LE(b, 0)
141+
return r0
142+
97143
[case testLibrtStrings_StringWriter_experimental_64bit]
98144
from librt.strings import StringWriter
99145
from mypy_extensions import i32, i64

0 commit comments

Comments
 (0)