Skip to content

Commit 1b2b0a3

Browse files
committed
fix: RNG performance improvements
1 parent fb47e3e commit 1b2b0a3

File tree

2 files changed

+153
-29
lines changed

2 files changed

+153
-29
lines changed

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
11
# igraph Python interface changelog
22

3+
## 0.9.5
4+
5+
### Fixed
6+
7+
* `set_random_number_generator(None)` now correctly switches back to igraph's
8+
own random number generator instead of the default one that hooks into
9+
the `random` module of Python.
10+
11+
* Improved performance in cases when igraph has to call back to Python's
12+
`random` module to generate random numbers. One example is
13+
`Graph.Degree_Sequence(method="vl")`, whose performance suffered a more than
14+
30x slowdown on 32-bit platforms before, compared to the native C
15+
implementation. Now the gap is smaller. Note that if you need performance and
16+
do not care about seeding the random number generator from Python, you can
17+
now use `set_random_number_generator(None)` to switch back to igraph's own
18+
RNG that does not need a roundtrip to Python.
19+
320
## 0.9.4
421

522
### Added

src/_igraph/random.c

Lines changed: 136 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,29 @@
2828
/**
2929
* \ingroup python_interface_rng
3030
* \brief Internal data structure for storing references to the
31-
* functions used from Python's random number generator.
31+
* functions and arguments used from Python's random number generator.
3232
*/
3333
typedef struct {
34+
PyObject* getrandbits_func;
3435
PyObject* randint_func;
3536
PyObject* random_func;
3637
PyObject* gauss_func;
38+
39+
PyObject* rng_bits_as_pyobject;
40+
PyObject* zero_as_pyobject;
41+
PyObject* one_as_pyobject;
42+
PyObject* rng_max_as_pyobject;
3743
} igraph_i_rng_Python_state_t;
3844

45+
/* igraph_rng_get_int31() is potentially faster if the max value of the RNG
46+
* is 0x7FFFFFFF; however, in case of Python, it is actually _slower_ because
47+
* Python long integers are not terribly efficient. We are better off with using
48+
* any other value here */
49+
#define RNG_MAX 0xFFFFFFFF
50+
51+
/* This must be consistent with the value of RNG_MAX above */
52+
#define RNG_BITS 32
53+
3954
static igraph_i_rng_Python_state_t igraph_rng_Python_state = {0, 0, 0};
4055
static igraph_rng_t igraph_rng_Python = {0, 0, 0};
4156
static igraph_rng_t igraph_rng_default_saved = {0, 0, 0};
@@ -66,25 +81,67 @@ PyObject* igraph_rng_Python_set_generator(PyObject* self, PyObject* object) {
6681
Py_RETURN_NONE;
6782
}
6883

69-
#define GET_FUNC(name) {\
84+
#define GET_FUNC(name) { \
7085
func = PyObject_GetAttrString(object, name); \
71-
if (func == 0) \
72-
return NULL; \
73-
if (!PyCallable_Check(func)) {\
74-
PyErr_SetString(PyExc_TypeError, name "attribute must be callable"); \
75-
return NULL; \
86+
if (func == 0) {\
87+
return 0; \
88+
} else if (!PyCallable_Check(func)) { \
89+
PyErr_SetString(PyExc_TypeError, "'" name "' attribute must be callable"); \
90+
return 0; \
7691
} \
7792
}
7893

94+
#define GET_OPTIONAL_FUNC(name) { \
95+
if (PyObject_HasAttrString(object, name)) { \
96+
func = PyObject_GetAttrString(object, name); \
97+
if (func == 0) { \
98+
return 0; \
99+
} else if (!PyCallable_Check(func)) { \
100+
PyErr_SetString(PyExc_TypeError, "'" name "' attribute must be callable"); \
101+
return 0; \
102+
} \
103+
} else { \
104+
func = 0; \
105+
} \
106+
}
107+
108+
GET_OPTIONAL_FUNC("getrandbits"); new_state.getrandbits_func = func;
79109
GET_FUNC("randint"); new_state.randint_func = func;
80110
GET_FUNC("random"); new_state.random_func = func;
81111
GET_FUNC("gauss"); new_state.gauss_func = func;
82112

