Skip to content

Commit 1c17dd2

Browse files
authored
Merge pull request #9 from dgoeries/nogil
feat: Release gil option for loop in LTTB
2 parents f23ee7b + 9e1872b commit 1c17dd2

3 files changed

Lines changed: 89 additions & 60 deletions

File tree

src/downsample/_lttb.c

Lines changed: 56 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
2-
#include "utils.h"
32
#include <Python.h>
43
#include <math.h>
54
#include <numpy/arrayobject.h>
65
#include <numpy/npy_math.h>
76

7+
#include "utils.h"
8+
89

910
static PyObject *largest_triangle_three_buckets(PyObject *self,
1011
PyObject *args) {
@@ -31,8 +32,11 @@ static PyObject *largest_triangle_three_buckets(PyObject *self,
3132
NPY_ARRAY_IN_ARRAY);
3233
y_array = (PyArrayObject *)PyArray_FROM_OTF(y_obj, NPY_DOUBLE,
3334
NPY_ARRAY_IN_ARRAY);
34-
if (!x_array || !y_array)
35+
if (!x_array || !y_array) {
36+
PyErr_SetString(PyExc_ValueError,
37+
"Failed to convert inputs to NumPy arrays.");
3538
goto fail;
39+
}
3640

3741
if (PyArray_NDIM(x_array) != 1 || PyArray_NDIM(y_array) != 1) {
3842
PyErr_SetString(PyExc_ValueError, "x and y must be 1-dimensional.");
@@ -52,37 +56,33 @@ static PyObject *largest_triangle_three_buckets(PyObject *self,
5256
Py_DECREF(y_array);
5357
return result;
5458
}
59+
5560
double *x = (double *)PyArray_DATA(x_array);
5661
double *y = (double *)PyArray_DATA(y_array);
57-
// Create an empty output array with shape and dim for the output!
58-
npy_intp dims[1];
59-
dims[0] = threshold;
60-
PyArrayObject *result_x = (PyArrayObject *)PyArray_Empty(
61-
1, dims, PyArray_DescrFromType(NPY_DOUBLE), 0);
62-
PyArrayObject *result_y = (PyArrayObject *)PyArray_Empty(
63-
1, dims, PyArray_DescrFromType(NPY_DOUBLE), 0);
64-
65-
// Get a pointer to its data
66-
double *result_x_data = (double *)PyArray_DATA(result_x);
67-
double *result_y_data = (double *)PyArray_DATA(result_y);
68-
69-
// The main loop here!
70-
const double every = (double)(len_points - 2) / (threshold - 2);
71-
72-
npy_intp a = 0;
73-
npy_intp next_a = 0;
7462

75-
double max_area_point_x = 0.0;
76-
double max_area_point_y = 0.0;
63+
double *result_x = (double *)malloc(threshold * sizeof(double));
64+
double *result_y = (double *)malloc(threshold * sizeof(double));
65+
if (!result_x || !result_y) {
66+
PyErr_SetString(PyExc_MemoryError,
67+
"Failed to allocate memory for result arrays.");
68+
free(result_x);
69+
free(result_y);
70+
goto fail;
71+
}
7772

73+
const double every = (double)(len_points - 2) / (threshold - 2);
7874
// Always add the first point!
79-
result_x_data[0] = npy_isfinite(x[a]) ? x[a] : 0.0;
80-
result_y_data[0] = npy_isfinite(y[a]) ? y[a] : 0.0;
75+
result_x[0] = npy_isfinite(x[0]) ? x[0] : 0.0;
76+
result_y[0] = npy_isfinite(y[0]) ? y[0] : 0.0;
8177

78+
npy_intp a = 0, next_a = 0;
79+
80+
Py_BEGIN_ALLOW_THREADS;
8281
for (npy_intp i = 0; i < threshold - 2; ++i) {
83-
// Calculate point average for next bucket (containing c)
84-
double avg_x = 0;
85-
double avg_y = 0;
82+
double avg_x = 0, avg_y = 0;
83+
// Careful, thread local variables
84+
double max_area_point_x = 0.0;
85+
double max_area_point_y = 0.0;
8686
npy_intp avg_start = (npy_intp)(floor((i + 1) * every) + 1);
8787
npy_intp avg_end = (npy_intp)(floor((i + 2) * every) + 1);
8888
if (avg_end >= len_points) {
@@ -98,49 +98,49 @@ static PyObject *largest_triangle_three_buckets(PyObject *self,
9898
avg_y /= avg_length;
9999

100100
// Get the range for this bucket
101-
npy_intp k = (npy_intp)(floor((i + 0) * every) + 1);
102-
npy_intp range_to = (npy_intp)(floor((i + 1) * every) + 1);
101+
npy_intp range_start = (npy_intp)(floor((i + 0) * every) + 1);
102+
npy_intp range_end = (npy_intp)(floor((i + 1) * every) + 1);
103103

104104
// Point a
105-
double point_a_x = x[a];
106-
double point_a_y = y[a];
107-
105+
double point_a[2] = {x[a], y[a]};
108106
double max_area = -1.0;
109-
for (; k < range_to; k++) {
110-
// Calculate triangle area over three buckets
111-
double point_data[2] = {point_a_x, point_a_y};
112-
double avg_data[2] = {avg_y, avg_y};
113-
double next_data[2] = {x[k], y[k]};
114-
double area =
115-
calculate_triangle_area(point_data, avg_data, next_data);
107+
108+
for (npy_intp k = range_start; k < range_end; k++) {
109+
double point_k[2] = {x[k], y[k]};
110+
double avg_point[2] = {avg_x, avg_y};
111+
double area = calculate_triangle_area(point_a, avg_point, point_k);
116112
if (area > max_area) {
117113
max_area = area;
118114
max_area_point_x = x[k];
119115
max_area_point_y = y[k];
120-
next_a = k; // Next a is this b
116+
next_a = k;
121117
}
122118
}
123-
// Pick this point from the bucket
124-
result_x_data[(npy_intp)i + 1] = max_area_point_x;
125-
result_y_data[(npy_intp)i + 1] = max_area_point_y;
126-
// Current a becomes the next_a (chosen b)
119+
120+
result_x[i + 1] = max_area_point_x;
121+
result_y[i + 1] = max_area_point_y;
127122
a = next_a;
128123
}
129-
130-
// Always add last! Check for finite values!
131-
double last_a_x = x[len_points - 1];
132-
double last_a_y = y[len_points - 1];
133-
result_x_data[threshold - 1] = npy_isfinite(last_a_x) ? last_a_x : 0.0;
134-
result_y_data[threshold - 1] = npy_isfinite(last_a_y) ? last_a_y : 0.0;
135-
136-
PyObject *result = PyTuple_Pack(2, result_x, result_y);
137-
138-
// And remove the references!
124+
Py_END_ALLOW_THREADS;
125+
126+
result_x[threshold - 1] =
127+
npy_isfinite(x[len_points - 1]) ? x[len_points - 1] : 0.0;
128+
result_y[threshold - 1] =
129+
npy_isfinite(y[len_points - 1]) ? y[len_points - 1] : 0.0;
130+
131+
npy_intp dims[1] = {threshold};
132+
PyObject *npx =
133+
PyArray_SimpleNewFromData(1, dims, NPY_DOUBLE, (void *)result_x);
134+
PyObject *npy =
135+
PyArray_SimpleNewFromData(1, dims, NPY_DOUBLE, (void *)result_y);
136+
PyArray_ENABLEFLAGS((PyArrayObject *)npx, NPY_ARRAY_OWNDATA);
137+
PyArray_ENABLEFLAGS((PyArrayObject *)npy, NPY_ARRAY_OWNDATA);
138+
139+
PyObject *result = PyTuple_Pack(2, npx, npy);
139140
Py_DECREF(x_array);
140141
Py_DECREF(y_array);
141-
Py_XDECREF(result_x);
142-
Py_XDECREF(result_y);
143-
142+
Py_DECREF(npx);
143+
Py_DECREF(npy);
144144
return result;
145145

146146
fail:
@@ -149,7 +149,6 @@ static PyObject *largest_triangle_three_buckets(PyObject *self,
149149
return NULL;
150150
}
151151

152-
153152
static PyMethodDef LTTBMethods[] = {
154153
{"largest_triangle_three_buckets", largest_triangle_three_buckets,
155154
METH_VARARGS,
@@ -163,9 +162,7 @@ static struct PyModuleDef LTTBModule = {
163162
"algorithm (LTTB) using C code.",
164163
-1, LTTBMethods};
165164

166-
167165
PyMODINIT_FUNC PyInit__lttb(void) {
168-
Py_Initialize();
169166
import_array();
170167
return PyModule_Create(&LTTBModule);
171168
}

src/downsample/tests/test_lttb.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,7 @@ def test_array_mix_inf_nan():
260260
assert sys.getrefcount(nx) == 2
261261
assert sys.getrefcount(ny) == 2
262262
test_array = np.array(
263-
[0., 0., 4., 4., 4., 4., 12., 12., 16., 19.], dtype=np.float64)
263+
[0., 0., 4., 0., 0., 0., 12., 0., 16., 19.], dtype=np.float64)
264264
np.testing.assert_array_almost_equal(ny, test_array)
265265

266266

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import tracemalloc
2+
import numpy as np
3+
from downsample import largest_triangle_three_buckets
4+
5+
6+
def test_memory_leak():
7+
tracemalloc.start()
8+
9+
size = 1_000_000
10+
x = np.linspace(0, 10, size)
11+
y = np.sin(x)
12+
threshold = 100
13+
14+
before_snapshot = tracemalloc.take_snapshot()
15+
16+
for _ in range(1_000):
17+
result = largest_triangle_three_buckets(x, y, threshold)
18+
del result
19+
import gc
20+
gc.collect()
21+
22+
after_snapshot = tracemalloc.take_snapshot()
23+
tracemalloc.stop()
24+
25+
before_size = sum(
26+
stat.size for stat in before_snapshot.statistics("filename"))
27+
after_size = sum(
28+
stat.size for stat in after_snapshot.statistics("filename"))
29+
30+
print(f"Memory before loop: {before_size} bytes")
31+
print(f"Memory after loop: {after_size} bytes")
32+
assert after_size <= before_size + 1024, "Memory not freed properly!"

0 commit comments

Comments
 (0)