Skip to content

Commit 7079d27

Browse files
authored
[Perf] Release the GIL in compute-intensive function (#60)
* Release GIL in `c_process_trace` * Move GIL position
1 parent b1c6ab7 commit 7079d27

2 files changed

Lines changed: 68 additions & 6 deletions

File tree

src/export_cache.cpp

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ cache_t* pypluginCache_init(
107107
py::function cache_init_hook, py::function cache_hit_hook,
108108
py::function cache_miss_hook, py::function cache_eviction_hook,
109109
py::function cache_remove_hook, py::function cache_free_hook) {
110+
py::gil_scoped_acquire acquire;
110111
// Initialize base cache structure with exception safety
111112
cache_t* cache = nullptr;
112113
std::unique_ptr<pypluginCache_params_t, PypluginCacheParamsDeleter> params;
@@ -163,20 +164,28 @@ cache_t* pypluginCache_init(
163164
}
164165

165166
static void pypluginCache_free(cache_t* cache) {
166-
if (!cache || !cache->eviction_params) {
167+
if (!cache) {
168+
return;
169+
}
170+
py::gil_scoped_acquire acquire;
171+
if (!cache->eviction_params) {
172+
// No params, just free the cache structure
173+
cache_struct_free(cache);
167174
return;
168175
}
169176

170-
// Use smart pointer for automatic cleanup
171-
std::unique_ptr<pypluginCache_params_t, PypluginCacheParamsDeleter> params(
172-
static_cast<pypluginCache_params_t*>(cache->eviction_params));
173-
177+
auto* raw_params = static_cast<pypluginCache_params_t*>(cache->eviction_params);
178+
cache->eviction_params = nullptr;
174179
// The smart pointer destructor will handle cleanup automatically
180+
std::unique_ptr<pypluginCache_params_t, PypluginCacheParamsDeleter> params(raw_params);
181+
params.reset();
182+
175183
cache_struct_free(cache);
176184
}
177185

178186
static bool pypluginCache_get(cache_t* cache, const request_t* req) {
179187
bool hit = cache_get_base(cache, req);
188+
py::gil_scoped_acquire acquire;
180189
pypluginCache_params_t* params =
181190
(pypluginCache_params_t*)cache->eviction_params;
182191

@@ -204,6 +213,7 @@ static cache_obj_t* pypluginCache_to_evict(cache_t* cache,
204213
}
205214

206215
static void pypluginCache_evict(cache_t* cache, const request_t* req) {
216+
py::gil_scoped_acquire acquire;
207217
pypluginCache_params_t* params =
208218
(pypluginCache_params_t*)cache->eviction_params;
209219

@@ -223,6 +233,7 @@ static void pypluginCache_evict(cache_t* cache, const request_t* req) {
223233
}
224234

225235
static bool pypluginCache_remove(cache_t* cache, const obj_id_t obj_id) {
236+
py::gil_scoped_acquire acquire;
226237
pypluginCache_params_t* params =
227238
(pypluginCache_params_t*)cache->eviction_params;
228239

@@ -568,7 +579,8 @@ void export_cache(py::module& m) {
568579
bytes_req > 0 ? 1.0 - (double)bytes_hit / bytes_req : 0.0;
569580
return std::make_tuple(obj_miss_ratio, byte_miss_ratio);
570581
},
571-
"cache"_a, "reader"_a, "start_req"_a = 0, "max_req"_a = -1);
582+
"cache"_a, "reader"_a, "start_req"_a = 0, "max_req"_a = -1,
583+
py::call_guard<py::gil_scoped_release>());
572584
}
573585

574586
} // namespace libcachesim

tests/test_gil.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pytest
2+
import libcachesim as lcs
3+
import threading
4+
import time
5+
6+
S3_URI = "s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst"
7+
8+
def run_heavy_simulation(name):
9+
# Create a large synthetic trace
10+
reader = lcs.TraceReader(trace=S3_URI)
11+
cache = lcs.LRU(cache_size=1024*1024)
12+
13+
print(f"Thread {name} starting simulation...")
14+
start = time.time()
15+
# Call C++ core logic
16+
lcs.Util.process_trace(cache, reader)
17+
end = time.time()
18+
print(f"Thread {name} completed in {end - start:.2f}s")
19+
20+
def test_gil_release():
21+
"""
22+
Test to verify that the GIL is released during heavy C++ processing.
23+
We run two threads that perform heavy simulations and measure total time.
24+
If the total time is close to the single-thread time, it indicates GIL release.
25+
If the total time is close to double the single-thread time, it indicates GIL is still held.
26+
"""
27+
# --- Experiment start ---
28+
29+
# test single-thread time for reference
30+
start_single = time.time()
31+
run_heavy_simulation("Single")
32+
end_single = time.time()
33+
single_thread_time = end_single - start_single
34+
print(f"\nSingle-thread time: {single_thread_time:.2f}s")
35+
36+
start_total = time.time()
37+
38+
t1 = threading.Thread(target=run_heavy_simulation, args=("A",))
39+
t2 = threading.Thread(target=run_heavy_simulation, args=("B",))
40+
41+
t1.start()
42+
t2.start()
43+
44+
t1.join()
45+
t2.join()
46+
47+
end_total = time.time()
48+
print(f"\nTotal elapsed time: {end_total - start_total:.2f}s")
49+
50+
assert single_thread_time * 1.5 > (end_total - start_total), "GIL release test failed: Total time should be close to single-thread time if GIL is released."

0 commit comments

Comments
 (0)