Skip to content

Commit f4d1e60

Browse files
Core: Use a consistent implementation for arrays (#48)
Mostly importantly, this removes any size limits and significantly reduces the size of a PyAwaitable object. A couple other things as well: - Add a `PYAWAITABLE_OPTIMIZED` setting for CI. - Changed some error types from `SystemError` to `RuntimeError`, because apparently it wasn't clear that those were user errors. - Removed the awaitable pool which only slightly improved performance, at the cost of using more preallocated memory. We should just use a freelist in the future.
1 parent a96d239 commit f4d1e60

File tree

15 files changed

+754
-315
lines changed

15 files changed

+754
-315
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ concurrency:
1616
env:
1717
CIBW_SKIP: >
1818
pp*
19+
PYAWAITABLE_OPTIMIZED: 1
1920

2021
jobs:
2122
binary-wheels-standard:

.github/workflows/memory_leak.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ env:
1818
PYTHONUNBUFFERED: "1"
1919
FORCE_COLOR: "1"
2020
PYTHONIOENCODING: "utf8"
21+
PYTHONMALLOC: malloc
2122

2223
jobs:
2324
memory-leaks:

.github/workflows/tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ env:
2424
PYTHONUNBUFFERED: "1"
2525
FORCE_COLOR: "1"
2626
PYTHONIOENCODING: "utf8"
27+
PYAWAITABLE_OPTIMIZED: 1
2728

2829
jobs:
2930
run-tests:

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
- Significantly reduced awaitable object size by dynamically allocating it.
11+
- Reduced memory footprint by removing preallocated awaitable objects.
12+
- Objects returned by a PyAwaitable object's `__await__` are now garbage collected (*i.e.*, they don't leak with rare circular references).
13+
- Removed limit on number of stored callbacks or values.
14+
- Switched some user-error messages to `RuntimeError` instead of `SystemError`.
15+
816
## [1.3.0] - 2024-10-26
917

1018
- Added support for `async with` via `pyawaitable_async_with`.

