Skip to content

Refactor no-exceptions load/save after v0.10.0-rc.2 review - #683

Open
ilyajob05 wants to merge 13 commits into
developfrom
f/rc2-review-followups
Open

ilyajob05 wants to merge 13 commits into
developfrom
f/rc2-review-followups

Conversation

@ilyajob05

Copy link
Copy Markdown
Collaborator
  • Drop StreamExceptionsOff / invokeWithoutStreamThrow. *NoExceptions I/O already returns Status via !good() / peek. Callers must leave the default iostream mask (goodbit)
  • Fail-closed load: do not clear() a live HNSW index until the on-disk layout checks out. clear() resets lookup / deleted set / max_elements_. Reload no longer keeps stale labels or deleted_elements
  • BruteforceSearch::loadIndexNoExceptions; throwing loadIndex is a wrapper. Rebuild the label map; memcpy labels; reject size_per_element mismatch vs Space
  • Status copies the message (no dangling stack const char*)
  • CMake: do not FORCE-strip /EHsc from the parent cache. HNSWLIB_ENABLE_EXCEPTIONS is example/test-only; MSVC OFF uses /EHs-c- on the target

The extra RAII class existed only for callers who enable iostream
failbit/badbit exceptions. Default streams do not throw; write/load
failures already return Status via !good() / peek. Restoring the old
mask could itself throw on eofbit.

Keep the Status checks. Document that *NoExceptions I/O assumes the
default goodbit mask.
clear() did not reset label_lookup_, num_deleted_, or deleted_elements,
so a second loadIndex kept stale labels and could replace a live point
after allow_replace_deleted reload.

Validate the stream header and on-disk layout before mutating the live
index; a truncated or corrupt file now returns Status and leaves the
existing graph in place. Bruteforce load rebuilds the label map and
reads labels via memcpy. addPoint rolls back the reserved slot if
link-list allocation fails and we are still the last element.
Throwing loadIndex is now a thin wrapper. Failures return Status without
mutating the live index (unopened/truncated streams). Successful loads
still replace data_ only after a full read and rebuild the label map.
Stacked on the stream-simplify commit, which removed the helper. Keep
the same Status checks as HierarchicalNSW *NoExceptions I/O.
Status copied a borrowed const char*, so a stack buffer or temporary
string became a dangling message(). Copy into std::string.

HNSWLIB_ENABLE_EXCEPTIONS=OFF no longer FORCE-writes CMAKE_CXX_FLAGS_*
without /EHsc, which leaked into parent projects when examples were on.
Exception flags stay per-target. The option does not export -fno-exceptions
on the INTERFACE library.
After a failed loadIndexNoExceptions the object looked empty
(cur_element_count == 0) but kept the incoming capacity with
data_level0_memory_ == nullptr. addPoint then passed the capacity
check and wrote through a null pointer. Also bound the link-list
free loop to element_levels_.size().
Bruteforce already failed closed on that header. HNSW allocated
file_max_elements slots and then read file_cur_count vectors, which
overflows when the two counts disagree. Also reject a zero
size_data_per_element before the layout walk.
element_levels_[cur_c] was already the intended level. On malloc
failure that left a null linkLists_[cur_c] inside the published
range, so saveIndex wrote from nullptr. Always drop the level to 0
first; fully unpublish the slot only when it is still last.
HNSW still builds mutex vectors, VisitedListPool, and label maps after
clear(). Catch std::bad_alloc and reset rather than escaping the
no-exceptions API. Bruteforce rebuilds the label map on the new buffer
before swapping it in, so a map allocation failure leaves the live
index untouched.
Owning the message via std::string fixes dangling stack pointers.
Typical error strings exceed SSO, so -fno-exceptions builds can still
abort while reporting an error. Callers should pass string literals.
The file stores size_per_element but load recomputed it from the Space
and ignored the on-disk value, so a smaller dim still returned OK and
read labels at the wrong packed offset.
CMAKE_CXX_FLAGS already has /EHsc. Per-target OFF only set /GR- and
_HAS_EXCEPTIONS=0, so EH codegen stayed on. /EHs-c- overrides the
default on the target and does not FORCE-write parent flags.
Keep the current invariant: do not mutate the live index until the
on-disk layout has been checked.
Comment thread hnswlib/bruteforce.h
if (file_size_per_element != size_per_element) {
return Status("Cannot load index: size_per_element does not match space");
}
char *new_data = (char *) malloc(file_maxelements * size_per_element);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be preferable to use RAII to manage this allocation.

Comment thread hnswlib/bruteforce.h
}

std::unordered_map<labeltype, size_t> new_dict;
#if defined(__EXCEPTIONS) || _HAS_EXCEPTIONS == 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of repeating defined(__EXCEPTIONS) || _HAS_EXCEPTIONS == 1 in multiple places, we could define a HNSWLIB_... macro, e.g.

In hnswlib.h:

#if defined(__EXCEPTIONS) || _HAS_EXCEPTIONS == 1
#define HNSWLIB_EXCEPTIONS_ENABLED 1
#else
#undef HNSWLIB_EXCEPTIONS_ENABLED
#endif

Then here

#ifdef HNSWLIB_EXCEPTIONS_ENABLED
try {
#endif

etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants