Skip to content

Commit 977f1a6

Browse files
committed
Add event loop performance optimizations
- Fix run_until_complete callback removal bug (use single reference) - Cache ast.literal_eval lookup at module initialization - Add O(1) timer cancellation via handle-to-callback_id reverse map - Detach pending queue under lock, build Erlang terms outside lock - Replace O(n) duplicate scan with hash set (128-slot open addressing) - Add PERF_BUILD cmake option for aggressive optimizations (-O3, LTO)
1 parent be35b2c commit 977f1a6

6 files changed

Lines changed: 267 additions & 50 deletions

File tree

c_src/CMakeLists.txt

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ if(NOT CMAKE_BUILD_TYPE)
4747
set(CMAKE_BUILD_TYPE Release)
4848
endif()
4949

50+
# Performance build option (for maximum optimization)
51+
option(PERF_BUILD "Enable aggressive performance optimizations (-O3, LTO, native arch)" OFF)
52+
53+
if(PERF_BUILD)
54+
message(STATUS "Performance build enabled - using aggressive optimizations")
55+
# Override compiler flags for maximum performance
56+
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")
57+
# Enable Link-Time Optimization
58+
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
59+
endif()
60+
5061
# Find Erlang
5162
include(FindErlang)
5263
include_directories(${ERLANG_ERTS_INCLUDE_PATH})
@@ -99,11 +110,24 @@ target_include_directories(py_nif PRIVATE
99110
)
100111

101112
# Compiler flags
102-
target_compile_options(py_nif PRIVATE
103-
-O2
104-
-Wall
105-
-fPIC
106-
)
113+
if(PERF_BUILD)
114+
# Performance build: aggressive optimizations
115+
target_compile_options(py_nif PRIVATE
116+
-O3
117+
-Wall
118+
-fPIC
119+
-march=native
120+
-ffast-math
121+
-funroll-loops
122+
)
123+
else()
124+
# Standard build
125+
target_compile_options(py_nif PRIVATE
126+
-O2
127+
-Wall
128+
-fPIC
129+
)
130+
endif()
107131

108132
# Platform-specific settings
109133
if(APPLE)

c_src/py_callback.c

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,44 @@
8383
* @note This file is included from py_nif.c (single compilation unit)
8484
*/
8585