include/pyawaitable/array.h

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
#ifndef PYAWAITABLE_ARRAY_H
2+
#define PYAWAITABLE_ARRAY_H
3+
4+
#include <Python.h>
5+
#include <stdlib.h>
6+
7+
#define pyawaitable_array_DEFAULT_SIZE 16
8+
9+
/*
10+
* Deallocator for items on a pyawaitable_array structure. A NULL pointer
11+
* will never be given to the deallocator.
12+
*/
13+
typedef void (*pyawaitable_array_deallocator)(void *);
14+
15+
/*
16+
* Internal only dynamic array for CPython.
17+
*/
18+
typedef struct
19+
{
20+
/*
21+
* The actual items in the dynamic array.
22+
* Don't access this field publicly to get
23+
* items--use pyawaitable_array_GET_ITEM() instead.
24+
*/
25+
void **items;
26+
/*
27+
* The length of the actual items array allocation.
28+
*/
29+
Py_ssize_t capacity;
30+
/*
31+
* The number of items in the array.
32+
* Don't use this field publicly--use pyawaitable_array_LENGTH()
33+
*/
34+
Py_ssize_t length;
35+
/*
36+
* The deallocator, set by one of the initializer functions.
37+
* This may be NULL.
38+
*/
39+
pyawaitable_array_deallocator deallocator;
40+
} pyawaitable_array;
41+
42+
43+
/* Zero out the array */
44+
static inline void
45+
pyawaitable_array_ZERO(pyawaitable_array *array)
46+
{
47+
assert(array != NULL);
48+
array->deallocator = NULL;
49+
array->items = NULL;
50+
array->length = 0;
51+
array->capacity = 0;
52+
}
53+
54+
static inline void
55+
pyawaitable_array_ASSERT_VALID(pyawaitable_array *array)
56+
{
57+
assert(array != NULL);
58+
assert(array->items != NULL);
59+
}
60+
61+
static inline void
62+
pyawaitable_array_ASSERT_INDEX(pyawaitable_array *array, Py_ssize_t index)
63+
{
64+
// Ensure the index is valid
65+
assert(index < array->length);
66+
assert(index >= 0);
67+
}
68+
69+
/*
70+
* Initialize a dynamic array with an initial size and deallocator.
71+
*
72+
* If the deallocator is NULL, then nothing happens to items upon
73+
* removal and upon array clearing.
74+
*
75+
* Returns -1 upon failure, 0 otherwise.
76+
*/
77+
int
78+
pyawaitable_array_init_with_size(
79+
pyawaitable_array *array,
80+
pyawaitable_array_deallocator deallocator,
81+
Py_ssize_t initial
82+
);
83+
84+
/*
85+
* Append to the array.
86+
*
87+
* Returns -1 upon failure, 0 otherwise.
88+
* If this fails, the deallocator is not ran on the item.
89+
*/
90+
int pyawaitable_array_append(pyawaitable_array *array, void *item);
91+
92+
/*
93+
* Insert an item at the target index. The index
94+
* must currently be a valid index in the array.
95+
*
96+
* Returns -1 upon failure, 0 otherwise.
97+
* If this fails, the deallocator is not ran on the item.
98+
*/
99+
int
100+
pyawaitable_array_insert(
101+
pyawaitable_array *array,
102+
Py_ssize_t index,
103+
void *item
104+
);
105+
106+
/* Remove all items from the array. */
107+
void
108+
pyawaitable_array_clear_items(pyawaitable_array *array);
109+
110+
/*
111+
* Clear all the fields on the array.
112+
*
113+
* Note that this does *not* free the actual dynamic array
114+
* structure--use pyawaitable_array_Free() for that.
115+
*
116+
* It's safe to call pyawaitable_array_init() or init_with_size() again
117+
* on the array after calling this.
118+
*/
119+
void pyawaitable_array_clear(pyawaitable_array *array);
120+
121+
/*
122+
* Set a value at index in the array.
123+
*
124+
* If an item already exists at the target index, the deallocator
125+
* is called on it, if the array has one set.
126+
*
127+
* This cannot fail.
128+
*/
129+
void
130+
pyawaitable_array_set(pyawaitable_array *array, Py_ssize_t index, void *item);
131+
132+
/*
133+
* Remove the item at the index, and call the deallocator on it (if the array
134+
* has one set).
135+
*
136+
* This cannot fail.
137+
*/
138+
void
139+
pyawaitable_array_remove(pyawaitable_array *array, Py_ssize_t index);
140+
141+
/*
142+
* Remove the item at the index *without* deallocating it, and
143+
* return the item.
144+
*
145+
* This cannot fail.
146+
*/
147+
void *
148+
pyawaitable_array_pop(pyawaitable_array *array, Py_ssize_t index);
149+
150+
/*
151+
* Clear all the fields on a dynamic array, and then
152+
* free the dynamic array structure itself.
153+
*
154+
* The array must have been created by pyawaitable_array_new()
155+
*/
156+
static inline void
157+
pyawaitable_array_free(pyawaitable_array *array)
158+
{
159+
pyawaitable_array_ASSERT_VALID(array);
160+
pyawaitable_array_clear(array);
161+
PyMem_RawFree(array);
162+
}
163+
164+
/*
165+
* Equivalent to pyawaitable_array_init_with_size() with a default size of 16.
166+
*
167+
* Returns -1 upon failure, 0 otherwise.
168+
*/
169+
static inline int
170+
pyawaitable_array_init(
171+
pyawaitable_array *array,
172+
pyawaitable_array_deallocator deallocator
173+
)
174+
{
175+
return pyawaitable_array_init_with_size(
176+
array,
177+
deallocator,
178+
pyawaitable_array_DEFAULT_SIZE
179+
);
180+
}
181+
182+
/*
183+
* Allocate and create a new dynamic array on the heap.
184+
*
185+
* The returned pointer should be freed with pyawaitable_array_free()
186+
* If this function fails, it returns NULL.
187+
*/
188+
static inline pyawaitable_array *
189+
pyawaitable_array_new_with_size(
190+
pyawaitable_array_deallocator deallocator,
191+
Py_ssize_t initial
192+
)
193+
{
194+
pyawaitable_array *array = PyMem_Malloc(sizeof(pyawaitable_array));
195+
if (array == NULL)
196+
{
197+
return NULL;
198+
}
199+
200+
if (pyawaitable_array_init_with_size(array, deallocator, initial) < 0)
201+
{
202+
PyMem_Free(array);
203+
return NULL;
204+
}
205+
206+
pyawaitable_array_ASSERT_VALID(array); // Sanity check
207+
return array;
208+
}
209+
210+
/*
211+
* Equivalent to pyawaitable_array_new_with_size() with a size of 16.
212+
*
213+
* The returned array must be freed with pyawaitable_array_free().
214+
* Returns NULL on failure.
215+
*/
216+
static inline pyawaitable_array *
217+
pyawaitable_array_new(pyawaitable_array_deallocator deallocator)
218+
{
219+
return pyawaitable_array_new_with_size(
220+
deallocator,
221+
pyawaitable_array_DEFAULT_SIZE
222+
);
223+
}
224+
225+
/*
226+
* Get an item from the array. This cannot fail.
227+
*
228+
* If the index is not valid, this is undefined behavior.
229+
*/
230+
static inline void *
231+
pyawaitable_array_GET_ITEM(pyawaitable_array *array, Py_ssize_t index)
232+
{
233+
pyawaitable_array_ASSERT_VALID(array);
234+
pyawaitable_array_ASSERT_INDEX(array, index);
235+
return array->items[index];
236+
}
237+
238+
/*
239+
* Get the length of the array. This cannot fail.
240+
*/
241+
static inline Py_ssize_t
242+
pyawaitable_array_LENGTH(pyawaitable_array *array)
243+
{
244+
pyawaitable_array_ASSERT_VALID(array);
245+
return array->length;
246+
}
247+
248+
/*
249+
* Pop the item at the end the array.
250+
* This function cannot fail.
251+
*/
252+
static inline void *
253+
pyawaitable_array_pop_top(pyawaitable_array *array)
254+
{
255+
return pyawaitable_array_pop(array, pyawaitable_array_LENGTH(array) - 1);
256+
}
257+
258+
#endif