113+
/* construct the arguments of getrandbits(RNG_BITS) and randint(0, RNG_MAX)
114+
* in advance */
115+
new_state.rng_bits_as_pyobject = PyLong_FromLong(RNG_BITS);
116+
if (new_state.rng_bits_as_pyobject == 0) {
117+
return 0;
118+
}
119+
new_state.zero_as_pyobject = PyLong_FromLong(0);
120+
if (new_state.zero_as_pyobject == 0) {
121+
return 0;
122+
}
123+
new_state.one_as_pyobject = PyLong_FromLong(1);
124+
if (new_state.one_as_pyobject == 0) {
125+
return 0;
126+
}
127+
new_state.rng_max_as_pyobject = PyLong_FromUnsignedLong(RNG_MAX);
128+
if (new_state.rng_max_as_pyobject == 0) {
129+
return 0;
130+
}
131+
132+
#undef GET_FUNC
133+
#undef GET_OPTIONAL_FUNC
134+
83135
old_state = igraph_rng_Python_state;
84136
igraph_rng_Python_state = new_state;
137+
Py_XDECREF(old_state.getrandbits_func);
85138
Py_XDECREF(old_state.randint_func);
86139
Py_XDECREF(old_state.random_func);
87140
Py_XDECREF(old_state.gauss_func);
141+
Py_XDECREF(old_state.rng_bits_as_pyobject);
142+
Py_XDECREF(old_state.zero_as_pyobject);
143+
Py_XDECREF(old_state.one_as_pyobject);
144+
Py_XDECREF(old_state.rng_max_as_pyobject);
88145

89146
igraph_rng_set_default(&igraph_rng_Python);
90147

