@@ -42,6 +42,14 @@ class BM_VecSimBasics : public BM_VecSimCommon<index_type_t> {
4242 static void Range_BF (benchmark::State &st);
4343 static void Range_HNSW (benchmark::State &st);
4444
45+ // Reproduces allocation/deallocation oscillation issue at block size boundaries.
46+ // Sets up index at blockSize+1 capacity, then repeatedly deletes and re-adds the same vector,
47+ // triggering constant grow-shrink cycles.
48+ // This behavior was fixed by PR #753 with a conservative resize strategy that only
49+ // shrinks containers when there are 2+ free blocks, preventing oscillation cycles.
50+ // Expected: High allocation overhead before fix, stable performance after fix.
51+ static void UpdateAtBlockSize (benchmark::State &st);
52+
4553private:
4654 // Vectors of vector to store deleted labels' data.
4755 using LabelData = std::vector<std::vector<data_t >>;
@@ -76,7 +84,9 @@ void BM_VecSimBasics<index_type_t>::AddLabel(benchmark::State &st) {
7684 // For tiered index, wait for all threads to finish indexing
7785 BM_VecSimGeneral::mock_thread_pool->thread_pool_wait ();
7886
79- st.counters [" memory_per_vector" ] = (double )memory_delta / (double )added_vec_count;
87+ st.counters [" memory_per_vector" ] =
88+ benchmark::Counter ((double )memory_delta / (double )added_vec_count,
89+ benchmark::Counter::kDefaults , benchmark::Counter::OneK::kIs1024 );
8090 st.counters [" vectors_per_label" ] = vec_per_label;
8191
8292 assert (VecSimIndex_IndexSize (index) == N_VECTORS + added_vec_count);
@@ -123,7 +133,9 @@ void BM_VecSimBasics<index_type_t>::AddLabel_AsyncIngest(benchmark::State &st) {
123133 }
124134
125135 size_t memory_delta = index->getAllocationSize () - memory_before;
126- st.counters [" memory_per_vector" ] = (double )memory_delta / (double )added_vec_count;
136+ st.counters [" memory_per_vector" ] =
137+ benchmark::Counter ((double )memory_delta / (double )added_vec_count,
138+ benchmark::Counter::kDefaults , benchmark::Counter::OneK::kIs1024 );
127139 st.counters [" vectors_per_label" ] = vec_per_label;
128140 st.counters [" num_threads" ] = BM_VecSimGeneral::mock_thread_pool->thread_pool_size ;
129141
@@ -173,7 +185,9 @@ void BM_VecSimBasics<index_type_t>::DeleteLabel(algo_t *index, benchmark::State
173185 if (VecSimIndex_BasicInfo (index).algo == VecSimAlgo_TIERED) {
174186 dynamic_cast <TieredHNSWIndex<data_t , dist_t > *>(index)->executeReadySwapJobs ();
175187 }
176- st.counters [" memory_per_vector" ] = memory_delta / (double )removed_vectors_count;
188+ st.counters [" memory_per_vector" ] =
189+ benchmark::Counter ((double )memory_delta / (double )removed_vectors_count,
190+ benchmark::Counter::kDefaults , benchmark::Counter::OneK::kIs1024 );
177191
178192 // Restore index state.
179193 // For each label in removed_labels_data
@@ -223,7 +237,10 @@ void BM_VecSimBasics<index_type_t>::DeleteLabel_AsyncRepair(benchmark::State &st
223237 // Avg. memory delta per vector equals the total memory delta divided by the number
224238 // of deleted vectors.
225239 double memory_delta = tiered_index->getAllocationSize () - memory_before;
226- st.counters [" memory_per_vector" ] = memory_delta / (double )removed_vectors_count;
240+
241+ st.counters [" memory_per_vector" ] =
242+ benchmark::Counter ((double )memory_delta / (double )removed_vectors_count,
243+ benchmark::Counter::kDefaults , benchmark::Counter::OneK::kIs1024 );
227244 st.counters [" num_threads" ] = (double )BM_VecSimGeneral::mock_thread_pool->thread_pool_size ;
228245 st.counters [" num_zombies" ] = tiered_index->idToSwapJob .size ();
229246
@@ -294,6 +311,69 @@ void BM_VecSimBasics<index_type_t>::Range_HNSW(benchmark::State &st) {
294311 st.counters [" Recall" ] = (float )total_res / total_res_bf;
295312}
296313
314+ template <typename index_type_t >
315+ void BM_VecSimBasics<index_type_t >::UpdateAtBlockSize(benchmark::State &st) {
316+ auto index = GET_INDEX (st.range (0 ));
317+ size_t initial_index_size = VecSimIndex_IndexSize (index);
318+ // Calculate vectors needed to reach next block boundary
319+ size_t vecs_to_blocksize =
320+ BM_VecSimGeneral::block_size - (initial_index_size % BM_VecSimGeneral::block_size);
321+ assert (vecs_to_blocksize < BM_VecSimGeneral::block_size);
322+ labelType initial_label_count = index->indexLabelCount ();
323+ labelType curr_label = initial_label_count;
324+
325+ // Set up index at blockSize+1 to trigger oscillation issue
326+ // Make sure we have enough queries to add a new label.
327+ assert (N_QUERIES > BM_VecSimGeneral::block_size);
328+ size_t overhead = 1 ;
329+ size_t added_vec_count = vecs_to_blocksize + overhead;
330+ for (size_t i = 0 ; i < added_vec_count; ++i) {
331+ VecSimIndex_AddVector (index, QUERIES[added_vec_count % N_QUERIES].data (), curr_label++);
332+ }
333+ // For tiered index, wait for all threads to finish indexing
334+ BM_VecSimGeneral::mock_thread_pool->thread_pool_wait ();
335+ assert (VecSimIndex_IndexSize (index) % BM_VecSimGeneral::block_size == overhead);
336+ assert (VecSimIndex_IndexSize (index) == N_VECTORS + added_vec_count);
337+
338+ std::cout << " Added " << added_vec_count << " vectors to reach block size boundary."
339+ << std::endl;
340+ std::cout << " Index size is now " << VecSimIndex_IndexSize (index) << std::endl;
341+ std::cout << " Last label is " << curr_label - 1 << std::endl;
342+
343+ // Benchmark loop: repeatedly delete/add same vector to trigger grow-shrink cycles
344+ labelType label_to_update = curr_label - 1 ;
345+ size_t index_cap = index->indexCapacity ();
346+ for (auto _ : st) {
347+ // Remove the vector directly from hnsw
348+ size_t ret = VecSimIndex_DeleteVector (
349+ GET_INDEX (st.range (0 ) == INDEX_TIERED_HNSW ? INDEX_HNSW : st.range (0 )),
350+ label_to_update);
351+ assert (ret == 1 );
352+ assert (index->indexCapacity () == index_cap - BM_VecSimGeneral::block_size);
353+ // Capacity should shrink by one block after deletion
354+ ret = VecSimIndex_AddVector (index, QUERIES[(added_vec_count - 1 ) % N_QUERIES].data (),
355+ label_to_update);
356+ assert (ret == 1 );
357+ BM_VecSimGeneral::mock_thread_pool->thread_pool_wait ();
358+ assert (VecSimIndex_IndexSize (
359+ GET_INDEX (st.range (0 ) == INDEX_TIERED_HNSW ? INDEX_HNSW : st.range (0 ))) ==
360+ N_VECTORS + added_vec_count);
361+ // Capacity should grow back to original size after addition
362+ assert (index->indexCapacity () == index_cap);
363+ }
364+ assert (VecSimIndex_IndexSize (index) == N_VECTORS + added_vec_count);
365+
366+ // Clean-up all the new vectors to restore the index size to its original value.
367+
368+ size_t new_label_count = index->indexLabelCount ();
369+ for (size_t label = initial_label_count; label < new_label_count; label++) {
370+ // If index is tiered HNSW, remove directly from the underline HNSW.
371+ VecSimIndex_DeleteVector (
372+ GET_INDEX (st.range (0 ) == INDEX_TIERED_HNSW ? INDEX_HNSW : st.range (0 )), label);
373+ }
374+ assert (VecSimIndex_IndexSize (index) == N_VECTORS);
375+ }
376+
297377#define UNIT_AND_ITERATIONS Unit (benchmark::kMillisecond )->Iterations(BM_VecSimGeneral::block_size)
298378
299379// These macros are used to make sure the expansion of other macros happens when needed
@@ -345,3 +425,8 @@ void BM_VecSimBasics<index_type_t>::Range_HNSW(benchmark::State &st) {
345425 }
346426#define REGISTER_DeleteLabel (BM_FUNC ) \
347427 BENCHMARK_REGISTER_F (BM_VecSimBasics, BM_FUNC)->UNIT_AND_ITERATIONS
428+
429+ #define REGISTER_UpdateAtBlockSize (BM_FUNC, VecSimAlgo ) \
430+ BENCHMARK_REGISTER_F (BM_VecSimBasics, BM_FUNC) \
431+ ->UNIT_AND_ITERATIONS->Arg(VecSimAlgo) \
432+ ->ArgName(#VecSimAlgo)
0 commit comments