mirror of
https://github.com/pgvector/pgvector.git
synced 2026-07-27 22:32:42 +08:00
Use LWLocks instead of SpinLocks (#410)
Spinlocks should be held only for a few instructions, for multiple reasons: - You have to be very careful not to elog() out while holding a spinlock, because there is no mechanism to release the spinlock on error. - Waiters can waste a lot of cycles spinning if the lock is contended. I you wait on a spinlock for too long, the PostgreSQL implementation will actually PANIC, see s_lock_stuck(). The flushLock is particularly problematic. It is held in exclusive mode, which means it holds a spinlock, over the call to FlushPages(). FlushPages() performs lots of I/O so it can take a very long time (>= minutes), and can also easily error out for various reasons. allocatorLock would perhaps be OK as a spinlocks, but even that feels a bit heavy, so I converted that to an LWLock, too. entryLock is usually held for a very short time, in shared mode, so that would be fine as a spinlock. However, in the rare case that the entry point is updated, it's held for a very long time. An LWLock used in shared mode is about as fast a spinlock, that path is pretty heavily optimized. I think we have some problems with the per-element spinlocks too. In HnswUpdateNeighborPagesInMemory(), it's held over a call to HnswUpdateConnection(), but HnswUpdateConnection() can error out at least in case of an out-of-memory error (it uses lappend(), which calls palloc()). It also calls the distance function, and I don't think they are guaranteed to be ereport-free either. However, I didn't address that in this PR, it needs a bit more thinking.
This commit is contained in:
committed by
GitHub
parent
fa0acbf62d
commit
719b4b7436
36
src/hnsw.c
36
src/hnsw.c
@@ -16,12 +16,48 @@
|
||||
int hnsw_ef_search;
|
||||
static relopt_kind hnsw_relopt_kind;
|
||||
|
||||
int entryLockTrancheId;
|
||||
int allocatorLockTrancheId;
|
||||
int flushLockTrancheId;
|
||||
|
||||
/*
|
||||
* Initialize index options and variables
|
||||
*/
|
||||
void
|
||||
HnswInit(void)
|
||||
{
|
||||
int *tranche_ids;
|
||||
bool found;
|
||||
|
||||
/*
|
||||
* Assign tranche IDs for our LWLocks. This only needs to be done by one
|
||||
* backend, the tranche IDs are remembered in shared memory.
|
||||
*
|
||||
* This shared memory area is very small, so we just allocate it from the
|
||||
* "slop" that PostgreSQL reserves for small allocations like this. If
|
||||
* this grows bigger, we should use a shmem_request_hook and
|
||||
* RequestAddinShmemSpace() to pre-reserve space for this.
|
||||
*/
|
||||
LWLockAcquire(AddinShmemInitLock, LW_EXCLUSIVE);
|
||||
tranche_ids = ShmemInitStruct("pgvector LWLock ids",
|
||||
sizeof(int) * 3,
|
||||
&found);
|
||||
if (!found)
|
||||
{
|
||||
tranche_ids[0] = LWLockNewTrancheId();
|
||||
tranche_ids[1] = LWLockNewTrancheId();
|
||||
tranche_ids[2] = LWLockNewTrancheId();
|
||||
}
|
||||
entryLockTrancheId = tranche_ids[0];
|
||||
allocatorLockTrancheId = tranche_ids[1];
|
||||
flushLockTrancheId = tranche_ids[2];
|
||||
LWLockRelease(AddinShmemInitLock);
|
||||
|
||||
/* Per-backend registration of the tranche IDs */
|
||||
LWLockRegisterTranche(entryLockTrancheId, "pgvector entryLock");
|
||||
LWLockRegisterTranche(allocatorLockTrancheId, "pgvector allocatorLock");
|
||||
LWLockRegisterTranche(flushLockTrancheId, "pgvector flushLock");
|
||||
|
||||
hnsw_relopt_kind = add_reloption_kind();
|
||||
add_int_reloption(hnsw_relopt_kind, "m", "Max number of connections",
|
||||
HNSW_DEFAULT_M, HNSW_MIN_M, HNSW_MAX_M
|
||||
|
||||
Reference in New Issue
Block a user