Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit 8ae35e9

Browse files
committed
Improve memory space management in tuplesort and tuplestore.
The code originally just doubled the size of the tuple-pointer array so long as that would fit in allowedMem. This could result in failing to use as much as half of allowedMem, if (as is typical) the last doubling attempt didn't quite fit. Worse, we might double the array size but be unable to use most of the added slots, because there was no room left within the allowedMem limit for tuples the slots should point to. To fix, double only so long as we've used less than half of allowedMem in total. Then do one more array enlargement, but scale it based on total memory consumption so far. This will work nicely as long as the average tuple size is reasonably stable, and in any case should be better than the old method. This change will result in large sort operations consuming a larger fraction of work_mem than they typically did in the past. The release notes should mention that users may want to revisit their work_mem settings, if they'd tuned those settings based on the old behavior of sorting. Jeff Janes, reviewed by Peter Geoghegan and Robert Haas
1 parent 1296d5c commit 8ae35e9

File tree

2 files changed

+226
-34
lines changed

2 files changed

+226
-34
lines changed

src/backend/utils/sort/tuplesort.c

+99-19
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ struct Tuplesortstate
276276
SortTuple *memtuples; /* array of SortTuple structs */
277277
int memtupcount; /* number of tuples currently present */
278278
int memtupsize; /* allocated length of memtuples array */
279+
bool growmemtuples; /* memtuples' growth still underway? */
279280

280281
/*
281282
* While building initial runs, this is the current output run number
@@ -570,6 +571,7 @@ tuplesort_begin_common(int workMem, bool randomAccess)
570571

571572
state->memtupcount = 0;
572573
state->memtupsize = 1024; /* initial guess */
574+
state->growmemtuples = true;
573575
state->memtuples = (SortTuple *) palloc(state->memtupsize * sizeof(SortTuple));
574576

575577
USEMEM(state, GetMemoryChunkSpace(state->memtuples));
@@ -955,44 +957,122 @@ tuplesort_end(Tuplesortstate *state)
955957

