Skip to content

Commit c4d5cb7

Browse files
committed
Increase the number of fast-path lock slots
Replace the fixed-size array of fast-path locks with arrays, sized on startup based on max_locks_per_transaction. This allows using fast-path locking for workloads that need more locks. The fast-path locking introduced in 9.2 allowed each backend to acquire a small number (16) of weak relation locks cheaply. If a backend needs to hold more locks, it has to insert them into the shared lock table. This is considerably more expensive, and may be subject to contention (especially on many-core systems). The limit of 16 fast-path locks was always rather low, because we have to lock all relations - not just tables, but also indexes, views, etc. For planning we need to lock all relations that might be used in the plan, not just those that actually get used in the final plan. So even with rather simple queries and schemas, we often need significantly more than 16 locks. As partitioning gets used more widely, and the number of partitions increases, this limit is trivial to hit. Complex queries may easily use hundreds or even thousands of locks. For workloads doing a lot of I/O this is not noticeable, but for workloads accessing only data in RAM, the access to the shared lock table may be a serious issue. This commit removes the hard-coded limit of the number of fast-path locks. Instead, the size of the fast-path arrays is calculated at startup, and can be set much higher than the original 16-lock limit. The overall fast-path locking protocol remains unchanged. The variable-sized fast-path arrays can no longer be part of PGPROC, but are allocated as a separate chunk of shared memory and then references from the PGPROC entries. The fast-path slots are organized as a 16-way set associative cache. You can imagine it as a hash table of 16-slot "groups". Each relation is mapped to exactly one group using hash(relid), and the group is then processed using linear search, just like the original fast-path cache. With only 16 entries this is cheap, with good locality. Treating this as a simple hash table with open addressing would not be efficient, especially once the hash table gets almost full. The usual remedy is to grow the table, but we can't do that here easily. The access would also be more random, with worse locality. The fast-path arrays are sized using the max_locks_per_transaction GUC. We try to have enough capacity for the number of locks specified in the GUC, using the traditional 2^n formula, with an upper limit of 1024 lock groups (i.e. 16k locks). The default value of max_locks_per_transaction is 64, which means those instances will have 64 fast-path slots. The main purpose of the max_locks_per_transaction GUC is to size the shared lock table. It is often set to the "average" number of locks needed by backends, with some backends using significantly more locks. This should not be a major issue, however. Some backens may have to insert locks into the shared lock table, but there can't be too many of them, limiting the contention. The only solution is to increase the GUC, even if the shared lock table already has sufficient capacity. That is not free, especially in terms of memory usage (the shared lock table entries are fairly large). It should only happen on machines with plenty of memory, though. In the future we may consider a separate GUC for the number of fast-path slots, but let's try without one first. Reviewed-by: Robert Haas, Jakub Wartak Discussion: https://postgr.es/m/510b887e-c0ce-4a0c-a17a-2c6abb8d9a5c@enterprisedb.com
1 parent b524974 commit c4d5cb7

File tree

9 files changed

+212
-27
lines changed

9 files changed

+212
-27
lines changed

src/backend/bootstrap/bootstrap.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,8 @@ BootstrapModeMain(int argc, char *argv[], bool check_only)
309309

310310
InitializeMaxBackends();
311311

312+
InitializeFastPathLocks();
313+
312314
CreateSharedMemoryAndSemaphores();
313315

314316
/*

src/backend/postmaster/postmaster.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,11 @@ PostmasterMain(int argc, char *argv[])
903903
*/
904904
InitializeMaxBackends();
905905

906+
/*
907+
* Calculate the size of the PGPROC fast-path lock arrays.
908+
*/
909+
InitializeFastPathLocks();
910+
906911
/*
907912
* Give preloaded libraries a chance to request additional shared memory.
908913
*/

src/backend/storage/ipc/ipci.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ AttachSharedMemoryStructs(void)
178178
Assert(MyProc != NULL);
179179
Assert(IsUnderPostmaster);
180180

181+
/*
182+
* In EXEC_BACKEND mode, backends don't inherit the number of fast-path
183+
* groups we calculated before setting the shmem up, so recalculate it.
184+
*/
185+
InitializeFastPathLocks();
186+
181187
CreateOrAttachShmemStructs();
182188