include/pyawaitable/awaitableobject.h

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
#include <Python.h>
55
#include <stdbool.h>
66

7+
#include <pyawaitable/array.h>
8+
79
typedef int (*awaitcallback)(PyObject *, PyObject *);
810
typedef int (*awaitcallback_err)(PyObject *, PyObject *);
9-
#define CALLBACK_ARRAY_SIZE 128
10-
#define VALUE_ARRAY_SIZE 32
1111

1212
typedef struct _pyawaitable_callback
1313
{
@@ -21,31 +21,23 @@ struct _PyAwaitableObject
2121
{
2222
PyObject_HEAD
2323

24-
// Callbacks
25-
pyawaitable_callback aw_callbacks[CALLBACK_ARRAY_SIZE];
26-
Py_ssize_t aw_callback_index;
27-
28-
// Stored Values
29-
PyObject *aw_values[VALUE_ARRAY_SIZE];
30-
Py_ssize_t aw_values_index;
31-
32-
// Arbitrary Values
33-
void *aw_arb_values[VALUE_ARRAY_SIZE];
34-
Py_ssize_t aw_arb_values_index;
35-
36-
// Integer Values
37-
long aw_int_values[VALUE_ARRAY_SIZE];
38-
Py_ssize_t aw_int_values_index;
24+
pyawaitable_array aw_callbacks;
25+
pyawaitable_array aw_object_values;
26+
pyawaitable_array aw_arbitrary_values;
27+
pyawaitable_array aw_integer_values;
3928

40-
// Awaitable State
29+
/* Index of current callback */
4130
Py_ssize_t aw_state;
31+
/* Is the awaitable done? */
4232
bool aw_done;
33+
/* Was the awaitable awaited? */
4334
bool aw_awaited;
44-
bool aw_used;
45-
46-
// Misc
35+
/* Strong reference to the result of the coroutine. */
4736
PyObject *aw_result;
37+
/* Strong reference to the genwrapper. */
4838
PyObject *aw_gen;
39+
/* Set to 1 if the object was cancelled, for introspection against callbacks */
40+
int aw_recently_cancelled;
4941
};
5042

5143
typedef struct _PyAwaitableObject PyAwaitableObject;
@@ -78,9 +70,4 @@ pyawaitable_await_function_impl(
7870
...
7971
);
8072

81-
int
82-
alloc_awaitable_pool(void);
83-
void
84-
dealloc_awaitable_pool(void);
85-
8673
#endif

include/pyawaitable/genwrapper.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ genwrapper_next(PyObject *self);
1818

1919
int genwrapper_fire_err_callback(
2020
PyObject *self,
21-
PyObject *await,
22-
pyawaitable_callback *cb
21+
awaitcallback_err err_callback
2322
);
2423

2524
PyObject *

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
from glob import glob
22

33
from setuptools import Extension, setup
4+
import os
45

56
if __name__ == "__main__":
67
setup(
78
name="pyawaitable",
89
license="MIT",
9-
version="1.3.0",
10+
version="1.4.0-dev",
1011
ext_modules=[
1112
Extension(
1213
"_pyawaitable",
1314
glob("./src/_pyawaitable/*.c"),
1415
include_dirs=["./include/", "./src/pyawaitable/"],
15-
extra_compile_args=["-g", "-O3"],
16+
extra_compile_args=["-g", "-O3" if os.environ.get("PYAWAITABLE_OPTIMIZED") else "-O0"],
1617
)
1718
],
1819
package_dir={"": "src"},

0 commit comments

Comments
 (0)