956958
/*
957959
* Grow the memtuples[] array, if possible within our memory constraint.
958-
* Return TRUE if able to enlarge the array, FALSE if not.
960+
* Return TRUE if we were able to enlarge the array, FALSE if not.
959961
*
960-
* At each increment we double the size of the array. When we are short
961-
* on memory we could consider smaller increases, but because availMem
962-
* moves around with tuple addition/removal, this might result in thrashing.
963-
* Small increases in the array size are likely to be pretty inefficient.
962+
* Normally, at each increment we double the size of the array. When we no
963+
* longer have enough memory to do that, we attempt one last, smaller increase
964+
* (and then clear the growmemtuples flag so we don't try any more). That
965+
* allows us to use allowedMem as fully as possible; sticking to the pure
966+
* doubling rule could result in almost half of allowedMem going unused.
967+
* Because availMem moves around with tuple addition/removal, we need some
968+
* rule to prevent making repeated small increases in memtupsize, which would
969+
* just be useless thrashing. The growmemtuples flag accomplishes that and
970+
* also prevents useless recalculations in this function.
964971
*/
965972
static bool
966973
grow_memtuples(Tuplesortstate *state)
967974
{
975+
int newmemtupsize;
976+
int memtupsize = state->memtupsize;
977+
long memNowUsed = state->allowedMem - state->availMem;
978+
979+
/* Forget it if we've already maxed out memtuples, per comment above */
980+
if (!state->growmemtuples)
981+
return false;
982+
983+
/* Select new value of memtupsize */
984+
if (memNowUsed <= state->availMem)
985+
{
986+
/*
987+
* It is surely safe to double memtupsize if we've used no more than
988+
* half of allowedMem.
989+
*
990+
* Note: it might seem that we need to worry about memtupsize * 2
991+
* overflowing an int, but the MaxAllocSize clamp applied below
992+
* ensures the existing memtupsize can't be large enough for that.
993+
*/
994+
newmemtupsize = memtupsize * 2;
995+
}
996+
else
997+
{
998+
/*
999+
* This will be the last increment of memtupsize. Abandon doubling
1000+
* strategy and instead increase as much as we safely can.
1001+
*
1002+
* To stay within allowedMem, we can't increase memtupsize by more
1003+
* than availMem / sizeof(SortTuple) elements. In practice, we want
1004+
* to increase it by considerably less, because we need to leave some
1005+
* space for the tuples to which the new array slots will refer. We
1006+
* assume the new tuples will be about the same size as the tuples
1007+
* we've already seen, and thus we can extrapolate from the space
1008+
* consumption so far to estimate an appropriate new size for the
1009+
* memtuples array. The optimal value might be higher or lower than
1010+
* this estimate, but it's hard to know that in advance.
1011+
*
1012+
* This calculation is safe against enlarging the array so much that
1013+
* LACKMEM becomes true, because the memory currently used includes
1014+
* the present array; thus, there would be enough allowedMem for the
1015+
* new array elements even if no other memory were currently used.
1016+
*
1017+
* We do the arithmetic in float8, because otherwise the product of
1018+
* memtupsize and allowedMem could overflow. (A little algebra shows
1019+
* that grow_ratio must be less than 2 here, so we are not risking
1020+
* integer overflow this way.) Any inaccuracy in the result should be
1021+
* insignificant; but even if we computed a completely insane result,
1022+
* the checks below will prevent anything really bad from happening.
1023+
*/
1024+
double grow_ratio;
1025+
1026+
grow_ratio = (double) state->allowedMem / (double) memNowUsed;
1027+
newmemtupsize = (int) (memtupsize * grow_ratio);
1028+
1029+
/* We won't make any further enlargement attempts */
1030+
state->growmemtuples = false;
1031+
}
1032+
1033+
/* Must enlarge array by at least one element, else report failure */
1034+
if (newmemtupsize <= memtupsize)
1035+
goto noalloc;
1036+
9681037
/*
969-
* We need to be sure that we do not cause LACKMEM to become true, else
970-
* the space management algorithm will go nuts. We assume here that the
971-
* memory chunk overhead associated with the memtuples array is constant
972-
* and so there will be no unexpected addition to what we ask for. (The
973-
* minimum array size established in tuplesort_begin_common is large
974-
* enough to force palloc to treat it as a separate chunk, so this
975-
* assumption should be good. But let's check it.)
1038+
* On a 64-bit machine, allowedMem could be more than MaxAllocSize. Clamp
1039+
* to ensure our request won't be rejected by palloc.
9761040
*/
977-
if (state->availMem <= (long) (state->memtupsize * sizeof(SortTuple)))
978-
return false;
1041+
if ((Size) newmemtupsize >= MaxAllocSize / sizeof(SortTuple))
1042+
{
1043+
newmemtupsize = (int) (MaxAllocSize / sizeof(SortTuple));
1044+
state->growmemtuples = false; /* can't grow any more */
1045+
}
9791046

9801047
/*
981-
* On a 64-bit machine, allowedMem could be high enough to get us into
982-
* trouble with MaxAllocSize, too.
1048+
* We need to be sure that we do not cause LACKMEM to become true, else
1049+
* the space management algorithm will go nuts. The code above should
1050+
* never generate a dangerous request, but to be safe, check explicitly
1051+
* that the array growth fits within availMem. (We could still cause
1052+
* LACKMEM if the memory chunk overhead associated with the memtuples
1053+
* array were to increase. That shouldn't happen with any sane value of
1054+
* allowedMem, because at any array size large enough to risk LACKMEM,
1055+
* palloc would be treating both old and new arrays as separate chunks.
1056+
* But we'll check LACKMEM explicitly below just in case.)
9831057
*/
984-
if ((Size) (state->memtupsize * 2) >= MaxAllocSize / sizeof(SortTuple))
985-
return false;
1058+
if (state->availMem < (long) ((newmemtupsize - memtupsize) * sizeof(SortTuple)))
1059+
goto noalloc;
9861060

1061+
/* OK, do it */
9871062
FREEMEM(state, GetMemoryChunkSpace(state->memtuples));
988-
state->memtupsize *= 2;
1063+
state->memtupsize = newmemtupsize;
9891064
state->memtuples = (SortTuple *)
9901065
repalloc(state->memtuples,
9911066
state->memtupsize * sizeof(SortTuple));
9921067
USEMEM(state, GetMemoryChunkSpace(state->memtuples));
9931068
if (LACKMEM(state))
9941069
elog(ERROR, "unexpected out-of-memory situation during sort");
9951070
return true;
1071+
1072+
noalloc:
1073+
/* If for any reason we didn't realloc, shut off future attempts */
1074+
state->growmemtuples = false;
1075+
return false;
9961076
}
9971077

