Skip to content

Commit d0fd0d5

Browse files
committed
fix: memory leak in delete
Signed-off-by: aaronzuo <anarionzuo@outlook.com>
1 parent c1b9b79 commit d0fd0d5

File tree

6 files changed

+119
-9
lines changed

6 files changed

+119
-9
lines changed

.github/workflows/build.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,40 @@ jobs:
4747
fi
4848
shell: bash
4949

50+
leak_sanitizers:
51+
runs-on: ubuntu-latest
52+
steps:
53+
- uses: actions/checkout@v3
54+
- uses: actions/setup-python@v4
55+
with:
56+
python-version: "3.11"
57+
58+
- name: Build and install (ASAN/LSAN)
59+
env:
60+
HNSWLIB_NO_NATIVE: "1"
61+
CFLAGS: "-O1 -g -fno-omit-frame-pointer -fsanitize=address,leak"
62+
CXXFLAGS: "-O1 -g -fno-omit-frame-pointer -fsanitize=address,leak"
63+
LDFLAGS: "-fsanitize=address,leak"
64+
run: |
65+
python -m pip install -U pip setuptools wheel
66+
python -m pip install -v --no-build-isolation .
67+
68+
- name: Python leak smoke test (ASAN/LSAN)
69+
timeout-minutes: 15
70+
env:
71+
ASAN_OPTIONS: "detect_leaks=1:halt_on_error=1:alloc_dealloc_mismatch=1"
72+
run: |
73+
python -m unittest discover -v --start-directory tests/python --pattern "bindings_test_leaks.py"
74+
75+
- name: C++ leak smoke test (ASAN/LSAN)
76+
timeout-minutes: 15
77+
env:
78+
ASAN_OPTIONS: "detect_leaks=1:halt_on_error=1:alloc_dealloc_mismatch=1"
79+
run: |
80+
cmake -S . -B build-asan -DHNSWLIB_EXAMPLES=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_FLAGS="-O1 -g -fno-omit-frame-pointer -fsanitize=address,leak -std=c++11" -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,leak"
81+
cmake --build build-asan --target leak_smoke_test -j 2
82+
./build-asan/leak_smoke_test
83+
5084
- name: Prepare test data
5185
run: |
5286
pip install numpy
@@ -77,4 +111,5 @@ jobs:
77111
./test_updates update
78112
./multivector_search_test
79113
./epsilon_search_test
114+
./leak_smoke_test
80115
shell: bash

CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ if(HNSWLIB_EXAMPLES)
100100
add_executable(multiThread_replace_test tests/cpp/multiThread_replace_test.cpp)
101101
target_link_libraries(multiThread_replace_test hnswlib)
102102

103+
add_executable(leak_smoke_test tests/cpp/leak_smoke_test.cpp)
104+
target_link_libraries(leak_smoke_test hnswlib)
105+
103106
add_executable(main tests/cpp/main.cpp tests/cpp/sift_1b.cpp)
104107
target_link_libraries(main hnswlib)
105108
endif()

python_bindings/bindings.cpp

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -392,19 +392,19 @@ class Index {
392392
}
393393

394394
py::capsule free_when_done_l0(data_level0_npy, [](void* f) {
395-
delete[] f;
395+
free(f);
396396
});
397397
py::capsule free_when_done_lvl(element_levels_npy, [](void* f) {
398-
delete[] f;
398+
free(f);
399399
});
400400
py::capsule free_when_done_lb(label_lookup_key_npy, [](void* f) {
401-
delete[] f;
401+
free(f);
402402
});
403403
py::capsule free_when_done_id(label_lookup_val_npy, [](void* f) {
404-
delete[] f;
404+
free(f);
405405
});
406406
py::capsule free_when_done_ll(link_list_npy, [](void* f) {
407-
delete[] f;
407+
free(f);
408408
});
409409