@@ -106,38 +163,75 @@ int igraph_rng_Python_seed(void *state, unsigned long int seed) {
106163
* \brief Generates an unsigned long integer using the Python random number generator.
107164
*/
108165
unsigned long int igraph_rng_Python_get(void *state) {
109-
PyObject* result = PyObject_CallFunction(igraph_rng_Python_state.randint_func, "kk", 0, LONG_MAX);
166+
PyObject* result;
167+
PyObject* exc_type;
110168
unsigned long int retval;
111169

170+
if (igraph_rng_Python_state.getrandbits_func) {
171+
/* This is the preferred code path if the random module given by the user
172+
* supports getrandbits(); it is faster than randint() but still slower
173+
* than simply calling random() */
174+
result = PyObject_CallFunctionObjArgs(
175+
igraph_rng_Python_state.getrandbits_func,
176+
igraph_rng_Python_state.rng_bits_as_pyobject,
177+
0
178+
);
179+
} else {
180+
/* We want to avoid hitting this path at all costs because randint() is
181+
* very costly in the Python layer */
182+
result = PyObject_CallFunctionObjArgs(
183+
igraph_rng_Python_state.randint_func,
184+
igraph_rng_Python_state.zero_as_pyobject,
185+
igraph_rng_Python_state.rng_max_as_pyobject,
186+
0
187+
);
188+
}
189+
112190
if (result == 0) {
113-
PyErr_WriteUnraisable(PyErr_Occurred());
114-
PyErr_Clear();
191+
exc_type = PyErr_Occurred();
192+
if (exc_type == PyExc_KeyboardInterrupt) {
193+
/* KeyboardInterrupt is okay, we don't report it, just store it and let
194+
* the caller handler it at the earliest convenience */
195+
} else {
196+
/* All other exceptions are reported and cleared */
197+
PyErr_WriteUnraisable(exc_type);
198+
PyErr_Clear();
199+
}
115200
/* Fallback to the C random generator */
116-
return rand() * LONG_MAX;
201+
return rand() * RNG_MAX;
202+
} else {
203+
retval = PyLong_AsUnsignedLong(result);
204+
Py_DECREF(result);
205+
return retval;
117206
}
118-
retval = PyLong_AsLong(result);
119-
Py_DECREF(result);
120-
return retval;
121207
}
122208

123209
/**
124210
* \ingroup python_interface_rng
125211
* \brief Generates a real number between 0 and 1 using the Python random number generator.
126212
*/
127213
igraph_real_t igraph_rng_Python_get_real(void *state) {
128-
PyObject* result = PyObject_CallObject(igraph_rng_Python_state.random_func, 0);
214+
PyObject* exc_type;
129215
double retval;
216+
PyObject* result = PyObject_CallObject(igraph_rng_Python_state.random_func, 0);
130217

131218
if (result == 0) {
132-
PyErr_WriteUnraisable(PyErr_Occurred());
133-
PyErr_Clear();
219+
exc_type = PyErr_Occurred();
220+
if (exc_type == PyExc_KeyboardInterrupt) {
221+
/* KeyboardInterrupt is okay, we don't report it, just store it and let
222+
* the caller handler it at the earliest convenience */
223+
} else {
224+
/* All other exceptions are reported and cleared */
225+
PyErr_WriteUnraisable(exc_type);
226+
PyErr_Clear();
227+
}
134228
/* Fallback to the C random generator */
135229
return rand();
230+
} else {
231+
retval = PyFloat_AsDouble(result);
232+
Py_DECREF(result);
233+
return retval;
136234
}
137-
138-
retval = PyFloat_AsDouble(result);
139-
Py_DECREF(result);
140-
return retval;
141235
}
142236

143237
/**
@@ -146,19 +240,32 @@ igraph_real_t igraph_rng_Python_get_real(void *state) {
146240
* around zero with unit variance.
147241
*/
148242
igraph_real_t igraph_rng_Python_get_norm(void *state) {
149-
PyObject* result = PyObject_CallFunction(igraph_rng_Python_state.gauss_func, "dd", 0.0, 1.0);
243+
PyObject* exc_type;
150244
double retval;
245+
PyObject* result = PyObject_CallFunctionObjArgs(
246+
igraph_rng_Python_state.gauss_func,
247+
igraph_rng_Python_state.zero_as_pyobject,
248+
igraph_rng_Python_state.one_as_pyobject,
249+
0
250+
);
151251

152252
if (result == 0) {
153-
PyErr_WriteUnraisable(PyErr_Occurred());
154-
PyErr_Clear();
253+
exc_type = PyErr_Occurred();
254+
if (exc_type == PyExc_KeyboardInterrupt) {
255+
/* KeyboardInterrupt is okay, we don't report it, just store it and let
256+
* the caller handler it at the earliest convenience */
257+
} else {
258+
/* All other exceptions are reported and cleared */
259+
PyErr_WriteUnraisable(exc_type);
260+
PyErr_Clear();
261+
}
155262
/* Fallback to the C random generator */
156263
return 0;
264+
} else {
265+
retval = PyFloat_AsDouble(result);
266+
Py_DECREF(result);
267+
return retval;
157268
}
158-
159-
retval = PyFloat_AsDouble(result);
160-
Py_DECREF(result);
161-
return retval;
162269
}
163270

164271
/**
@@ -169,7 +276,7 @@ igraph_real_t igraph_rng_Python_get_norm(void *state) {
169276
igraph_rng_type_t igraph_rngtype_Python = {
170277
/* name= */ "Python random generator",
171278
/* min= */ 0,
172-
/* max= */ LONG_MAX,
279+
/* max= */ RNG_MAX,
173280
/* init= */ igraph_rng_Python_init,
174281
/* destroy= */ igraph_rng_Python_destroy,
175282
/* seed= */ igraph_rng_Python_seed,

0 commit comments

Comments
 (0)