9981078
/*

src/backend/utils/sort/tuplestore.c

+127-15
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ struct Tuplestorestate
105105
bool interXact; /* keep open through transactions? */
106106
bool truncated; /* tuplestore_trim has removed tuples? */
107107
long availMem; /* remaining memory available, in bytes */
108+
long allowedMem; /* total memory allowed, in bytes */
108109
BufFile *myfile; /* underlying file, or NULL if none */
109110
MemoryContext context; /* memory context for holding tuples */
110111
ResourceOwner resowner; /* resowner for holding temp files */
@@ -156,6 +157,7 @@ struct Tuplestorestate
156157
int memtupdeleted; /* the first N slots are currently unused */
157158
int memtupcount; /* number of tuples currently present */
158159
int memtupsize; /* allocated length of memtuples array */
160+
bool growmemtuples; /* memtuples' growth still underway? */
159161

160162
/*
161163
* These variables are used to keep track of the current positions.
@@ -254,14 +256,16 @@ tuplestore_begin_common(int eflags, bool interXact, int maxKBytes)
254256
state->eflags = eflags;
255257
state->interXact = interXact;
256258
state->truncated = false;
257-
state->availMem = maxKBytes * 1024L;
259+
state->allowedMem = maxKBytes * 1024L;
260+
state->availMem = state->allowedMem;
258261
state->myfile = NULL;
259262
state->context = CurrentMemoryContext;
260263
state->resowner = CurrentResourceOwner;
261264

262265
state->memtupdeleted = 0;
263266
state->memtupcount = 0;
264267
state->memtupsize = 1024; /* initial guess */
268+
state->growmemtuples = true;
265269
state->memtuples = (void **) palloc(state->memtupsize * sizeof(void *));
266270

267271
USEMEM(state, GetMemoryChunkSpace(state->memtuples));
@@ -526,6 +530,126 @@ tuplestore_ateof(Tuplestorestate *state)
526530
return state->readptrs[state->activeptr].eof_reached;
527531
}
528532