410410
/* TODO: serialize state of random generators appr_alg->level_generator_ and appr_alg->update_probability_generator_ */
@@ -676,10 +676,10 @@ class Index {
676676
}
677677
}
678678
py::capsule free_when_done_l(data_numpy_l, [](void* f) {
679-
delete[] f;
679+
delete[] reinterpret_cast<hnswlib::labeltype*>(f);
680680
});
681681
py::capsule free_when_done_d(data_numpy_d, [](void* f) {
682-
delete[] f;
682+
delete[] reinterpret_cast<dist_t*>(f);
683683
});
684684

685685
return py::make_tuple(
@@ -884,10 +884,10 @@ class BFIndex {
884884
}
885885

886886
py::capsule free_when_done_l(data_numpy_l, [](void *f) {
887-
delete[] f;
887+
delete[] reinterpret_cast<hnswlib::labeltype*>(f);
888888
});
889889
py::capsule free_when_done_d(data_numpy_d, [](void *f) {
890-
delete[] f;
890+
delete[] reinterpret_cast<dist_t*>(f);
891891
});
892892

893893

tests/cpp/leak_smoke_test.cpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#include "assert.h"
2+
#include "../../hnswlib/hnswlib.h"
3+
#include <random>
4+
#include <vector>
5+
6+
int main() {
7+
int dim = 16;
8+
int max_elements = 1000;
9+
int M = 16;
10+
int ef_construction = 200;
11+
12+
std::mt19937 rng(42);
13+
std::uniform_real_distribution<float> distrib(0.0f, 1.0f);
14+
15+
std::vector<float> data(dim * max_elements);
16+
for (size_t i = 0; i < data.size(); i++) {
17+
data[i] = distrib(rng);
18+
}
19+
20+
for (int iter = 0; iter < 5; iter++) {
21+
hnswlib::L2Space space(dim);
22+
hnswlib::HierarchicalNSW<float>* alg_hnsw = new hnswlib::HierarchicalNSW<float>(
23+
&space, max_elements, M, ef_construction, 42 + iter);
24+
25+
for (int i = 0; i < max_elements; i++) {
26+
alg_hnsw->addPoint(data.data() + (i * dim), i);
27+
}
28+
29+
for (int i = 0; i < 50; i++) {
30+
auto result = alg_hnsw->searchKnn(data.data() + (i * dim), 10);
31+
assert(result.size() == 10);
32+
}
33+
34+
delete alg_hnsw;
35+
}
36+
37+
return 0;
38+
}

tests/python/bindings_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def testRandomSelf(self):
4444
# Query the elements for themselves and measure recall:
4545
labels, distances = p.knn_query(data1, k=1)
4646
self.assertAlmostEqual(np.mean(labels.reshape(-1) == np.arange(len(data1))), 1.0, 3)
47+
del labels, distances
4748

4849
# Serializing and deleting the index:
4950
index_path = 'first_half.bin'
@@ -64,5 +65,6 @@ def testRandomSelf(self):
6465
labels, distances = p.knn_query(data, k=1)
6566

6667
self.assertAlmostEqual(np.mean(labels.reshape(-1) == np.arange(len(data))), 1.0, 3)
68+
del labels, distances
6769

6870
os.remove(index_path)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import gc
2+
import pickle
3+
import unittest
4+
5+
import numpy as np
6+
7+
import hnswlib
8+
9+
10+
class LeakSmokeTestCase(unittest.TestCase):
11+
def testLeakSmoke(self):
12+
dim = 8
13+
max_elements = 200
14+
15+
for _ in range(25):
16+
data = np.float32(np.random.random((max_elements, dim)))
17+
18+
p = hnswlib.Index(space='l2', dim=dim)
19+
p.init_index(max_elements=max_elements, ef_construction=100, M=16)
20+
p.add_items(data)
21+
22+
labels, distances = p.knn_query(data[:25], k=5)
23+
del labels, distances
24+
25+
payload = pickle.dumps(p)
26+
del p
27+
gc.collect()
28+
29+
p2 = pickle.loads(payload)
30+
labels2, distances2 = p2.knn_query(data[:10], k=3)
31+
del labels2, distances2, p2, payload, data
32+
gc.collect()

0 commit comments

Comments
 (0)