183189
/*

src/backend/storage/lmgr/lock.c

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,13 @@ typedef struct TwoPhaseLockRecord
166166
* might be higher than the real number if another backend has transferred
167167
* our locks to the primary lock table, but it can never be lower than the
168168
* real value, since only we can acquire locks on our own behalf.
169+
*
170+
* XXX Allocate a static array of the maximum size. We could use a pointer
171+
* and then allocate just the right size to save a couple kB, but then we
172+
* would have to initialize that, while for the static array that happens
173+
* automatically. Doesn't seem worth the extra complexity.
169174
*/
170-
static int FastPathLocalUseCount = 0;
175+
static int FastPathLocalUseCounts[FP_LOCK_GROUPS_PER_BACKEND_MAX];
171176

172177
/*
173178
* Flag to indicate if the relation extension lock is held by this backend.
@@ -184,23 +189,68 @@ static int FastPathLocalUseCount = 0;
184189
*/
185190
static bool IsRelationExtensionLockHeld PG_USED_FOR_ASSERTS_ONLY = false;
186191

192+
/*
193+
* Number of fast-path locks per backend - size of the arrays in PGPROC.
194+
* This is set only once during start, before initializing shared memory,
195+
* and remains constant after that.
196+
*
197+
* We set the limit based on max_locks_per_transaction GUC, because that's
198+
* the best information about expected number of locks per backend we have.
199+
* See InitializeFastPathLocks() for details.
200+
*/
201+
int FastPathLockGroupsPerBackend = 0;
202+
203+
/*
204+
* Macros to calculate the fast-path group and index for a relation.
205+
*
206+
* The formula is a simple hash function, designed to spread the OIDs a bit,
207+
* so that even contiguous values end up in different groups. In most cases
208+
* there will be gaps anyway, but the multiplication should help a bit.
209+
*
210+
* The selected constant (49157) is a prime not too close to 2^k, and it's
211+
* small enough to not cause overflows (in 64-bit).
212+
*/
213+
#define FAST_PATH_REL_GROUP(rel) \
214+
(((uint64) (rel) * 49157) % FastPathLockGroupsPerBackend)
215+
216+
/*
217+
* Given the group/slot indexes, calculate the slot index in the whole array
218+
* of fast-path lock slots.
219+
*/
220+
#define FAST_PATH_SLOT(group, index) \
221+
(AssertMacro(((group) >= 0) && ((group) < FastPathLockGroupsPerBackend)), \
222+
AssertMacro(((index) >= 0) && ((index) < FP_LOCK_SLOTS_PER_GROUP)), \
223+
((group) * FP_LOCK_SLOTS_PER_GROUP + (index)))
224+
225+
/*
226+
* Given a slot index (into the whole per-backend array), calculated using
227+
* the FAST_PATH_SLOT macro, split it into group and index (in the group).
228+
*/
229+
#define FAST_PATH_GROUP(index) \
230+
(AssertMacro(((index) >= 0) && ((index) < FP_LOCK_SLOTS_PER_BACKEND)), \
231+
((index) / FP_LOCK_SLOTS_PER_GROUP))
232+
#define FAST_PATH_INDEX(index) \
233+
(AssertMacro(((index) >= 0) && ((index) < FP_LOCK_SLOTS_PER_BACKEND)), \
234+
((index) % FP_LOCK_SLOTS_PER_GROUP))
235+
187236
/* Macros for manipulating proc->fpLockBits */
188237
#define FAST_PATH_BITS_PER_SLOT 3
189238
#define FAST_PATH_LOCKNUMBER_OFFSET 1
190239
#define FAST_PATH_MASK ((1 << FAST_PATH_BITS_PER_SLOT) - 1)
240+
#define FAST_PATH_BITS(proc, n) (proc)->fpLockBits[FAST_PATH_GROUP(n)]
191241
#define FAST_PATH_GET_BITS(proc, n) \
192-
(((proc)->fpLockBits >> (FAST_PATH_BITS_PER_SLOT * n)) & FAST_PATH_MASK)
242+
((FAST_PATH_BITS(proc, n) >> (FAST_PATH_BITS_PER_SLOT * FAST_PATH_INDEX(n))) & FAST_PATH_MASK)
193243
#define FAST_PATH_BIT_POSITION(n, l) \
194244
(AssertMacro((l) >= FAST_PATH_LOCKNUMBER_OFFSET), \
195245
AssertMacro((l) < FAST_PATH_BITS_PER_SLOT+FAST_PATH_LOCKNUMBER_OFFSET), \
196246
AssertMacro((n) < FP_LOCK_SLOTS_PER_BACKEND), \
197-
((l) - FAST_PATH_LOCKNUMBER_OFFSET + FAST_PATH_BITS_PER_SLOT * (n)))
247+
((l) - FAST_PATH_LOCKNUMBER_OFFSET + FAST_PATH_BITS_PER_SLOT * (FAST_PATH_INDEX(n))))
198248
#define FAST_PATH_SET_LOCKMODE(proc, n, l) \
199-
(proc)->fpLockBits |= UINT64CONST(1) << FAST_PATH_BIT_POSITION(n, l)
249+
FAST_PATH_BITS(proc, n) |= UINT64CONST(1) << FAST_PATH_BIT_POSITION(n, l)
200250
#define FAST_PATH_CLEAR_LOCKMODE(proc, n, l) \
201-
(proc)->fpLockBits &= ~(UINT64CONST(1) << FAST_PATH_BIT_POSITION(n, l))
251+
FAST_PATH_BITS(proc, n) &= ~(UINT64CONST(1) << FAST_PATH_BIT_POSITION(n, l))
202252
#define FAST_PATH_CHECK_LOCKMODE(proc, n, l) \
203-
((proc)->fpLockBits & (UINT64CONST(1) << FAST_PATH_BIT_POSITION(n, l)))
253+
(FAST_PATH_BITS(proc, n) & (UINT64CONST(1) << FAST_PATH_BIT_POSITION(n, l)))
204254