86+
/* ============================================================================
87+
* Cached Python Function References
88+
*
89+
* Cache frequently-used Python functions to avoid repeated module import
90+
* and attribute lookup overhead on every callback.
91+
* ============================================================================ */
92+
93+
/** @brief Cached reference to ast.literal_eval function */
94+
static PyObject *g_ast_literal_eval = NULL;
95+
96+
/**
97+
* @brief Initialize cached Python function references
98+
*
99+
* Called during module initialization. Must be called with GIL held.
100+
*/
101+
static void init_callback_cache(void) {
102+
if (g_ast_literal_eval == NULL) {
103+
PyObject *ast_mod = PyImport_ImportModule("ast");
104+
if (ast_mod != NULL) {
105+
g_ast_literal_eval = PyObject_GetAttrString(ast_mod, "literal_eval");
106+
Py_DECREF(ast_mod);
107+
}
108+
if (g_ast_literal_eval == NULL) {
109+
PyErr_Clear(); /* Non-fatal if unavailable */
110+
}
111+
}
112+
}
113+
114+
/**
115+
* @brief Cleanup cached Python function references
116+
*
117+
* Called during module cleanup. Must be called with GIL held.
118+
*/
119+
static void cleanup_callback_cache(void) {
120+
Py_XDECREF(g_ast_literal_eval);
121+
g_ast_literal_eval = NULL;
122+
}
123+
86124
/* ============================================================================
87125
* Callback Name Registry
88126
*
@@ -580,24 +618,18 @@ static PyObject *parse_callback_response(unsigned char *response_data, size_t re
580618

581619
PyObject *result = NULL;
582620
if (status == 0) {
583-
/* Try to evaluate the result string as Python literal */
584-
PyObject *ast_module = PyImport_ImportModule("ast");
585-
if (ast_module != NULL) {
586-
PyObject *literal_eval = PyObject_GetAttrString(ast_module, "literal_eval");
587-
if (literal_eval != NULL) {
588-
PyObject *arg = PyUnicode_FromStringAndSize(result_str, result_len);
589-
if (arg != NULL) {
590-
result = PyObject_CallFunctionObjArgs(literal_eval, arg, NULL);
591-
Py_DECREF(arg);
592-
if (result == NULL) {
593-
/* If literal_eval fails, return as string */
594-
PyErr_Clear();
595-
result = PyUnicode_FromStringAndSize(result_str, result_len);
596-
}
621+
/* Try to evaluate the result string as Python literal using cached function */
622+
if (g_ast_literal_eval != NULL) {
623+
PyObject *arg = PyUnicode_FromStringAndSize(result_str, result_len);
624+
if (arg != NULL) {
625+
result = PyObject_CallFunctionObjArgs(g_ast_literal_eval, arg, NULL);
626+
Py_DECREF(arg);
627+
if (result == NULL) {
628+
/* If literal_eval fails, return as string */
629+
PyErr_Clear();
630+
result = PyUnicode_FromStringAndSize(result_str, result_len);
597631
}
598-
Py_DECREF(literal_eval);
599632
}
600-
Py_DECREF(ast_module);
601633
}
602634
if (result == NULL) {
603635
result = PyUnicode_FromStringAndSize(result_str, result_len);
@@ -1291,6 +1323,9 @@ static struct PyModuleDef ErlangModuleDef = {
12911323
* Called during Python initialization.
12921324
*/
12931325
static int create_erlang_module(void) {
1326+
/* Initialize cached Python function references */
1327+
init_callback_cache();
1328+
12941329
/* Initialize ErlangFunction type */
12951330
if (PyType_Ready(&ErlangFunctionType) < 0) {
12961331
return -1;

c_src/py_event_loop.c

Lines changed: 139 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -785,6 +785,9 @@ ERL_NIF_TERM nif_poll_events(ErlNifEnv *env, int argc,
785785
return enif_make_tuple2(env, ATOM_OK, enif_make_int(env, num_events));
786786
}
787787

788+
/* Forward declaration for hash set clear function (defined with other hash functions) */
789+
static inline void pending_hash_clear(erlang_event_loop_t *loop);
790+
788791
/**
789792
* get_pending(LoopRef) -> [{CallbackId, Type}]
790793
*
@@ -800,10 +803,28 @@ ERL_NIF_TERM nif_get_pending(ErlNifEnv *env, int argc,
800803
return enif_make_list(env, 0);
801804
}
802805

806+
/*
807+
* Phase 1: Detach pending list under lock (fast - just pointer swap)
808+
* This minimizes lock contention by doing minimal work under the mutex.
809+
*/
803810
pthread_mutex_lock(&loop->mutex);
804811

812+
pending_event_t *snapshot_head = loop->pending_head;
813+
loop->pending_head = NULL;
814+
loop->pending_tail = NULL;
815+
atomic_store(&loop->pending_count, 0);
816+
817+
/* Clear the hash set since we're consuming all pending events */
818+
pending_hash_clear(loop);
819+
820+
pthread_mutex_unlock(&loop->mutex);
821+
822+
/*
823+
* Phase 2: Build Erlang list outside lock (no contention)
824+
* Term creation and memory operations happen without holding the mutex.
825+
*/
805826
ERL_NIF_TERM list = enif_make_list(env, 0);
806-
pending_event_t *current = loop->pending_head;
827+
pending_event_t *current = snapshot_head;
807828

808829
while (current != NULL) {
809830
ERL_NIF_TERM type_atom;
@@ -833,12 +854,6 @@ ERL_NIF_TERM nif_get_pending(ErlNifEnv *env, int argc,
833854
current = next;
834855
}
835856

836-
loop->pending_head = NULL;
837-
loop->pending_tail = NULL;
838-
atomic_store(&loop->pending_count, 0);
839-
840-
pthread_mutex_unlock(&loop->mutex);
841-
842857
/* Reverse the list to maintain order */
843858
ERL_NIF_TERM reversed = enif_make_list(env, 0);
844859
ERL_NIF_TERM head;
@@ -1033,19 +1048,114 @@ static inline void return_pending_event(erlang_event_loop_t *loop,
10331048
}
10341049
}
10351050

1051+
/* ============================================================================
1052+
* Pending Event Hash Set (O(1) duplicate detection)
1053+
*
1054+
* Uses open addressing with linear probing. Key is (callback_id, type)
1055+
* combined into a single uint64_t.
1056+
* ============================================================================ */
1057+
1058+
/**
1059+
* @brief Compute hash key from callback_id and event type
1060+
*/
1061+
static inline uint64_t pending_hash_key(uint64_t callback_id, event_type_t type) {
1062+
/* Combine callback_id and type into a single key */
1063+
return (callback_id << 2) | (uint64_t)type;
1064+
}
1065+
1066+
/**
1067+
* @brief Compute hash bucket index
1068+
*/
1069+
static inline uint32_t pending_hash_index(uint64_t key) {
1070+
/* Simple hash: XOR fold and modulo */
1071+
return (uint32_t)((key ^ (key >> 32)) % PENDING_HASH_SIZE);
1072+
}
1073+
1074+
/**
1075+
* @brief Check if a (callback_id, type) pair exists in the hash set
1076+
*
1077+
* @param loop Event loop containing the hash set
1078+
* @param callback_id Callback ID to check
1079+
* @param type Event type to check
1080+
* @return true if exists, false otherwise
1081+
*/
1082+
static inline bool pending_hash_contains(erlang_event_loop_t *loop,
1083+
uint64_t callback_id, event_type_t type) {
1084+
if (loop->pending_hash_count == 0) {
1085+
return false;
1086+
}
1087+
1088+
uint64_t key = pending_hash_key(callback_id, type);
1089+
uint32_t idx = pending_hash_index(key);
1090+
1091+
/* Linear probing */
1092+
for (int i = 0; i < PENDING_HASH_SIZE; i++) {
1093+
uint32_t probe = (idx + i) % PENDING_HASH_SIZE;
1094+
if (!loop->pending_hash_occupied[probe]) {
1095+
return false; /* Empty slot means key not present */
1096+
}
1097+
if (loop->pending_hash_keys[probe] == key) {
1098+
return true;
1099+
}
1100+
}
1101+
return false; /* Table full, key not found */
1102+
}
1103+
1104+
/**
1105+
* @brief Insert a (callback_id, type) pair into the hash set
1106+
*
1107+
* @param loop Event loop containing the hash set
1108+
* @param callback_id Callback ID to insert
1109+
* @param type Event type to insert
1110+
* @return true if inserted, false if already exists or table full
1111+
*/
1112+
static inline bool pending_hash_insert(erlang_event_loop_t *loop,
1113+
uint64_t callback_id, event_type_t type) {
1114+
/* Don't insert if table is too full (load factor > 0.75) */
1115+
if (loop->pending_hash_count >= (PENDING_HASH_SIZE * 3) / 4) {
1116+
return false;
1117+
}
1118+
1119+
uint64_t key = pending_hash_key(callback_id, type);
1120+
uint32_t idx = pending_hash_index(key);
1121+
1122+
/* Linear probing */
1123+
for (int i = 0; i < PENDING_HASH_SIZE; i++) {
1124+
uint32_t probe = (idx + i) % PENDING_HASH_SIZE;
1125+
if (!loop->pending_hash_occupied[probe]) {
1126+
loop->pending_hash_keys[probe] = key;
1127+
loop->pending_hash_occupied[probe] = true;
1128+
loop->pending_hash_count++;
1129+
return true;
1130+
}
1131+
if (loop->pending_hash_keys[probe] == key) {
1132+
return false; /* Already exists */
1133+
}
1134+
}
1135+
return false; /* Table full */
1136+
}
1137+
1138+
/**
1139+
* @brief Clear the pending hash set
1140+
*
1141+
* @param loop Event loop containing the hash set
1142+
*/
1143+
static inline void pending_hash_clear(erlang_event_loop_t *loop) {
1144+
if (loop->pending_hash_count > 0) {
1145+
memset(loop->pending_hash_occupied, 0, sizeof(loop->pending_hash_occupied));
1146+
loop->pending_hash_count = 0;
1147+
}
1148+
}
1149+
10361150
void event_loop_add_pending(erlang_event_loop_t *loop, event_type_t type,
10371151
uint64_t callback_id, int fd) {
10381152
pthread_mutex_lock(&loop->mutex);
10391153

1040-
/* Check for duplicate - don't add if same callback_id already pending */
1041-
pending_event_t *current = loop->pending_head;
1042-
while (current != NULL) {
1043-
if (current->callback_id == callback_id && current->type == type) {
1044-
/* Already have this event pending, skip */
1045-
pthread_mutex_unlock(&loop->mutex);
1046-
return;
1047-
}
1048-
current = current->next;
1154+
/* O(1) duplicate check using hash set */
1155+
if (pending_hash_contains(loop, callback_id, type)) {
1156+
/* Already have this event pending, skip */
1157+
pthread_mutex_unlock(&loop->mutex);
1158+
return;
10491159
}
10501160

10511161
/* Get event from freelist or allocate new (Phase 7 optimization) */
@@ -1068,6 +1178,9 @@ void event_loop_add_pending(erlang_event_loop_t *loop, event_type_t type,
10681178
loop->pending_tail = event;
10691179
}
10701180

1181+
/* Add to hash set for future O(1) duplicate checks */
1182+
pending_hash_insert(loop, callback_id, type);
1183+
10711184
atomic_fetch_add(&loop->pending_count, 1);
10721185
pthread_cond_signal(&loop->event_cond);
10731186

@@ -1089,6 +1202,9 @@ void event_loop_clear_pending(erlang_event_loop_t *loop) {
10891202
loop->pending_tail = NULL;
10901203
atomic_store(&loop->pending_count, 0);
10911204

1205+
/* Clear the hash set since all pending events are cleared */
1206+
pending_hash_clear(loop);
1207+
10921208
pthread_mutex_unlock(&loop->mutex);
10931209
}
10941210

@@ -2084,6 +2200,9 @@ static PyObject *py_get_pending(PyObject *self, PyObject *args) {
20842200
loop->pending_tail = NULL;
20852201
atomic_store(&loop->pending_count, 0);
20862202

2203+
/* Clear the hash set since we're consuming all pending events */
2204+
pending_hash_clear(loop);
2205+
20872206
pthread_mutex_unlock(&loop->mutex);
20882207

20892208
return list;
@@ -2455,6 +2574,7 @@ static PyObject *py_run_once(PyObject *self, PyObject *args) {
24552574
loop->pending_head = NULL;
24562575
loop->pending_tail = NULL;
24572576
atomic_store(&loop->pending_count, 0);
2577+
pending_hash_clear(loop);
24582578
pthread_mutex_unlock(&loop->mutex);
24592579
return NULL;
24602580
}
@@ -2477,6 +2597,9 @@ static PyObject *py_run_once(PyObject *self, PyObject *args) {
24772597
loop->pending_tail = NULL;
24782598
atomic_store(&loop->pending_count, 0);
24792599

2600+
/* Clear the hash set since we're consuming all pending events */
2601+
pending_hash_clear(loop);
2602+
24802603
pthread_mutex_unlock(&loop->mutex);
24812604

24822605
return list;

c_src/py_event_loop.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
/** @brief Maximum events to keep in freelist (Phase 7 optimization) */
5050
#define EVENT_FREELIST_SIZE 256
5151

52+
/** @brief Size of pending event hash set for O(1) duplicate detection */
53+
#define PENDING_HASH_SIZE 128
54+
5255
/** @brief Event types for pending callbacks */
5356
typedef enum {
5457
EVENT_TYPE_READ = 1,
@@ -210,6 +213,22 @@ typedef struct erlang_event_loop {
210213

211214
/** @brief Number of events currently in freelist */
212215
int freelist_count;
216+
217+
/* ========== O(1) Duplicate Detection Hash Set ========== */
218+
219+
/**
220+
* @brief Hash set for O(1) duplicate pending event detection
221+
*
222+
* Key: (callback_id, type) combined into a single uint64_t
223+
* Uses open addressing with linear probing.
224+
*/
225+
uint64_t pending_hash_keys[PENDING_HASH_SIZE];
226+
227+
/** @brief Occupancy flags for hash set slots */
228+
bool pending_hash_occupied[PENDING_HASH_SIZE];
229+
230+
/** @brief Count of occupied slots in hash set */
231+
int pending_hash_count;
213232
} erlang_event_loop_t;
214233

215234
/* ============================================================================

0 commit comments

Comments
 (0)