@@ -32,6 +32,14 @@ class BM_VecSimBasics : public BM_VecSimCommon<index_type_t> {
3232 static void Range_BF (benchmark::State &st);
3333 static void Range_HNSW (benchmark::State &st);
3434
35+ // Reproduces allocation/deallocation oscillation issue at block size boundaries.
36+ // Sets up index at blockSize+1 capacity, then repeatedly deletes and re-adds the same vector,
37+ // triggering constant grow-shrink cycles.
38+ // This behavior was fixed by PR #753 with a conservative resize strategy that only
39+ // shrinks containers when there are 2+ free blocks, preventing oscillation cycles.
40+ // Expected: High allocation overhead before fix, stable performance after fix.
41+ static void UpdateAtBlockSize (benchmark::State &st);
42+
3543private:
3644 // Vectors of vector to store deleted labels' data.
3745 using LabelData = std::vector<std::vector<data_t >>;
@@ -66,7 +74,9 @@ void BM_VecSimBasics<index_type_t>::AddLabel(benchmark::State &st) {
6674 // For tiered index, wait for all threads to finish indexing
6775 BM_VecSimGeneral::mock_thread_pool.thread_pool_wait ();
6876
69- st.counters [" memory_per_vector" ] = (double )memory_delta / (double )added_vec_count;
77+ st.counters [" memory_per_vector" ] =
78+ benchmark::Counter ((double )memory_delta / (double )added_vec_count,
79+ benchmark::Counter::kDefaults , benchmark::Counter::OneK::kIs1024 );
7080 st.counters [" vectors_per_label" ] = vec_per_label;
7181
7282 assert (VecSimIndex_IndexSize (index) == N_VECTORS + added_vec_count);
@@ -114,7 +124,9 @@ void BM_VecSimBasics<index_type_t>::AddLabel_AsyncIngest(benchmark::State &st) {
114124 }
115125
116126 size_t memory_delta = index->getAllocationSize () - memory_before;
117- st.counters [" memory_per_vector" ] = (double )memory_delta / (double )added_vec_count;
127+ st.counters [" memory_per_vector" ] =
128+ benchmark::Counter ((double )memory_delta / (double )added_vec_count,
129+ benchmark::Counter::kDefaults , benchmark::Counter::OneK::kIs1024 );
118130 st.counters [" vectors_per_label" ] = vec_per_label;
119131 st.counters [" num_threads" ] = BM_VecSimGeneral::mock_thread_pool.thread_pool_size ;
120132
@@ -164,7 +176,9 @@ void BM_VecSimBasics<index_type_t>::DeleteLabel(algo_t *index, benchmark::State
164176 if (VecSimIndex_BasicInfo (index).algo == VecSimAlgo_TIERED) {
165177 dynamic_cast <TieredHNSWIndex<data_t , dist_t > *>(index)->executeReadySwapJobs ();
166178 }
167- st.counters [" memory_per_vector" ] = memory_delta / (double )removed_vectors_count;
179+ st.counters [" memory_per_vector" ] =
180+ benchmark::Counter ((double )memory_delta / (double )removed_vectors_count,
181+ benchmark::Counter::kDefaults , benchmark::Counter::OneK::kIs1024 );
168182
169183 // Restore index state.
170184 // For each label in removed_labels_data
@@ -214,7 +228,10 @@ void BM_VecSimBasics<index_type_t>::DeleteLabel_AsyncRepair(benchmark::State &st
214228 // Avg. memory delta per vector equals the total memory delta divided by the number
215229 // of deleted vectors.
216230 double memory_delta = tiered_index->getAllocationSize () - memory_before;
217- st.counters [" memory_per_vector" ] = memory_delta / (double )removed_vectors_count;
231+
232+ st.counters [" memory_per_vector" ] =
233+ benchmark::Counter ((double )memory_delta / (double )removed_vectors_count,
234+ benchmark::Counter::kDefaults , benchmark::Counter::OneK::kIs1024 );
218235 st.counters [" num_threads" ] = (double )BM_VecSimGeneral::mock_thread_pool.thread_pool_size ;
219236 st.counters [" num_zombies" ] = tiered_index->idToSwapJob .size ();
220237
@@ -286,6 +303,69 @@ void BM_VecSimBasics<index_type_t>::Range_HNSW(benchmark::State &st) {
286303 st.counters [" Recall" ] = (float )total_res / total_res_bf;
287304}
288305
306+ template <typename index_type_t >
307+ void BM_VecSimBasics<index_type_t >::UpdateAtBlockSize(benchmark::State &st) {
308+ auto index = INDICES[st.range (0 )];
309+ size_t initial_index_size = VecSimIndex_IndexSize (index);
310+ // Calculate vectors needed to reach next block boundary
311+ size_t vecs_to_blocksize =
312+ BM_VecSimGeneral::block_size - (initial_index_size % BM_VecSimGeneral::block_size);
313+ assert (vecs_to_blocksize < BM_VecSimGeneral::block_size);
314+ labelType initial_label_count = index->indexLabelCount ();
315+ labelType curr_label = initial_label_count;
316+
317+ // Set up index at blockSize+1 to trigger oscillation issue
318+ // Make sure we have enough queries to add a new label.
319+ assert (N_QUERIES > BM_VecSimGeneral::block_size);
320+ size_t overhead = 1 ;
321+ size_t added_vec_count = vecs_to_blocksize + overhead;
322+ for (size_t i = 0 ; i < added_vec_count; ++i) {
323+ VecSimIndex_AddVector (index, QUERIES[added_vec_count % N_QUERIES].data (), curr_label++);
324+ }
325+ // For tiered index, wait for all threads to finish indexing
326+ BM_VecSimGeneral::mock_thread_pool.thread_pool_wait ();
327+ assert (VecSimIndex_IndexSize (index) % BM_VecSimGeneral::block_size == overhead);
328+ assert (VecSimIndex_IndexSize (index) == N_VECTORS + added_vec_count);
329+
330+ std::cout << " Added " << added_vec_count << " vectors to reach block size boundary."
331+ << std::endl;
332+ std::cout << " Index size is now " << VecSimIndex_IndexSize (index) << std::endl;
333+ std::cout << " Last label is " << curr_label - 1 << std::endl;
334+
335+ // Benchmark loop: repeatedly delete/add same vector to trigger grow-shrink cycles
336+ labelType label_to_update = curr_label - 1 ;
337+ size_t index_cap = index->indexCapacity ();
338+ for (auto _ : st) {
339+ // Remove the vector directly from hnsw
340+ size_t ret = VecSimIndex_DeleteVector (
341+ INDICES[st.range (0 ) == VecSimAlgo_TIERED ? VecSimAlgo_HNSWLIB : st.range (0 )],
342+ label_to_update);
343+ assert (ret == 1 );
344+ assert (index->indexCapacity () == index_cap - BM_VecSimGeneral::block_size);
345+ // Capacity should shrink by one block after deletion
346+ ret = VecSimIndex_AddVector (index, QUERIES[(added_vec_count - 1 ) % N_QUERIES].data (),
347+ label_to_update);
348+ assert (ret == 1 );
349+ BM_VecSimGeneral::mock_thread_pool.thread_pool_wait ();
350+ assert (VecSimIndex_IndexSize (
351+ INDICES[st.range (0 ) == VecSimAlgo_TIERED ? VecSimAlgo_HNSWLIB : st.range (0 )]) ==
352+ N_VECTORS + added_vec_count);
353+ // Capacity should grow back to original size after addition
354+ assert (index->indexCapacity () == index_cap);
355+ }
356+ assert (VecSimIndex_IndexSize (index) == N_VECTORS + added_vec_count);
357+
358+ // Clean-up all the new vectors to restore the index size to its original value.
359+
360+ size_t new_label_count = index->indexLabelCount ();
361+ for (size_t label = initial_label_count; label < new_label_count; label++) {
362+ // If index is tiered HNSW, remove directly from the underline HNSW.
363+ VecSimIndex_DeleteVector (
364+ INDICES[st.range (0 ) == VecSimAlgo_TIERED ? VecSimAlgo_HNSWLIB : st.range (0 )], label);
365+ }
366+ assert (VecSimIndex_IndexSize (index) == N_VECTORS);
367+ }
368+
289369#define UNIT_AND_ITERATIONS Unit (benchmark::kMillisecond )->Iterations(BM_VecSimGeneral::block_size)
290370
291371// The actual radius will be the given arg divided by 100, since arg must be an integer.
@@ -331,3 +411,8 @@ void BM_VecSimBasics<index_type_t>::Range_HNSW(benchmark::State &st) {
331411 }
332412#define REGISTER_DeleteLabel (BM_FUNC ) \
333413 BENCHMARK_REGISTER_F (BM_VecSimBasics, BM_FUNC)->UNIT_AND_ITERATIONS
414+
415+ #define REGISTER_UpdateAtBlockSize (BM_FUNC, VecSimAlgo ) \
416+ BENCHMARK_REGISTER_F (BM_VecSimBasics, BM_FUNC) \
417+ ->UNIT_AND_ITERATIONS->Arg(VecSimAlgo) \
418+ ->ArgName(#VecSimAlgo)
0 commit comments