533+
/*
534+
* Grow the memtuples[] array, if possible within our memory constraint.
535+
* Return TRUE if we were able to enlarge the array, FALSE if not.
536+
*
537+
* Normally, at each increment we double the size of the array. When we no
538+
* longer have enough memory to do that, we attempt one last, smaller increase
539+
* (and then clear the growmemtuples flag so we don't try any more). That
540+
* allows us to use allowedMem as fully as possible; sticking to the pure
541+
* doubling rule could result in almost half of allowedMem going unused.
542+
* Because availMem moves around with tuple addition/removal, we need some
543+
* rule to prevent making repeated small increases in memtupsize, which would
544+
* just be useless thrashing. The growmemtuples flag accomplishes that and
545+
* also prevents useless recalculations in this function.
546+
*/
547+
static bool
548+
grow_memtuples(Tuplestorestate *state)
549+
{
550+
int newmemtupsize;
551+
int memtupsize = state->memtupsize;
552+
long memNowUsed = state->allowedMem - state->availMem;
553+
554+
/* Forget it if we've already maxed out memtuples, per comment above */
555+
if (!state->growmemtuples)
556+
return false;
557+
558+
/* Select new value of memtupsize */
559+
if (memNowUsed <= state->availMem)
560+
{
561+
/*
562+
* It is surely safe to double memtupsize if we've used no more than
563+
* half of allowedMem.
564+
*
565+
* Note: it might seem that we need to worry about memtupsize * 2
566+
* overflowing an int, but the MaxAllocSize clamp applied below
567+
* ensures the existing memtupsize can't be large enough for that.
568+
*/
569+
newmemtupsize = memtupsize * 2;
570+
}
571+
else
572+
{
573+
/*
574+
* This will be the last increment of memtupsize. Abandon doubling
575+
* strategy and instead increase as much as we safely can.
576+
*
577+
* To stay within allowedMem, we can't increase memtupsize by more
578+
* than availMem / sizeof(void *) elements. In practice, we want
579+
* to increase it by considerably less, because we need to leave some
580+
* space for the tuples to which the new array slots will refer. We
581+
* assume the new tuples will be about the same size as the tuples
582+
* we've already seen, and thus we can extrapolate from the space
583+
* consumption so far to estimate an appropriate new size for the
584+
* memtuples array. The optimal value might be higher or lower than
585+
* this estimate, but it's hard to know that in advance.
586+
*
587+
* This calculation is safe against enlarging the array so much that
588+
* LACKMEM becomes true, because the memory currently used includes
589+
* the present array; thus, there would be enough allowedMem for the
590+
* new array elements even if no other memory were currently used.
591+
*
592+
* We do the arithmetic in float8, because otherwise the product of
593+
* memtupsize and allowedMem could overflow. (A little algebra shows
594+
* that grow_ratio must be less than 2 here, so we are not risking
595+
* integer overflow this way.) Any inaccuracy in the result should be
596+
* insignificant; but even if we computed a completely insane result,
597+
* the checks below will prevent anything really bad from happening.
598+
*/
599+
double grow_ratio;
600+
601+
grow_ratio = (double) state->allowedMem / (double) memNowUsed;
602+
newmemtupsize = (int) (memtupsize * grow_ratio);
603+
604+
/* We won't make any further enlargement attempts */
605+
state->growmemtuples = false;
606+
}
607+
608+
/* Must enlarge array by at least one element, else report failure */
609+
if (newmemtupsize <= memtupsize)
610+
goto noalloc;
611+
612+
/*
613+
* On a 64-bit machine, allowedMem could be more than MaxAllocSize. Clamp
614+
* to ensure our request won't be rejected by palloc.
615+
*/
616+
if ((Size) newmemtupsize >= MaxAllocSize / sizeof(void *))
617+
{
618+
newmemtupsize = (int) (MaxAllocSize / sizeof(void *));
619+
state->growmemtuples = false; /* can't grow any more */
620+
}
621+
622+
/*
623+
* We need to be sure that we do not cause LACKMEM to become true, else
624+
* the space management algorithm will go nuts. The code above should
625+
* never generate a dangerous request, but to be safe, check explicitly
626+
* that the array growth fits within availMem. (We could still cause
627+
* LACKMEM if the memory chunk overhead associated with the memtuples
628+
* array were to increase. That shouldn't happen with any sane value of
629+
* allowedMem, because at any array size large enough to risk LACKMEM,
630+
* palloc would be treating both old and new arrays as separate chunks.
631+
* But we'll check LACKMEM explicitly below just in case.)
632+
*/
633+
if (state->availMem < (long) ((newmemtupsize - memtupsize) * sizeof(void *)))
634+
goto noalloc;
635+
636+
/* OK, do it */
637+
FREEMEM(state, GetMemoryChunkSpace(state->memtuples));
638+
state->memtupsize = newmemtupsize;
639+
state->memtuples = (void **)
640+
repalloc(state->memtuples,
641+
state->memtupsize * sizeof(void *));
642+
USEMEM(state, GetMemoryChunkSpace(state->memtuples));
643+
if (LACKMEM(state))
644+
elog(ERROR, "unexpected out-of-memory situation during sort");
645+
return true;
646+
647+
noalloc:
648+
/* If for any reason we didn't realloc, shut off future attempts */
649+
state->growmemtuples = false;
650+
return false;
651+
}
652+
529653
/*
530654
* Accept one tuple and append it to the tuplestore.
531655
*
@@ -631,20 +755,8 @@ tuplestore_puttuple_common(Tuplestorestate *state, void *tuple)
631755
*/
632756
if (state->memtupcount >= state->memtupsize - 1)
633757
{
634-
/*
635-
* See grow_memtuples() in tuplesort.c for the rationale
636-
* behind these two tests.
637-
*/
638-
if (state->availMem > (long) (state->memtupsize * sizeof(void *)) &&
639-
(Size) (state->memtupsize * 2) < MaxAllocSize / sizeof(void *))
640-
{
641-
FREEMEM(state, GetMemoryChunkSpace(state->memtuples));
642-
state->memtupsize *= 2;
643-
state->memtuples = (void **)
644-
repalloc(state->memtuples,
645-
state->memtupsize * sizeof(void *));
646-
USEMEM(state, GetMemoryChunkSpace(state->memtuples));
647-
}
758+
(void) grow_memtuples(state);
759+
Assert(state->memtupcount < state->memtupsize);
648760
}
649761

650762
/* Stash the tuple in the in-memory array */

0 commit comments

Comments
 (0)