Add auxiliary lists to GUC data structures for better performance.

The previous patch made addition of new GUCs cheap, but other GUC
operations aren't improved and indeed get a bit slower, because
hash_seq_search() is slower than just scanning a pointer array.

However, most performance-critical GUC operations only need
to touch a relatively small fraction of the GUCs; especially
so for AtEOXact_GUC().  We can improve matters at the cost
of a bit more space by adding dlist or slist links to the
GUC data structures.  This patch invents lists that track

(1) all GUCs with non-default "source";

(2) all GUCs with nonempty state stack (implying they've
been changed in the current transaction);

(3) all GUCs due for reporting to the client.

All of guc.c's performance-critical cases can make use of one or
another of these lists to avoid searching the whole hash table.
In particular, the stack list means that transaction end
doesn't take time proportional to the number of GUCs, but
only to the number changed in the current transaction.

Discussion: https://postgr.es/m/2982579.1662416866@sss.pgh.pa.us
This commit is contained in:
Tom Lane 2022-10-14 12:36:14 -04:00
parent 3057465acf
commit f13b2088fa
2 changed files with 169 additions and 98 deletions

View File

@ -205,12 +205,23 @@ typedef struct
static HTAB *guc_hashtab; /* entries are GUCHashEntrys */
static bool guc_dirty; /* true if need to do commit/abort work */
/*
* In addition to the hash table, variables having certain properties are
* linked into these lists, so that we can find them without scanning the
* whole hash table. In most applications, only a small fraction of the
* GUCs appear in these lists at any given time. The usage of the stack
* and report lists is stylized enough that they can be slists, but the
* nondef list has to be a dlist to avoid O(N) deletes in common cases.
*/
static dlist_head guc_nondef_list; /* list of variables that have source
* different from PGC_S_DEFAULT */
static slist_head guc_stack_list; /* list of variables that have non-NULL
* stack */
static slist_head guc_report_list; /* list of variables that have the
* GUC_NEEDS_REPORT bit set in status */
static bool reporting_enabled; /* true to enable GUC_REPORT */
static bool report_needed; /* true if any GUC_REPORT reports are needed */
static int GUCNestLevel = 0; /* 1 when in main transaction */
@ -219,6 +230,8 @@ static uint32 guc_name_hash(const void *key, Size keysize);
static int guc_name_match(const void *key1, const void *key2, Size keysize);
static void InitializeGUCOptionsFromEnvironment(void);
static void InitializeOneGUCOption(struct config_generic *gconf);
static void RemoveGUCFromLists(struct config_generic *gconf);
static void set_guc_source(struct config_generic *gconf, GucSource newsource);
static void pg_timezone_abbrev_initialize(void);
static void push_old_value(struct config_generic *gconf, GucAction action);
static void ReportGUCOption(struct config_generic *record);
@ -465,7 +478,7 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel)
if (gconf->reset_source == PGC_S_FILE)
gconf->reset_source = PGC_S_DEFAULT;
if (gconf->source == PGC_S_FILE)
gconf->source = PGC_S_DEFAULT;
set_guc_source(gconf, PGC_S_DEFAULT);
for (stack = gconf->stack; stack; stack = stack->prev)
{
if (stack->source == PGC_S_FILE)
@ -1403,8 +1416,6 @@ InitializeGUCOptions(void)
InitializeOneGUCOption(hentry->gucvar);
}
guc_dirty = false;
reporting_enabled = false;
/*
@ -1600,6 +1611,23 @@ InitializeOneGUCOption(struct config_generic *gconf)
}
}
/*
* Summarily remove a GUC variable from any linked lists it's in.
*
* We use this in cases where the variable is about to be deleted or reset.
* These aren't common operations, so it's okay if this is a bit slow.
*/
static void
RemoveGUCFromLists(struct config_generic *gconf)
{
if (gconf->source != PGC_S_DEFAULT)
dlist_delete(&gconf->nondef_link);
if (gconf->stack != NULL)
slist_delete(&guc_stack_list, &gconf->stack_link);
if (gconf->status & GUC_NEEDS_REPORT)
slist_delete(&guc_report_list, &gconf->report_link);
}
/*
* Select the configuration files and data directory to be used, and
@ -1835,13 +1863,13 @@ pg_timezone_abbrev_initialize(void)
void
ResetAllOptions(void)
{
HASH_SEQ_STATUS status;
GUCHashEntry *hentry;
dlist_mutable_iter iter;
hash_seq_init(&status, guc_hashtab);
while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
/* We need only consider GUCs not already at PGC_S_DEFAULT */
dlist_foreach_modify(iter, &guc_nondef_list)
{
struct config_generic *gconf = hentry->gucvar;
struct config_generic *gconf = dlist_container(struct config_generic,
nondef_link, iter.cur);
/* Don't reset non-SET-able values */
if (gconf->context != PGC_SUSET &&
@ -1921,19 +1949,44 @@ ResetAllOptions(void)
}
}
gconf->source = gconf->reset_source;
set_guc_source(gconf, gconf->reset_source);
gconf->scontext = gconf->reset_scontext;
gconf->srole = gconf->reset_srole;
if (gconf->flags & GUC_REPORT)
if ((gconf->flags & GUC_REPORT) && !(gconf->status & GUC_NEEDS_REPORT))
{
gconf->status |= GUC_NEEDS_REPORT;
report_needed = true;
slist_push_head(&guc_report_list, &gconf->report_link);
}
}
}
/*
* Apply a change to a GUC variable's "source" field.
*
* Use this rather than just assigning, to ensure that the variable's
* membership in guc_nondef_list is updated correctly.
*/
static void
set_guc_source(struct config_generic *gconf, GucSource newsource)
{
/* Adjust nondef list membership if appropriate for change */
if (gconf->source == PGC_S_DEFAULT)
{
if (newsource != PGC_S_DEFAULT)
dlist_push_tail(&guc_nondef_list, &gconf->nondef_link);
}
else
{
if (newsource == PGC_S_DEFAULT)
dlist_delete(&gconf->nondef_link);
}
/* Now update the source field */
gconf->source = newsource;
}
/*
* push_old_value
* Push previous state during transactional assignment to a GUC variable.
@ -1980,7 +2033,6 @@ push_old_value(struct config_generic *gconf, GucAction action)
Assert(stack->state == GUC_SAVE);
break;
}
Assert(guc_dirty); /* must be set already */
return;
}
@ -2011,10 +2063,9 @@ push_old_value(struct config_generic *gconf, GucAction action)
stack->srole = gconf->srole;
set_stack_value(gconf, &stack->prior);
if (gconf->stack == NULL)
slist_push_head(&guc_stack_list, &gconf->stack_link);
gconf->stack = stack;
/* Ensure we remember to pop at end of xact */
guc_dirty = true;
}
@ -2058,9 +2109,7 @@ NewGUCNestLevel(void)
void
AtEOXact_GUC(bool isCommit, int nestLevel)
{
bool still_dirty;
HASH_SEQ_STATUS status;
GUCHashEntry *hentry;
slist_mutable_iter iter;
/*
* Note: it's possible to get here with GUCNestLevel == nestLevel-1 during
@ -2071,18 +2120,11 @@ AtEOXact_GUC(bool isCommit, int nestLevel)
(nestLevel <= GUCNestLevel ||
(nestLevel == GUCNestLevel + 1 && !isCommit)));
/* Quick exit if nothing's changed in this transaction */
if (!guc_dirty)
/* We need only process GUCs having nonempty stacks */
slist_foreach_modify(iter, &guc_stack_list)
{
GUCNestLevel = nestLevel - 1;
return;
}
still_dirty = false;
hash_seq_init(&status, guc_hashtab);
while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
{
struct config_generic *gconf = hentry->gucvar;
struct config_generic *gconf = slist_container(struct config_generic,
stack_link, iter.cur);
GucStack *stack;
/*
@ -2315,30 +2357,30 @@ AtEOXact_GUC(bool isCommit, int nestLevel)
set_extra_field(gconf, &(stack->masked.extra), NULL);
/* And restore source information */
gconf->source = newsource;
set_guc_source(gconf, newsource);
gconf->scontext = newscontext;
gconf->srole = newsrole;
}
/* Finish popping the state stack */
/*
* Pop the GUC's state stack; if it's now empty, remove the GUC
* from guc_stack_list.
*/
gconf->stack = prev;
if (prev == NULL)
slist_delete_current(&iter);
pfree(stack);
/* Report new value if we changed it */
if (changed && (gconf->flags & GUC_REPORT))
if (changed && (gconf->flags & GUC_REPORT) &&
!(gconf->status & GUC_NEEDS_REPORT))
{
gconf->status |= GUC_NEEDS_REPORT;
report_needed = true;
slist_push_head(&guc_report_list, &gconf->report_link);
}
} /* end of stack-popping loop */
if (stack != NULL)
still_dirty = true;
}
/* If there are no remaining stack entries, we can reset guc_dirty */
guc_dirty = still_dirty;
/* Update nesting level */
GUCNestLevel = nestLevel - 1;
}
@ -2383,8 +2425,6 @@ BeginReportingGUCOptions(void)
if (conf->flags & GUC_REPORT)
ReportGUCOption(conf);
}
report_needed = false;
}
/*
@ -2403,8 +2443,7 @@ BeginReportingGUCOptions(void)
void
ReportChangedGUCOptions(void)
{
HASH_SEQ_STATUS status;
GUCHashEntry *hentry;
slist_mutable_iter iter;
/* Quick exit if not (yet) enabled */
if (!reporting_enabled)
@ -2420,28 +2459,24 @@ ReportChangedGUCOptions(void)
SetConfigOption("in_hot_standby", "false",
PGC_INTERNAL, PGC_S_OVERRIDE);
/* Quick exit if no values have been changed */
if (!report_needed)
return;
/* Transmit new values of interesting variables */
hash_seq_init(&status, guc_hashtab);
while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
slist_foreach_modify(iter, &guc_report_list)
{
struct config_generic *conf = hentry->gucvar;
struct config_generic *conf = slist_container(struct config_generic,
report_link, iter.cur);
if ((conf->flags & GUC_REPORT) && (conf->status & GUC_NEEDS_REPORT))
ReportGUCOption(conf);
Assert((conf->flags & GUC_REPORT) && (conf->status & GUC_NEEDS_REPORT));
ReportGUCOption(conf);
conf->status &= ~GUC_NEEDS_REPORT;
slist_delete_current(&iter);
}
report_needed = false;
}
/*
* ReportGUCOption: if appropriate, transmit option value to frontend
*
* We need not transmit the value if it's the same as what we last
* transmitted. However, clear the NEEDS_REPORT flag in any case.
* transmitted.
*/
static void
ReportGUCOption(struct config_generic *record)
@ -2468,8 +2503,6 @@ ReportGUCOption(struct config_generic *record)
}
pfree(val);
record->status &= ~GUC_NEEDS_REPORT;
}
/*
@ -3524,7 +3557,7 @@ set_config_option_ext(const char *name, const char *value,
*conf->variable = newval;
set_extra_field(&conf->gen, &conf->gen.extra,
newextra);
conf->gen.source = source;
set_guc_source(&conf->gen, source);
conf->gen.scontext = context;
conf->gen.srole = srole;
}
@ -3622,7 +3655,7 @@ set_config_option_ext(const char *name, const char *value,
*conf->variable = newval;
set_extra_field(&conf->gen, &conf->gen.extra,
newextra);
conf->gen.source = source;
set_guc_source(&conf->gen, source);
conf->gen.scontext = context;
conf->gen.srole = srole;
}
@ -3720,7 +3753,7 @@ set_config_option_ext(const char *name, const char *value,
*conf->variable = newval;
set_extra_field(&conf->gen, &conf->gen.extra,
newextra);
conf->gen.source = source;
set_guc_source(&conf->gen, source);
conf->gen.scontext = context;
conf->gen.srole = srole;
}
@ -3844,7 +3877,7 @@ set_config_option_ext(const char *name, const char *value,
set_string_field(conf, conf->variable, newval);
set_extra_field(&conf->gen, &conf->gen.extra,
newextra);
conf->gen.source = source;
set_guc_source(&conf->gen, source);
conf->gen.scontext = context;
conf->gen.srole = srole;
}
@ -3947,7 +3980,7 @@ set_config_option_ext(const char *name, const char *value,
*conf->variable = newval;
set_extra_field(&conf->gen, &conf->gen.extra,
newextra);
conf->gen.source = source;
set_guc_source(&conf->gen, source);
conf->gen.scontext = context;
conf->gen.srole = srole;
}
@ -3987,10 +4020,11 @@ set_config_option_ext(const char *name, const char *value,
}
}
if (changeVal && (record->flags & GUC_REPORT))
if (changeVal && (record->flags & GUC_REPORT) &&
!(record->status & GUC_NEEDS_REPORT))
{
record->status |= GUC_NEEDS_REPORT;
report_needed = true;
slist_push_head(&guc_report_list, &record->report_link);
}
return changeVal ? 1 : -1;
@ -4663,6 +4697,11 @@ define_custom_variable(struct config_generic *variable)
hentry->gucname = name;
hentry->gucvar = variable;
/*
* Remove the placeholder from any lists it's in, too.
*/
RemoveGUCFromLists(&pHolder->gen);
/*
* Assign the string value(s) stored in the placeholder to the real
* variable. Essentially, we need to duplicate all the active and stacked
@ -4794,7 +4833,11 @@ reapply_stacked_values(struct config_generic *variable,
(void) set_config_option_ext(name, curvalue,
curscontext, cursource, cursrole,
GUC_ACTION_SET, true, WARNING, false);
variable->stack = NULL;
if (variable->stack != NULL)
{
slist_delete(&guc_stack_list, &variable->stack_link);
variable->stack = NULL;
}
}
}
}
@ -4978,10 +5021,13 @@ MarkGUCPrefixReserved(const char *className)
var->name),
errdetail("\"%s\" is now a reserved prefix.",
className)));
/* Remove it from the hash table */
hash_search(guc_hashtab,
&var->name,
HASH_REMOVE,
NULL);
/* Remove it from any lists it's in, too */
RemoveGUCFromLists(var);
}
}
@ -5002,8 +5048,7 @@ struct config_generic **
get_explain_guc_options(int *num)
{
struct config_generic **result;
HASH_SEQ_STATUS status;
GUCHashEntry *hentry;
dlist_iter iter;
*num = 0;
@ -5013,10 +5058,11 @@ get_explain_guc_options(int *num)
*/
result = palloc(sizeof(struct config_generic *) * hash_get_num_entries(guc_hashtab));
hash_seq_init(&status, guc_hashtab);
while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
/* We need only consider GUCs with source not PGC_S_DEFAULT */
dlist_foreach(iter, &guc_nondef_list)
{
struct config_generic *conf = hentry->gucvar;
struct config_generic *conf = dlist_container(struct config_generic,
nondef_link, iter.cur);
bool modified;
/* return only parameters marked for inclusion in explain */
@ -5251,8 +5297,7 @@ ShowGUCOption(struct config_generic *record, bool use_units)
static void
write_one_nondefault_variable(FILE *fp, struct config_generic *gconf)
{
if (gconf->source == PGC_S_DEFAULT)
return;
Assert(gconf->source != PGC_S_DEFAULT);
fprintf(fp, "%s", gconf->name);
fputc(0, fp);
@ -5321,8 +5366,7 @@ write_nondefault_variables(GucContext context)
{
int elevel;
FILE *fp;
HASH_SEQ_STATUS status;
GUCHashEntry *hentry;
dlist_iter iter;
Assert(context == PGC_POSTMASTER || context == PGC_SIGHUP);
@ -5341,10 +5385,13 @@ write_nondefault_variables(GucContext context)
return;
}
hash_seq_init(&status, guc_hashtab);
while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
/* We need only consider GUCs with source not PGC_S_DEFAULT */
dlist_foreach(iter, &guc_nondef_list)
{
write_one_nondefault_variable(fp, hentry->gucvar);
struct config_generic *gconf = dlist_container(struct config_generic,
nondef_link, iter.cur);
write_one_nondefault_variable(fp, gconf);
}
if (FreeFile(fp))
@ -5618,17 +5665,23 @@ Size
EstimateGUCStateSpace(void)
{
Size size;
HASH_SEQ_STATUS status;
GUCHashEntry *hentry;
dlist_iter iter;
/* Add space reqd for saving the data size of the guc state */
size = sizeof(Size);
/* Add up the space needed for each GUC variable */
hash_seq_init(&status, guc_hashtab);
while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
size = add_size(size,
estimate_variable_size(hentry->gucvar));
/*
* Add up the space needed for each GUC variable.
*
* We need only process non-default GUCs.
*/
dlist_foreach(iter, &guc_nondef_list)
{
struct config_generic *gconf = dlist_container(struct config_generic,
nondef_link, iter.cur);
size = add_size(size, estimate_variable_size(gconf));
}
return size;
}
@ -5767,17 +5820,21 @@ SerializeGUCState(Size maxsize, char *start_address)
char *curptr;
Size actual_size;
Size bytes_left;
HASH_SEQ_STATUS status;
GUCHashEntry *hentry;
dlist_iter iter;
/* Reserve space for saving the actual size of the guc state */
Assert(maxsize > sizeof(actual_size));
curptr = start_address + sizeof(actual_size);
bytes_left = maxsize - sizeof(actual_size);
hash_seq_init(&status, guc_hashtab);
while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
serialize_variable(&curptr, &bytes_left, hentry->gucvar);
/* We need only consider GUCs with source not PGC_S_DEFAULT */
dlist_foreach(iter, &guc_nondef_list)
{
struct config_generic *gconf = dlist_container(struct config_generic,
nondef_link, iter.cur);
serialize_variable(&curptr, &bytes_left, gconf);
}
/* Store actual size without assuming alignment of start_address. */
actual_size = maxsize - bytes_left - sizeof(actual_size);
@ -5862,8 +5919,7 @@ RestoreGUCState(void *gucstate)
char *srcptr = (char *) gucstate;
char *srcend;
Size len;
HASH_SEQ_STATUS status;
GUCHashEntry *hentry;
dlist_mutable_iter iter;
ErrorContextCallback error_context_callback;
/*
@ -5888,10 +5944,10 @@ RestoreGUCState(void *gucstate)
* also ensures that set_config_option won't refuse to set them because of
* source-priority comparisons.
*/
hash_seq_init(&status, guc_hashtab);
while ((hentry = (GUCHashEntry *) hash_seq_search(&status)) != NULL)
dlist_foreach_modify(iter, &guc_nondef_list)
{
struct config_generic *gconf = hentry->gucvar;
struct config_generic *gconf = dlist_container(struct config_generic,
nondef_link, iter.cur);
/* Do nothing if non-shippable or if already at PGC_S_DEFAULT. */
if (can_skip_gucvar(gconf))
@ -5902,7 +5958,8 @@ RestoreGUCState(void *gucstate)
* first we must free any existing subsidiary data to avoid leaking
* memory. The stack must be empty, but we have to clean up all other
* fields. Beware that there might be duplicate value or "extra"
* pointers.
* pointers. We also have to be sure to take it out of any lists it's
* in.
*/
Assert(gconf->stack == NULL);
guc_free(gconf->extra);
@ -5954,6 +6011,8 @@ RestoreGUCState(void *gucstate)
break;
}
}
/* Remove it from any lists it's in. */
RemoveGUCFromLists(gconf);
/* Now we can reset the struct to PGS_S_DEFAULT state. */
InitializeOneGUCOption(gconf);
}

View File

@ -14,6 +14,7 @@
#ifndef GUC_TABLES_H
#define GUC_TABLES_H 1
#include "lib/ilist.h"
#include "utils/guc.h"
/*
@ -138,6 +139,11 @@ typedef struct guc_stack
* if the value came from an internal source or the config file. Similarly
* for reset_srole (which is usually BOOTSTRAP_SUPERUSERID, but not always).
*
* Variables that are currently of active interest for maintenance
* operations are linked into various lists using the xxx_link fields.
* The link fields are unused/garbage in variables not currently having
* the specified properties.
*
* Note that sourcefile/sourceline are kept here, and not pushed into stacked
* values, although in principle they belong with some stacked value if the
* active value is session- or transaction-local. This is to avoid bloating
@ -163,6 +169,12 @@ struct config_generic
Oid reset_srole; /* role that set the reset value */
GucStack *stack; /* stacked prior values */
void *extra; /* "extra" pointer for current actual value */
dlist_node nondef_link; /* list link for variables that have source
* different from PGC_S_DEFAULT */
slist_node stack_link; /* list link for variables that have non-NULL
* stack */
slist_node report_link; /* list link for variables that have the
* GUC_NEEDS_REPORT bit set in status */
char *last_reported; /* if variable is GUC_REPORT, value last sent
* to client (NULL if not yet sent) */
char *sourcefile; /* file current setting is from (NULL if not