Skip to content

Commit 9bcb18e

Browse files
authored
[mypyc] Speed up BytesWriter append, write and len operations (#20380)
These made two microbenchmarks 1.3x to 4.4x faster. Make it possible to inline some functions, and support a faster calling convention by linking the C function for `write` statically. This means that we can't change the internal representation of BytesWriter without breaking backward compatibility after we make BytesWriter non-experimental.
1 parent b0abcac commit 9bcb18e

File tree

9 files changed

+103
-33
lines changed

9 files changed

+103
-33
lines changed

mypyc/ir/deps.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ def get_header(self) -> str:
5050
LIBRT_BASE64: Final = Capsule("librt.base64")
5151

5252
BYTES_EXTRA_OPS: Final = SourceDep("bytes_extra_ops.c")
53+
BYTES_WRITER_EXTRA_OPS: Final = SourceDep("byteswriter_extra_ops.c")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Primitives related to librt.strings.BytesWriter that get linked statically
2+
// with compiled modules, instead of being called via a capsule.
3+
4+
#include "byteswriter_extra_ops.h"
5+
6+
char CPyBytesWriter_Write(PyObject *obj, PyObject *value) {
7+
BytesWriterObject *self = (BytesWriterObject *)obj;
8+
const char *data;
9+
Py_ssize_t size;
10+
if (likely(PyBytes_Check(value))) {
11+
data = PyBytes_AS_STRING(value);
12+
size = PyBytes_GET_SIZE(value);
13+
} else {
14+
data = PyByteArray_AS_STRING(value);
15+
size = PyByteArray_GET_SIZE(value);
16+
}
17+
// Write bytes content.
18+
if (!CPyBytesWriter_EnsureSize(self, size))
19+
return CPY_NONE_ERROR;
20+
if (size < 8) {
21+
// Loop tends to be faster for small sizes
22+
char *p = self->buf + self->len;
23+
for (Py_ssize_t i = 0; i < size; i++) {
24+
p[i] = data[i];
25+
}
26+
} else {
27+
memcpy(self->buf + self->len, data, size);
28+
}
29+
self->len += size;
30+
return CPY_NONE;
31+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#ifndef BYTESWRITER_EXTRA_OPS_H
2+
#define BYTESWRITER_EXTRA_OPS_H
3+
4+
#include "librt_strings.h"
5+
6+
static inline CPyTagged
7+
CPyBytesWriter_Len(PyObject *obj) {
8+
return (CPyTagged)((BytesWriterObject *)obj)->len << 1;
9+
}
10+
11+
static inline bool
12+
CPyBytesWriter_EnsureSize(BytesWriterObject *data, Py_ssize_t n) {
13+
if (likely(data->capacity - data->len >= n)) {
14+
return true;
15+
} else {
16+
return LibRTStrings_ByteWriter_grow_buffer_internal(data, n);
17+
}
18+
}
19+
20+
static inline char
21+
CPyBytesWriter_Append(PyObject *obj, uint8_t value) {
22+
BytesWriterObject *self = (BytesWriterObject *)obj;
23+
// Store length in a local variable to enable additional optimizations
24+
Py_ssize_t len = self->len;
25+
if (!CPyBytesWriter_EnsureSize(self, 1))
26+
return CPY_NONE_ERROR;
27+
self->buf[len] = value;
28+
self->len = len + 1;
29+
return CPY_NONE;
30+
}
31+
32+
char CPyBytesWriter_Write(PyObject *obj, PyObject *value);
33+
34+
#endif

mypyc/lib-rt/librt_internal.c

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
#define LONG_INT_TRAILER 15
2828

2929
#define CPY_BOOL_ERROR 2
30-
#define CPY_NONE_ERROR 2
31-
#define CPY_NONE 1
3230

3331
#define _CHECK_READ_BUFFER(data, err) if (unlikely(_check_read_buffer(data) == CPY_NONE_ERROR)) \
3432
return err;

mypyc/lib-rt/librt_strings.c

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,19 @@
77
#include "librt_strings.h"
88

99
#define CPY_BOOL_ERROR 2
10-
#define CPY_NONE_ERROR 2
11-
#define CPY_NONE 1
1210

1311
//
1412
// BytesWriter
1513
//
1614

17-
// Length of the default buffer embedded directly in a BytesWriter object
18-
#define WRITER_EMBEDDED_BUF_LEN 512
19-
20-
typedef struct {
21-
PyObject_HEAD
22-
char *buf; // Beginning of the buffer
23-
Py_ssize_t len; // Current length (number of bytes written)
24-
Py_ssize_t capacity; // Total capacity of the buffer
25-
char data[WRITER_EMBEDDED_BUF_LEN]; // Default buffer
26-
} BytesWriterObject;
27-
2815
#define _WRITE(data, type, v) \
2916
do { \
3017
*(type *)(((BytesWriterObject *)data)->buf + ((BytesWriterObject *)data)->len) = v; \
3118
((BytesWriterObject *)data)->len += sizeof(type); \
3219
} while (0)
3320

21+
#ifdef MYPYC_EXPERIMENTAL
22+
3423
static PyTypeObject BytesWriterType;
3524

3625
static bool
@@ -390,6 +379,8 @@ BytesWriter_len_internal(PyObject *self) {
390379
return writer->len << 1;
391380
}
392381

382+
#endif
383+
393384
static PyMethodDef librt_strings_module_methods[] = {
394385
{NULL, NULL, 0, NULL}
395386
};
@@ -426,9 +417,8 @@ librt_strings_module_exec(PyObject *m)
426417
(void *)BytesWriter_internal,
427418
(void *)BytesWriter_getvalue_internal,
428419
(void *)BytesWriter_append_internal,
429-
(void *)BytesWriter_write_internal,
420+
(void *)_grow_buffer,
430421
(void *)BytesWriter_type_internal,
431-
(void *)BytesWriter_len_internal,
432422
(void *)BytesWriter_truncate_internal,
433423
};
434424
PyObject *c_api_object = PyCapsule_New((void *)librt_strings_api, "librt.strings._C_API", NULL);

mypyc/lib-rt/librt_strings.h

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,38 @@ import_librt_strings(void)
1515
// ABI version -- only an exact match is compatible. This will only be changed in
1616
// very exceptional cases (likely never) due to strict backward compatibility
1717
// requirements.
18-
#define LIBRT_STRINGS_ABI_VERSION 0
18+
#define LIBRT_STRINGS_ABI_VERSION 1
1919

2020
// API version -- more recent versions must maintain backward compatibility, i.e.
2121
// we can add new features but not remove or change existing features (unless
2222
// ABI version is changed, but see the comment above).
23-
#define LIBRT_STRINGS_API_VERSION 1
23+
#define LIBRT_STRINGS_API_VERSION 2
2424

2525
// Number of functions in the capsule API. If you add a new function, also increase
2626
// LIBRT_STRINGS_API_VERSION.
27-
#define LIBRT_STRINGS_API_LEN 9
27+
#define LIBRT_STRINGS_API_LEN 8
2828

2929
static void *LibRTStrings_API[LIBRT_STRINGS_API_LEN];
3030

31+
// Length of the default buffer embedded directly in a BytesWriter object
32+
#define WRITER_EMBEDDED_BUF_LEN 512
33+
34+
typedef struct {
35+
PyObject_HEAD
36+
char *buf; // Beginning of the buffer
37+
Py_ssize_t len; // Current length (number of bytes written)
38+
Py_ssize_t capacity; // Total capacity of the buffer
39+
char data[WRITER_EMBEDDED_BUF_LEN]; // Default buffer
40+
} BytesWriterObject;
41+
3142
#define LibRTStrings_ABIVersion (*(int (*)(void)) LibRTStrings_API[0])
3243
#define LibRTStrings_APIVersion (*(int (*)(void)) LibRTStrings_API[1])
3344
#define LibRTStrings_BytesWriter_internal (*(PyObject* (*)(void)) LibRTStrings_API[2])
3445
#define LibRTStrings_BytesWriter_getvalue_internal (*(PyObject* (*)(PyObject *source)) LibRTStrings_API[3])
3546
#define LibRTStrings_BytesWriter_append_internal (*(char (*)(PyObject *source, uint8_t value)) LibRTStrings_API[4])
36-
#define LibRTStrings_BytesWriter_write_internal (*(char (*)(PyObject *source, PyObject *value)) LibRTStrings_API[5])
47+
#define LibRTStrings_ByteWriter_grow_buffer_internal (*(bool (*)(BytesWriterObject *obj, Py_ssize_t size)) LibRTStrings_API[5])
3748
#define LibRTStrings_BytesWriter_type_internal (*(PyTypeObject* (*)(void)) LibRTStrings_API[6])
38-
#define LibRTStrings_BytesWriter_len_internal (*(CPyTagged (*)(PyObject *self)) LibRTStrings_API[7])
39-
#define LibRTStrings_BytesWriter_truncate_internal (*(char (*)(PyObject *self, int64_t size)) LibRTStrings_API[8])
49+
#define LibRTStrings_BytesWriter_truncate_internal (*(char (*)(PyObject *self, int64_t size)) LibRTStrings_API[7])
4050

4151
static int
4252
import_librt_strings(void)

mypyc/lib-rt/mypyc_util.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ typedef PyObject CPyModule;
142142
// Error value for floats
143143
#define CPY_FLOAT_ERROR -113.0
144144

145+
// Value for 'None' primitive type
146+
#define CPY_NONE_ERROR 2
147+
#define CPY_NONE 1
148+
145149
typedef void (*CPyVTableItem)(void);
146150

147151
static inline CPyTagged CPyTagged_ShortFromInt(int x) {

mypyc/primitives/librt_strings_ops.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Final
22

3-
from mypyc.ir.deps import LIBRT_STRINGS
3+
from mypyc.ir.deps import BYTES_WRITER_EXTRA_OPS, LIBRT_STRINGS
44
from mypyc.ir.ops import ERR_MAGIC, ERR_NEVER
55
from mypyc.ir.rtypes import (
66
KNOWN_NATIVE_TYPES,
@@ -38,20 +38,20 @@
3838
name="write",
3939
arg_types=[bytes_writer_rprimitive, bytes_rprimitive],
4040
return_type=none_rprimitive,
41-
c_function_name="LibRTStrings_BytesWriter_write_internal",
41+
c_function_name="CPyBytesWriter_Write",
4242
error_kind=ERR_MAGIC,
4343
experimental=True,
44-
dependencies=[LIBRT_STRINGS],
44+
dependencies=[LIBRT_STRINGS, BYTES_WRITER_EXTRA_OPS],
4545
)
4646

4747
method_op(
4848
name="append",
4949
arg_types=[bytes_writer_rprimitive, uint8_rprimitive],
5050
return_type=none_rprimitive,
51-
c_function_name="LibRTStrings_BytesWriter_append_internal",
51+
c_function_name="CPyBytesWriter_Append",
5252
error_kind=ERR_MAGIC,
5353
experimental=True,
54-
dependencies=[LIBRT_STRINGS],
54+
dependencies=[LIBRT_STRINGS, BYTES_WRITER_EXTRA_OPS],
5555
)
5656

5757
method_op(
@@ -60,14 +60,16 @@
6060
return_type=none_rprimitive,
6161
c_function_name="LibRTStrings_BytesWriter_truncate_internal",
6262
error_kind=ERR_MAGIC,
63+
experimental=True,
64+
dependencies=[LIBRT_STRINGS],
6365
)
6466

6567
function_op(
6668
name="builtins.len",
6769
arg_types=[bytes_writer_rprimitive],
6870
return_type=short_int_rprimitive,
69-
c_function_name="LibRTStrings_BytesWriter_len_internal",
71+
c_function_name="CPyBytesWriter_Len",
7072
error_kind=ERR_NEVER,
7173
experimental=True,
72-
dependencies=[LIBRT_STRINGS],
74+
dependencies=[LIBRT_STRINGS, BYTES_WRITER_EXTRA_OPS],
7375
)

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ def bytes_writer_basics():
2929
L0:
3030
r0 = LibRTStrings_BytesWriter_internal()
3131
b = r0
32-
r1 = LibRTStrings_BytesWriter_append_internal(b, 1)
32+
r1 = CPyBytesWriter_Append(b, 1)
3333
r2 = b'foo'
34-
r3 = LibRTStrings_BytesWriter_write_internal(b, r2)
34+
r3 = CPyBytesWriter_Write(b, r2)
3535
n = 4
3636
r4 = n & 1
3737
r5 = r4 == 0
@@ -55,6 +55,6 @@ def bytes_writer_len(b):
5555
r0 :: short_int
5656
r1 :: i64
5757
L0:
58-
r0 = LibRTStrings_BytesWriter_len_internal(b)
58+
r0 = CPyBytesWriter_Len(b)
5959
r1 = r0 >> 1
6060
return r1

0 commit comments

Comments
 (0)