205255
/*
206256
* The fast-path lock mechanism is concerned only with relation locks on
@@ -926,7 +976,7 @@ LockAcquireExtended(const LOCKTAG *locktag,
926976
* for now we don't worry about that case either.
927977
*/
928978
if (EligibleForRelationFastPath(locktag, lockmode) &&
929-
FastPathLocalUseCount < FP_LOCK_SLOTS_PER_BACKEND)
979+
FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < FP_LOCK_SLOTS_PER_GROUP)
930980
{
931981
uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
932982
bool acquired;
@@ -2065,7 +2115,7 @@ LockRelease(const LOCKTAG *locktag, LOCKMODE lockmode, bool sessionLock)
20652115

20662116
/* Attempt fast release of any lock eligible for the fast path. */
20672117
if (EligibleForRelationFastPath(locktag, lockmode) &&
2068-
FastPathLocalUseCount > 0)
2118+
FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] > 0)
20692119
{
20702120
bool released;
20712121

@@ -2633,12 +2683,18 @@ LockReassignOwner(LOCALLOCK *locallock, ResourceOwner parent)
26332683
static bool
26342684
FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
26352685
{
2636-
uint32 f;
2686+
uint32 i;
26372687
uint32 unused_slot = FP_LOCK_SLOTS_PER_BACKEND;
26382688

2689+
/* fast-path group the lock belongs to */
2690+
uint32 group = FAST_PATH_REL_GROUP(relid);
2691+
26392692
/* Scan for existing entry for this relid, remembering empty slot. */
2640-
for (f = 0; f < FP_LOCK_SLOTS_PER_BACKEND; f++)
2693+
for (i = 0; i < FP_LOCK_SLOTS_PER_GROUP; i++)
26412694
{
2695+
/* index into the whole per-backend array */
2696+
uint32 f = FAST_PATH_SLOT(group, i);
2697+
26422698
if (FAST_PATH_GET_BITS(MyProc, f) == 0)
26432699
unused_slot = f;
26442700
else if (MyProc->fpRelId[f] == relid)
@@ -2654,7 +2710,7 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
26542710
{
26552711
MyProc->fpRelId[unused_slot] = relid;
26562712
FAST_PATH_SET_LOCKMODE(MyProc, unused_slot, lockmode);
2657-
++FastPathLocalUseCount;
2713+
++FastPathLocalUseCounts[group];
26582714
return true;
26592715
}
26602716

@@ -2670,12 +2726,18 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
26702726
static bool
26712727
FastPathUnGrantRelationLock(Oid relid, LOCKMODE lockmode)
26722728
{
2673-
uint32 f;
2729+
uint32 i;
26742730
bool result = false;
26752731

2676-
FastPathLocalUseCount = 0;
2677-
for (f = 0; f < FP_LOCK_SLOTS_PER_BACKEND; f++)
2732+
/* fast-path group the lock belongs to */
2733+
uint32 group = FAST_PATH_REL_GROUP(relid);
2734+
2735+
FastPathLocalUseCounts[group] = 0;
2736+
for (i = 0; i < FP_LOCK_SLOTS_PER_GROUP; i++)
26782737
{
2738+
/* index into the whole per-backend array */
2739+
uint32 f = FAST_PATH_SLOT(group, i);
2740+
26792741
if (MyProc->fpRelId[f] == relid
26802742
&& FAST_PATH_CHECK_LOCKMODE(MyProc, f, lockmode))
26812743
{
@@ -2685,7 +2747,7 @@ FastPathUnGrantRelationLock(Oid relid, LOCKMODE lockmode)
26852747
/* we continue iterating so as to update FastPathLocalUseCount */
26862748
}
26872749
if (FAST_PATH_GET_BITS(MyProc, f) != 0)
2688-
++FastPathLocalUseCount;
2750+
++FastPathLocalUseCounts[group];
26892751
}
26902752
return result;
26912753
}
@@ -2714,7 +2776,8 @@ FastPathTransferRelationLocks(LockMethod lockMethodTable, const LOCKTAG *locktag
27142776
for (i = 0; i < ProcGlobal->allProcCount; i++)
27152777
{
27162778
PGPROC *proc = &ProcGlobal->allProcs[i];
2717-
uint32 f;
2779+
uint32 j,
2780+
group;
27182781

27192782
LWLockAcquire(&proc->fpInfoLock, LW_EXCLUSIVE);
27202783

@@ -2739,10 +2802,16 @@ FastPathTransferRelationLocks(LockMethod lockMethodTable, const LOCKTAG *locktag
27392802
continue;
27402803
}
27412804

2742-
for (f = 0; f < FP_LOCK_SLOTS_PER_BACKEND; f++)
2805+
/* fast-path group the lock belongs to */
2806+
group = FAST_PATH_REL_GROUP(relid);
2807+
2808+
for (j = 0; j < FP_LOCK_SLOTS_PER_GROUP; j++)
27432809
{
27442810
uint32 lockmode;
27452811

2812+
/* index into the whole per-backend array */
2813+
uint32 f = FAST_PATH_SLOT(group, j);
2814+
27462815
/* Look for an allocated slot matching the given relid. */
27472816
if (relid != proc->fpRelId[f] || FAST_PATH_GET_BITS(proc, f) == 0)
27482817
continue;
@@ -2793,14 +2862,21 @@ FastPathGetRelationLockEntry(LOCALLOCK *locallock)
27932862
PROCLOCK *proclock = NULL;
27942863
LWLock *partitionLock = LockHashPartitionLock(locallock->hashcode);
27952864
Oid relid = locktag->locktag_field2;
2796-
uint32 f;
2865+
uint32 i,
2866+
group;
2867+
2868+
/* fast-path group the lock belongs to */
2869+
group = FAST_PATH_REL_GROUP(relid);
27972870

27982871
LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
27992872

2800-
for (f = 0; f < FP_LOCK_SLOTS_PER_BACKEND; f++)
2873+
for (i = 0; i < FP_LOCK_SLOTS_PER_GROUP; i++)
28012874
{
28022875
uint32 lockmode;
28032876

2877+
/* index into the whole per-backend array */
2878+
uint32 f = FAST_PATH_SLOT(group, i);
2879+
28042880
/* Look for an allocated slot matching the given relid. */
28052881
if (relid != MyProc->fpRelId[f] || FAST_PATH_GET_BITS(MyProc, f) == 0)
28062882
continue;
@@ -2957,7 +3033,8 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
29573033
for (i = 0; i < ProcGlobal->allProcCount; i++)
29583034
{
29593035
PGPROC *proc = &ProcGlobal->allProcs[i];
2960-
uint32 f;
3036+
uint32 j,
3037+
group;
29613038

29623039
/* A backend never blocks itself */
29633040
if (proc == MyProc)
@@ -2979,10 +3056,16 @@ GetLockConflicts(const LOCKTAG *locktag, LOCKMODE lockmode, int *countp)
29793056
continue;
29803057
}
29813058

2982-
for (f = 0; f < FP_LOCK_SLOTS_PER_BACKEND; f++)
3059+
/* fast-path group the lock belongs to */
3060+
group = FAST_PATH_REL_GROUP(relid);
3061+
3062+
for (j = 0; j < FP_LOCK_SLOTS_PER_GROUP; j++)
29833063
{
29843064
uint32 lockmask;
29853065

3066+
/* index into the whole per-backend array */
3067+
uint32 f = FAST_PATH_SLOT(group, j);
3068+
29863069
/* Look for an allocated slot matching the given relid. */
29873070
if (relid != proc->fpRelId[f])
29883071
continue;

src/backend/storage/lmgr/proc.c

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ ProcGlobalShmemSize(void)
103103
Size size = 0;
104104
Size TotalProcs =
105105
add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts));
106+
Size fpLockBitsSize,
107+
fpRelIdSize;
106108

107109
/* ProcGlobal */
108110
size = add_size(size, sizeof(PROC_HDR));
@@ -113,6 +115,15 @@ ProcGlobalShmemSize(void)
113115
size = add_size(size, mul_size(TotalProcs, sizeof(*ProcGlobal->subxidStates)));
114116
size = add_size(size, mul_size(TotalProcs, sizeof(*ProcGlobal->statusFlags)));
115117

118+
/*
119+
* Memory needed for PGPROC fast-path lock arrays. Make sure the sizes are
120+
* nicely aligned in each backend.
121+
*/
122+
fpLockBitsSize = MAXALIGN(FastPathLockGroupsPerBackend * sizeof(uint64));
123+
fpRelIdSize = MAXALIGN(FastPathLockGroupsPerBackend * sizeof(Oid) * FP_LOCK_SLOTS_PER_GROUP);
124+
125+
size = add_size(size, mul_size(TotalProcs, (fpLockBitsSize + fpRelIdSize)));
126+
116127
return size;
117128
}
118129

@@ -163,6 +174,12 @@ InitProcGlobal(void)
163174
bool found;
164175
uint32 TotalProcs = MaxBackends + NUM_AUXILIARY_PROCS + max_prepared_xacts;
165176

177+
/* Used for setup of per-backend fast-path slots. */
178+
char *fpPtr,
179+
*fpEndPtr PG_USED_FOR_ASSERTS_ONLY;
180+
Size fpLockBitsSize,
181+
fpRelIdSize;
182+
166183
/* Create the ProcGlobal shared structure */
167184
ProcGlobal = (PROC_HDR *)
168185
ShmemInitStruct("Proc Header", sizeof(PROC_HDR), &found);
@@ -211,12 +228,38 @@ InitProcGlobal(void)
211228
ProcGlobal->statusFlags = (uint8 *) ShmemAlloc(TotalProcs * sizeof(*ProcGlobal->statusFlags));
212229
MemSet(ProcGlobal->statusFlags, 0, TotalProcs * sizeof(*ProcGlobal->statusFlags));
213230

231+
/*
232+
* Allocate arrays for fast-path locks. Those are variable-length, so
233+
* can't be included in PGPROC directly. We allocate a separate piece of
234+
* shared memory and then divide that between backends.
235+
*/
236+
fpLockBitsSize = MAXALIGN(FastPathLockGroupsPerBackend * sizeof(uint64));
237+
fpRelIdSize = MAXALIGN(FastPathLockGroupsPerBackend * sizeof(Oid) * FP_LOCK_SLOTS_PER_GROUP);
238+
239+
fpPtr = ShmemAlloc(TotalProcs * (fpLockBitsSize + fpRelIdSize));
240+
MemSet(fpPtr, 0, TotalProcs * (fpLockBitsSize + fpRelIdSize));
241+
242+
/* For asserts checking we did not overflow. */
243+
fpEndPtr = fpPtr + (TotalProcs * (fpLockBitsSize + fpRelIdSize));
244+
214245
for (i = 0; i < TotalProcs; i++)
215246
{
216247
PGPROC *proc = &procs[i];
217248

218249
/* Common initialization for all PGPROCs, regardless of type. */
219250

251+
/*
252+
* Set the fast-path lock arrays, and move the pointer. We interleave
253+
* the two arrays, to (hopefully) get some locality for each backend.
254+
*/
255+
proc->fpLockBits = (uint64 *) fpPtr;
256+
fpPtr += fpLockBitsSize;
257+
258+
proc->fpRelId = (Oid *) fpPtr;
259+
fpPtr += fpRelIdSize;
260+
261+
Assert(fpPtr <= fpEndPtr);
262+
220263
/*
221264
* Set up per-PGPROC semaphore, latch, and fpInfoLock. Prepared xact
222265
* dummy PGPROCs don't need these though - they're never associated
@@ -278,6 +321,9 @@ InitProcGlobal(void)
278321
pg_atomic_init_u64(&(proc->waitStart), 0);
279322
}
280323

324+
/* Should have consumed exactly the expected amount of fast-path memory. */
325+
Assert(fpPtr = fpEndPtr);
326+
281327
/*
282328
* Save pointers to the blocks of PGPROC structures reserved for auxiliary
283329
* processes and prepared transactions.

src/backend/tcop/postgres.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4190,6 +4190,9 @@ PostgresSingleUserMain(int argc, char *argv[],
41904190
/* Initialize MaxBackends */
41914191
InitializeMaxBackends();
41924192

4193+
/* Initialize size of fast-path lock cache. */
4194+
InitializeFastPathLocks();
4195+
41934196
/*
41944197
* Give preloaded libraries a chance to request additional shared memory.
41954198
*/

0 commit comments

Comments
 (0)