| 1 | #include <Python.h> |
| 2 | |
| 3 | extern "C" { |
| 4 | |
| 5 | #include <stdbool.h> |
| 6 | #include <time.h> |
| 7 | |
| 8 | #if defined(__i386__) || defined(__x86_64__) |
| 9 | #include <cpuid.h> |
| 10 | #include <x86intrin.h> |
| 11 | #endif |
| 12 | |
| 13 | #if defined(CLOCK_MONOTONIC_RAW) |
| 14 | #define TIMEOUT_CLOCK CLOCK_MONOTONIC_RAW |
| 15 | #else |
| 16 | #define TIMEOUT_CLOCK CLOCK_MONOTONIC |
| 17 | #endif |
| 18 | |
| 19 | #define CPU_SUPPORT_NONE 0 |
| 20 | #define CPU_SUPPORT_MONITORX 1 |
| 21 | |
| 22 | #define MWAITX_DEFAULT_TIMEOUT_CYCLES 1000000 |
| 23 | |
| 24 | typedef struct { |
| 25 | unsigned int cpu_support; |
| 26 | unsigned int max_monitor_line_size; |
| 27 | } spinloop_state_t; |
| 28 | |
| 29 | static void determine_cpu_support(spinloop_state_t* state) { |
| 30 | state->cpu_support = CPU_SUPPORT_NONE; |
| 31 | state->max_monitor_line_size = 0; |
| 32 | |
| 33 | #if defined(__i386__) || defined(__x86_64__) |
| 34 | unsigned int eax, ebx, ecx, edx; |
| 35 | if (__get_cpuid(0, &eax, &ebx, &ecx, &edx) == 1) { |
| 36 | // AMD CPU (possible monitorx/mwaitx support) |
| 37 | if (ebx == 0x68747541 && edx == 0x69746e65 && ecx == 0x444d4163) { |
| 38 | if (__get_cpuid(0x80000000, &eax, &ebx, &ecx, &edx) == 1 && |
| 39 | eax >= 0x80000001 && |
| 40 | __get_cpuid(0x80000001, &eax, &ebx, &ecx, &edx) == 1) { |
| 41 | if ((ecx & (1 << 29)) != 0) { |
| 42 | state->cpu_support = CPU_SUPPORT_MONITORX; |
| 43 | } |
| 44 | } |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | if (state->cpu_support == CPU_SUPPORT_MONITORX) { |
| 49 | if (__get_cpuid(5, &eax, &ebx, &ecx, &edx) == 1) { |
| 50 | state->max_monitor_line_size = ebx & 0xff; |
| 51 | } |
| 52 | } |
| 53 | #endif |
| 54 | } |
| 55 | |
| 56 | static PyObject* method_spinloop(PyObject* self, PyObject* args, |
| 57 | PyObject* kwargs) { |
| 58 | Py_buffer buffer; |
| 59 | PyObject* callback; |
| 60 | double timeout = 0.; |
| 61 | |
| 62 | spinloop_state_t* state = (spinloop_state_t*)PyModule_GetState(self); |
| 63 | if (state == NULL) { |
| 64 | PyErr_SetString(PyExc_TypeError, "Failed to retrieve module state!"); |
| 65 | return NULL; |
| 66 | } |
| 67 | |
| 68 | static const char* keywords[] = {"buffer", "callback", "timeout", NULL}; |
| 69 | if (!PyArg_ParseTupleAndKeywords(args, kwargs, "y*O|d", (char**)keywords, |
| 70 | &buffer, &callback, &timeout)) { |
| 71 | return NULL; |
| 72 | } |
| 73 | |
| 74 | if (!PyCallable_Check(callback)) { |
| 75 | PyErr_SetString(PyExc_TypeError, "callback parameter must be callable!"); |
| 76 | PyBuffer_Release(&buffer); |
| 77 | return NULL; |
| 78 | } |
| 79 | |
| 80 | struct timespec t_start; |
| 81 | if (clock_gettime(TIMEOUT_CLOCK, &t_start) != 0) { |
| 82 | PyErr_SetString(PyExc_RuntimeError, "clock_gettime() failed!"); |
| 83 | PyBuffer_Release(&buffer); |
| 84 | return NULL; |
| 85 | } |
| 86 | |
| 87 | bool result = false; |
| 88 | bool error = false; |
| 89 | bool have_timeout = (timeout > 1e-9); |
| 90 | unsigned int iteration = 0; |
| 91 | const bool buffer_qualifies = (buffer.len <= state->max_monitor_line_size); |
| 92 | |
| 93 | while (true) { |
| 94 | PyObject* res = PyObject_CallNoArgs(callback); |
| 95 | if (res == NULL) { |
| 96 | error = true; |
| 97 | break; |
| 98 | } |
| 99 | int ok = (res == Py_True); |
| 100 | Py_DECREF(res); |
| 101 | |
| 102 | if (ok) { |
| 103 | result = true; |
| 104 | break; |
| 105 | } |
| 106 | |
| 107 | // Check timeout at most every 16 iterations to avoid clock_gettime and |
| 108 | // comparison cost |
| 109 | if (have_timeout && (iteration & 15u) == 0) { |
| 110 | struct timespec t_now; |
| 111 | if (clock_gettime(TIMEOUT_CLOCK, &t_now) != 0) { |
| 112 | PyErr_SetString(PyExc_RuntimeError, "clock_gettime() failed!"); |
| 113 | error = true; |
| 114 | break; |
| 115 | } |
| 116 | |
| 117 | const double elapsed = (double)(t_now.tv_sec - t_start.tv_sec) + |
| 118 | (t_now.tv_nsec - t_start.tv_nsec) * 1e-9; |
| 119 | if (elapsed >= timeout) { |
| 120 | result = false; |
| 121 | break; |
| 122 | } |
| 123 | } |
| 124 | ++iteration; |
| 125 | |
| 126 | #if defined(__i386__) || defined(__x86_64__) |
| 127 | // monitorx + mwaitx with qualified buffer |
| 128 | if (buffer_qualifies && state->cpu_support == CPU_SUPPORT_MONITORX) { |
| 129 | _mm_monitorx(buffer.buf, 0, 0); |
| 130 | |
| 131 | // Check once more in case the buffer has been modified while we were |
| 132 | // arming the monitor hardware |
| 133 | res = PyObject_CallNoArgs(callback); |
| 134 | if (res == NULL) { |
| 135 | error = true; |
| 136 | break; |
| 137 | } |
| 138 | ok = (res == Py_True); |
| 139 | Py_DECREF(res); |
| 140 | |
| 141 | if (ok) { |
| 142 | result = true; |
| 143 | break; |
| 144 | } |
| 145 | |
| 146 | // Run mwaitx with enabled timeout (bit 1). The actual timeout value |
| 147 | // is not very important, we just want to ensure we don't lock up |
| 148 | // here for too long. |
| 149 | Py_BEGIN_ALLOW_THREADS _mm_mwaitx((1 << 1), 0, |
| 150 | MWAITX_DEFAULT_TIMEOUT_CYCLES); |
| 151 | Py_END_ALLOW_THREADS |
| 152 | } |
| 153 | |
| 154 | // Fallback: Busy poll |
| 155 | else { |
| 156 | #endif |
| 157 | // Give other threads a chance to be scheduled |
| 158 | Py_BEGIN_ALLOW_THREADS |
| 159 | #if defined(__i386__) || defined(__x86_64__) |
| 160 | __builtin_ia32_pause(); |
| 161 | #elif defined(__aarch64__) |
| 162 | __asm__ volatile("yield" :: : "memory"); |
| 163 | #endif |
| 164 | Py_END_ALLOW_THREADS |
| 165 | #if defined(__i386__) || defined(__x86_64__) |
| 166 | } |
| 167 | #endif |
| 168 | } |
| 169 | |
| 170 | PyBuffer_Release(&buffer); |
| 171 | |
| 172 | if (error) { |
| 173 | return NULL; |
| 174 | } |
| 175 | |
| 176 | if (result) { |
| 177 | Py_RETURN_TRUE; |
| 178 | } |
| 179 | |
| 180 | Py_RETURN_FALSE; |
| 181 | } |
| 182 | |
| 183 | static PyMethodDef spinloop_methods[] = { |
| 184 | {"spinloop", (PyCFunction)method_spinloop, METH_VARARGS | METH_KEYWORDS, |
| 185 | "Wait for store with callback"}, |
| 186 | {NULL, NULL, 0, NULL}}; |
| 187 | |
| 188 | static struct PyModuleDef spinloop_module = { |
| 189 | PyModuleDef_HEAD_INIT, "spinloop", |
| 190 | "Hardware-optimized spinloops for Python", sizeof(spinloop_state_t), |
| 191 | spinloop_methods}; |
| 192 | |
| 193 | PyMODINIT_FUNC PyInit_spinloop(void) { |
| 194 | PyObject* m = PyModule_Create(&spinloop_module); |
| 195 | if (m != NULL) { |
| 196 | spinloop_state_t* state = (spinloop_state_t*)PyModule_GetState(m); |
| 197 | if (state != NULL) { |
| 198 | determine_cpu_support(state); |
| 199 | } |
| 200 | } |
| 201 | return m; |
| 202 | } |
| 203 | |
| 204 | } // extern "C" |