Avoid overhead open-close indexes (catalog updates)

Started by Ranier Vilelaover 3 years ago13 messages
#1Ranier Vilela
ranier.vf@gmail.com
1 attachment(s)

Hi,

The commit
https://github.com/postgres/postgres/commit/b17ff07aa3eb142d2cde2ea00e4a4e8f63686f96
Introduced the CopyStatistics function.

To do the work, CopyStatistics uses a less efficient function
to update/insert tuples at catalog systems.

The comment at indexing.c says:
"Avoid using it for multiple tuples, since opening the indexes
* and building the index info structures is moderately expensive.
* (Use CatalogTupleInsertWithInfo in such cases.)"

So inspired by the comment, changed in some fews places,
the CatalogInsert/CatalogUpdate to more efficient functions
CatalogInsertWithInfo/CatalogUpdateWithInfo.

With quick tests, resulting in small performance.

head:

1. REINDEX TABLE CONCURRENTLY pgbench_accounts;
Time: 77,805 ms
Time: 74,836 ms
Time: 73,480 ms

2. REINDEX TABLE CONCURRENTLY pgbench_tellers;
Time: 22,260 ms
Time: 22,205 ms
Time: 21,008 ms

patched:

1. REINDEX TABLE CONCURRENTLY pgbench_accounts;
Time: 65,048 ms
Time: 61,853 ms
Time: 61,119 ms

2. REINDEX TABLE CONCURRENTLY pgbench_tellers;
Time: 15,999 ms
Time: 15,961 ms
Time: 13,264 ms

There are other places that this could be useful,
but a careful analysis is necessary.

regards,
Ranier Vilela

Attachments:

0001-avoid-overhead-open-close-indexes.patchtext/x-patch; charset=US-ASCII; name=0001-avoid-overhead-open-close-indexes.patchDownload
diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 9b03579e6e..cf15051a05 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2856,8 +2856,7 @@ CopyStatistics(Oid fromrelid, Oid torelid)
 	SysScanDesc scan;
 	ScanKeyData key[1];
 	Relation	statrel;
-
-	statrel = table_open(StatisticRelationId, RowExclusiveLock);
+	CatalogIndexState indstate;
 
 	/* Now search for stat records */
 	ScanKeyInit(&key[0],
@@ -2865,6 +2864,9 @@ CopyStatistics(Oid fromrelid, Oid torelid)
 				BTEqualStrategyNumber, F_OIDEQ,
 				ObjectIdGetDatum(fromrelid));
 
+	statrel = table_open(StatisticRelationId, RowExclusiveLock);
+	indstate = CatalogOpenIndexes(statrel);
+
 	scan = systable_beginscan(statrel, StatisticRelidAttnumInhIndexId,
 							  true, NULL, 1, key);
 
@@ -2878,13 +2880,14 @@ CopyStatistics(Oid fromrelid, Oid torelid)
 
 		/* update the copy of the tuple and insert it */
 		statform->starelid = torelid;
-		CatalogTupleInsert(statrel, tup);
+		CatalogTupleInsertWithInfo(statrel, tup, indstate);
 
 		heap_freetuple(tup);
 	}
 
 	systable_endscan(scan);
 
+	CatalogCloseIndexes(indstate);
 	table_close(statrel, RowExclusiveLock);
 }
 
diff --git a/src/backend/catalog/pg_enum.c b/src/backend/catalog/pg_enum.c
index 114715498d..785fbc1669 100644
--- a/src/backend/catalog/pg_enum.c
+++ b/src/backend/catalog/pg_enum.c
@@ -69,6 +69,7 @@ EnumValuesCreate(Oid enumTypeOid, List *vals)
 	bool		nulls[Natts_pg_enum];
 	ListCell   *lc;
 	HeapTuple	tup;
+	CatalogIndexState indstate;
 
 	num_elems = list_length(vals);
 
@@ -113,6 +114,9 @@ EnumValuesCreate(Oid enumTypeOid, List *vals)
 	/* and make the entries */
 	memset(nulls, false, sizeof(nulls));
 
+	/* open the Catalog Indexes for Insert Tuples */
+	indstate = CatalogOpenIndexes(pg_enum);
+
 	elemno = 0;
 	foreach(lc, vals)
 	{
@@ -137,7 +141,7 @@ EnumValuesCreate(Oid enumTypeOid, List *vals)
 
 		tup = heap_form_tuple(RelationGetDescr(pg_enum), values, nulls);
 
-		CatalogTupleInsert(pg_enum, tup);
+		CatalogTupleInsertWithInfo(pg_enum, tup, indstate);
 		heap_freetuple(tup);
 
 		elemno++;
@@ -145,6 +149,7 @@ EnumValuesCreate(Oid enumTypeOid, List *vals)
 
 	/* clean up */
 	pfree(oids);
+	CatalogCloseIndexes(indstate);
 	table_close(pg_enum, RowExclusiveLock);
 }
 
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index a7966fff83..5cfcdf8ba7 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -1624,12 +1624,14 @@ static void
 update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 {
 	Relation	sd;
+	CatalogIndexState indstate;
 	int			attno;
 
 	if (natts <= 0)
 		return;					/* nothing to do */
 
 	sd = table_open(StatisticRelationId, RowExclusiveLock);
+	indstate = CatalogOpenIndexes(sd);
 
 	for (attno = 0; attno < natts; attno++)
 	{
@@ -1735,18 +1737,19 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 									 nulls,
 									 replaces);
 			ReleaseSysCache(oldtup);
-			CatalogTupleUpdate(sd, &stup->t_self, stup);
+			CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
 		}
 		else
 		{
 			/* No, insert new tuple */
 			stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
-			CatalogTupleInsert(sd, stup);
+			CatalogTupleInsertWithInfo(sd, stup, indstate);
 		}
 
 		heap_freetuple(stup);
 	}
 
+	CatalogCloseIndexes(indstate);
 	table_close(sd, RowExclusiveLock);
 }
 
diff --git a/src/backend/commands/tsearchcmds.c b/src/backend/commands/tsearchcmds.c
index 4cc4e3c00f..b95b989c5a 100644
--- a/src/backend/commands/tsearchcmds.c
+++ b/src/backend/commands/tsearchcmds.c
@@ -1004,14 +1004,16 @@ DefineTSConfiguration(List *names, List *parameters, ObjectAddress *copied)
 		ScanKeyData skey;
 		SysScanDesc scan;
 		HeapTuple	maptup;
-
-		mapRel = table_open(TSConfigMapRelationId, RowExclusiveLock);
+		CatalogIndexState indstate;
 
 		ScanKeyInit(&skey,
 					Anum_pg_ts_config_map_mapcfg,
 					BTEqualStrategyNumber, F_OIDEQ,
 					ObjectIdGetDatum(sourceOid));
 
+		mapRel = table_open(TSConfigMapRelationId, RowExclusiveLock);
+		indstate = CatalogOpenIndexes(mapRel);
+
 		scan = systable_beginscan(mapRel, TSConfigMapIndexId, true,
 								  NULL, 1, &skey);
 
@@ -1032,12 +1034,14 @@ DefineTSConfiguration(List *names, List *parameters, ObjectAddress *copied)
 
 			newmaptup = heap_form_tuple(mapRel->rd_att, mapvalues, mapnulls);
 
-			CatalogTupleInsert(mapRel, newmaptup);
+			CatalogTupleInsertWithInfo(mapRel, newmaptup, indstate);
 
 			heap_freetuple(newmaptup);
 		}
 
 		systable_endscan(scan);
+
+		CatalogCloseIndexes(indstate);
 	}
 
 	address = makeConfigurationDependencies(tup, false, mapRel);
@@ -1225,6 +1229,7 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt,
 	Oid		   *dictIds;
 	int			ndict;
 	ListCell   *c;
+	CatalogIndexState indstate;
 
 	tsform = (Form_pg_ts_config) GETSTRUCT(tup);
 	cfgId = tsform->oid;
@@ -1275,6 +1280,9 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt,
 		i++;
 	}
 
+	/* open the Catalog Indexes for Update/Insert Tuples */
+	indstate = CatalogOpenIndexes(relMap);
+
 	if (stmt->replace)
 	{
 		/*
@@ -1334,7 +1342,7 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt,
 				newtup = heap_modify_tuple(maptup,
 										   RelationGetDescr(relMap),
 										   repl_val, repl_null, repl_repl);
-				CatalogTupleUpdate(relMap, &newtup->t_self, newtup);
+				CatalogTupleUpdateWithInfo(relMap, &newtup->t_self, newtup, indstate);
 			}
 		}
 
@@ -1359,12 +1367,13 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt,
 				values[Anum_pg_ts_config_map_mapdict - 1] = ObjectIdGetDatum(dictIds[j]);
 
 				tup = heap_form_tuple(relMap->rd_att, values, nulls);
-				CatalogTupleInsert(relMap, tup);
+				CatalogTupleInsertWithInfo(relMap, tup, indstate);
 
 				heap_freetuple(tup);
 			}
 		}
 	}
+	CatalogCloseIndexes(indstate);
 
 	EventTriggerCollectAlterTSConfig(stmt, cfgId, dictIds, ndict);
 }
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 265a48af7e..f9521bc1dd 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1551,6 +1551,7 @@ AddRoleMems(const char *rolename, Oid roleid,
 {
 	Relation	pg_authmem_rel;
 	TupleDesc	pg_authmem_dsc;
+	CatalogIndexState indstate;
 	ListCell   *specitem;
 	ListCell   *iditem;
 	Oid			currentUserId = GetUserId();
@@ -1718,6 +1719,9 @@ AddRoleMems(const char *rolename, Oid roleid,
 		ReleaseSysCacheList(memlist);
 	}
 
+	/* open the Catalog Indexes for Update/Insert Tuples */
+	indstate = CatalogOpenIndexes(pg_authmem_rel);
+
 	/* Now perform the catalog updates. */
 	forboth(specitem, memberSpecs, iditem, memberIds)
 	{
@@ -1789,7 +1793,7 @@ AddRoleMems(const char *rolename, Oid roleid,
 			tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
 									  new_record,
 									  new_record_nulls, new_record_repl);
-			CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
+			CatalogTupleUpdateWithInfo(pg_authmem_rel, &tuple->t_self, tuple, indstate);
 
 			ReleaseSysCache(authmem_tuple);
 		}
@@ -1829,7 +1833,7 @@ AddRoleMems(const char *rolename, Oid roleid,
 			new_record[Anum_pg_auth_members_oid - 1] = objectId;
 			tuple = heap_form_tuple(pg_authmem_dsc,
 									new_record, new_record_nulls);
-			CatalogTupleInsert(pg_authmem_rel, tuple);
+			CatalogTupleInsertWithInfo(pg_authmem_rel, tuple, indstate);
 
 			/* updateAclDependencies wants to pfree array inputs */
 			newmembers[0] = grantorId;
@@ -1842,6 +1846,7 @@ AddRoleMems(const char *rolename, Oid roleid,
 		/* CCI after each change, in case there are duplicates in list */
 		CommandCounterIncrement();
 	}
+	CatalogCloseIndexes(indstate);
 
 	/*
 	 * Close pg_authmem, but keep lock till commit.
#2Kyotaro Horiguchi
horikyota.ntt@gmail.com
In reply to: Ranier Vilela (#1)
Re: Avoid overhead open-close indexes (catalog updates)

At Wed, 31 Aug 2022 08:16:55 -0300, Ranier Vilela <ranier.vf@gmail.com> wrote in

Hi,

The commit
https://github.com/postgres/postgres/commit/b17ff07aa3eb142d2cde2ea00e4a4e8f63686f96
Introduced the CopyStatistics function.

To do the work, CopyStatistics uses a less efficient function
to update/insert tuples at catalog systems.

The comment at indexing.c says:
"Avoid using it for multiple tuples, since opening the indexes
* and building the index info structures is moderately expensive.
* (Use CatalogTupleInsertWithInfo in such cases.)"

So inspired by the comment, changed in some fews places,
the CatalogInsert/CatalogUpdate to more efficient functions
CatalogInsertWithInfo/CatalogUpdateWithInfo.

With quick tests, resulting in small performance.

Considering the whole operation usually takes far longer time, I'm not
sure that amount of performance gain is useful or not, but I like the
change as a matter of tidiness or as example for later codes.

There are other places that this could be useful,
but a careful analysis is necessary.

What kind of concern do have in your mind?

By the way, there is another similar function
CatalogTupleMultiInsertWithInfo() which would be more time-efficient
(but not space-efficient), which is used in InsertPgAttributeTuples. I
don't see a clear criteria of choosing which one of the two, though.

I think the overhead of catalog index open is significant when any
other time-consuming tasks are not involved in the whole operation.
In that sense, in term of performance, rather storeOperations and
storePrecedures (called under DefineOpCalss) might get more benefit
from that if disregarding the rareness of the command being used..

regards.

--
Kyotaro Horiguchi
NTT Open Source Software Center

#3Ranier Vilela
ranier.vf@gmail.com
In reply to: Kyotaro Horiguchi (#2)
Re: Avoid overhead open-close indexes (catalog updates)

Em qua., 31 de ago. de 2022 às 22:12, Kyotaro Horiguchi <
horikyota.ntt@gmail.com> escreveu:

At Wed, 31 Aug 2022 08:16:55 -0300, Ranier Vilela <ranier.vf@gmail.com>
wrote in

Hi,

The commit

https://github.com/postgres/postgres/commit/b17ff07aa3eb142d2cde2ea00e4a4e8f63686f96

Introduced the CopyStatistics function.

To do the work, CopyStatistics uses a less efficient function
to update/insert tuples at catalog systems.

The comment at indexing.c says:
"Avoid using it for multiple tuples, since opening the indexes
* and building the index info structures is moderately expensive.
* (Use CatalogTupleInsertWithInfo in such cases.)"

So inspired by the comment, changed in some fews places,
the CatalogInsert/CatalogUpdate to more efficient functions
CatalogInsertWithInfo/CatalogUpdateWithInfo.

With quick tests, resulting in small performance.

Hi,
Thanks for taking a look at this.

Considering the whole operation usually takes far longer time, I'm not
sure that amount of performance gain is useful or not, but I like the
change as a matter of tidiness or as example for later codes.

Yeah, this serves as an example for future codes.

There are other places that this could be useful,
but a careful analysis is necessary.

What kind of concern do have in your mind?

Code Bloat.
3 more lines are required per call (CatalogTupleInsert/CatalogTupleUpdate).
However not all code paths are reachable.
The ideal typical case would be CopyStatistics, I think.
With none or at least one filter in tuples loop.
The cost to call CatalogOpenIndexes unconditionally, should be considered.

By the way, there is another similar function
CatalogTupleMultiInsertWithInfo() which would be more time-efficient
(but not space-efficient), which is used in InsertPgAttributeTuples. I
don't see a clear criteria of choosing which one of the two, though.

I don't think CatalogTupleMultiInsertWithInfo would be useful in these

cases reported here.
The cost of building the slots I think would be unfeasible and would add
unnecessary complexity.

I think the overhead of catalog index open is significant when any
other time-consuming tasks are not involved in the whole operation.
In that sense, in term of performance, rather storeOperations and
storePrecedures (called under DefineOpCalss) might get more benefit
from that if disregarding the rareness of the command being used..

Yeah, storeOperations and storePrecedures are good candidates.

Let's wait for the patch to be accepted and committed, so we can try to
change it.

I will create a CF entry.

regards,
Ranier Vilela

#4Michael Paquier
michael@paquier.xyz
In reply to: Ranier Vilela (#3)
Re: Avoid overhead open-close indexes (catalog updates)

On Thu, Sep 01, 2022 at 08:42:15AM -0300, Ranier Vilela wrote:

Let's wait for the patch to be accepted and committed, so we can try to
change it.

FWIW, I think that this switch is a good idea for cases where we
potentially update a bunch of tuples, especially based on what
CatalogTupleInsert() tells in its top comment. Each code path updated
here needs a performance check to see if that's noticeable enough, but
I can get behind the one of CopyStatistics(), at least.

EnumValuesCreate() would matter less as this would require a large set
of values in an enum, but perhaps ORMs would care and that should be
measurable. update_attstats() should lead to a measurable difference
with a relation that has a bunch of attributes with few tuples.
DefineTSConfiguration() is less of an issue, still fine to change.
AddRoleMems() should be equally measurable with a large DDL. As a
whole, this looks pretty sane to me and a good idea to move on with.

I still need to check properly the code paths changed here, of
course..
--
Michael

#5Ranier Vilela
ranier.vf@gmail.com
In reply to: Michael Paquier (#4)
Re: Avoid overhead open-close indexes (catalog updates)

Em qui., 10 de nov. de 2022 às 05:16, Michael Paquier <michael@paquier.xyz>
escreveu:

On Thu, Sep 01, 2022 at 08:42:15AM -0300, Ranier Vilela wrote:

Let's wait for the patch to be accepted and committed, so we can try to
change it.

FWIW, I think that this switch is a good idea for cases where we
potentially update a bunch of tuples, especially based on what
CatalogTupleInsert() tells in its top comment.

That's the idea.

Each code path updated
here needs a performance check to see if that's noticeable enough, but
I can get behind the one of CopyStatistics(), at least.

For CopyStatistics() have performance checks.

EnumValuesCreate() would matter less as this would require a large set
of values in an enum, but perhaps ORMs would care and that should be
measurable.

Have a list_length call, for a number of vals.
For 2 or more vals, it is already worth it, since
CatalogOpenIndexes/CatalogCloseIndexes will be called for each val.

update_attstats() should lead to a measurable difference
with a relation that has a bunch of attributes with few tuples.

Same here.
For 2 or more attributes, it is already worth it, since
CatalogOpenIndexes/CatalogCloseIndexes will be called for each.

DefineTSConfiguration() is less of an issue, still fine to change.

Ok.

AddRoleMems() should be equally measurable with a large DDL. As a

whole, this looks pretty sane to me and a good idea to move on with.

One filter, only.

For all these functions, the only case that would possibly have no effect
would be in the case of changing a single tuple, in which case there would
be only one call CatalogOpenIndexes/CatalogCloseIndexes for both paths.

I still need to check properly the code paths changed here, of
course..

At least, the patch still applies properly.

regards,
Ranier Vilela

#6Michael Paquier
michael@paquier.xyz
In reply to: Ranier Vilela (#5)
1 attachment(s)
Re: Avoid overhead open-close indexes (catalog updates)

On Thu, Nov 10, 2022 at 08:56:25AM -0300, Ranier Vilela wrote:

For CopyStatistics() have performance checks.

You are not giving all the details of your tests, though, so I had a
look with some of my stuff using the attached set of SQL functions
(create_function.sql) to create a bunch of indexes with a maximum
number of expressions, as of:
select create_table_cols('tab', 32);
select create_index_multi_exprs('ind', 400, 'tab', 32);
insert into tab values (1);
analyze tab; -- 12.8k~ pg_statistic records

On HEAD, a REINDEX CONCURRENTLY for the table 'tab' takes 1550ms on my
laptop with an average of 10 runs. The patch impacts the runtime with
a single session, making the execution down to 1480ms as per an effect
of the maximum number of attributes on an index being 32. There may
be some noise, but there is a trend, and some perf profiles confirm
the same with CopyStatistics(). My case is a bit extreme, of course,
still that's something.

Anyway, while reviewing this code, it occured to me that we could do
even better than this proposal once we switch to
CatalogTuplesMultiInsertWithInfo() for the data insertion.

This would reduce more the operation overhead by switching to multi
INSERTs rather than 1 INSERT for each index attribute with tuples
stored in a set of TupleTableSlots, meaning 1 WAL record rather than N
records. The approach would be similar to what you do for
dependencies, see for example recordMultipleDependencies() when it
comes to the number of slots used etc.
--
Michael

Attachments:

create_function.sqlapplication/sqlDownload
#7Ranier Vilela
ranier.vf@gmail.com
In reply to: Michael Paquier (#6)
Re: Avoid overhead open-close indexes (catalog updates)

Em sex., 11 de nov. de 2022 às 01:54, Michael Paquier <michael@paquier.xyz>
escreveu:

On Thu, Nov 10, 2022 at 08:56:25AM -0300, Ranier Vilela wrote:

For CopyStatistics() have performance checks.

You are not giving all the details of your tests, though,

Windows 10 64 bits
SSD 256 GB

pgbench -i
pgbench_accounts;
pgbench_tellers;

Simple test, based on tables created by pgbench.

so I had a
look with some of my stuff using the attached set of SQL functions
(create_function.sql) to create a bunch of indexes with a maximum
number of expressions, as of:
select create_table_cols('tab', 32);
select create_index_multi_exprs('ind', 400, 'tab', 32);
insert into tab values (1);
analyze tab; -- 12.8k~ pg_statistic records

On HEAD, a REINDEX CONCURRENTLY for the table 'tab' takes 1550ms on my
laptop with an average of 10 runs. The patch impacts the runtime with
a single session, making the execution down to 1480ms as per an effect
of the maximum number of attributes on an index being 32. There may
be some noise, but there is a trend, and some perf profiles confirm
the same with CopyStatistics(). My case is a bit extreme, of course,
still that's something.

Anyway, while reviewing this code, it occured to me that we could do
even better than this proposal once we switch to
CatalogTuplesMultiInsertWithInfo() for the data insertion.

This would reduce more the operation overhead by switching to multi
INSERTs rather than 1 INSERT for each index attribute with tuples
stored in a set of TupleTableSlots, meaning 1 WAL record rather than N
records. The approach would be similar to what you do for
dependencies, see for example recordMultipleDependencies() when it
comes to the number of slots used etc.

I think complexity doesn't pay off.
For example, CopyStatistics not knowing how many tuples will be processed.
IMHO, this step is right now.
CatalogTupleInsertWithInfo offers considerable improvement without
introducing bugs and maintenance issues.

regards,
Ranier Vilela

#8Michael Paquier
michael@paquier.xyz
In reply to: Ranier Vilela (#7)
Re: Avoid overhead open-close indexes (catalog updates)

On Sat, Nov 12, 2022 at 11:03:46AM -0300, Ranier Vilela wrote:

I think complexity doesn't pay off.
For example, CopyStatistics not knowing how many tuples will be processed.
IMHO, this step is right now.
CatalogTupleInsertWithInfo offers considerable improvement without
introducing bugs and maintenance issues.

Considerable may be a bit an overstatement? I can see a difference in
profiles when switching from one to the other in some extreme cases,
but for the REINDEX CONCURRENTLY case most of the runtime is going to
be eaten in the wait phases, the index build and its validation.

Anyway, multi-inserts are going to be solution better than
CatalogTupleInsertWithInfo() in some cases, because we would just
generate one WAL record of N inserts rather than N records with one
INSERT each.

Looking closely, EnumValuesCreate() is a DDL path but I'd like to
think that two enum values are at least present at creation in most
cases. AddRoleMems() becomes relevant when using more than one role,
which is a less common pattern, so I'd be fine with switching to a
single index-opening approach with CatalogTupleUpdateWithInfo() as you
suggest without the tuple slot management. CopyStatistics() does not
know in advance the number of tuples it would insert, and it would be
a gain when there are more than 2 expressions with entries in
pg_statistic as of HEAD. Perhaps you're right with your simple
suggestion to stick with CatalogTupleUpdateWithInfo() in this case.
Maybe there is some external code calling this routine for tables, who
knows.

update_attstats() is actually an area that cannot be changed now that
I look at it, as we could finish to update some entries, so the slot
approach will not be relevant, but using CatalogTupleUpdateWithInfo()
is. (As a matter of fact, the regression test suite is reporting that
update_attstats() is called for one attribute 10% of the time, did not
check the insert/update rate though).

Would you like to give a try with the tuple slot management in
EnumValuesCreate()?
--
Michael

#9Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#8)
1 attachment(s)
Re: Avoid overhead open-close indexes (catalog updates)

On Tue, Nov 15, 2022 at 09:57:26AM +0900, Michael Paquier wrote:

Anyway, multi-inserts are going to be solution better than
CatalogTupleInsertWithInfo() in some cases, because we would just
generate one WAL record of N inserts rather than N records with one
INSERT each.

Something that you did not consider in the initial patch is that we
may finish by opening catalog indexes even in cases where this would
not have happened on HEAD, as we may finish by doing nothing when
copying the stats or updating them during an analyze, and that's not
fine IMO. However it is easy enough to minimize the cost: just do a
CatalogOpenIndexes() when absolutely required, and close things only
if the indexes have been opened.

Then, there are the cases where it is worth switching to a
multi-insert logic as these are going to manipulate more than 2
entries all the time: enum list addition and two code paths of
tsearchcmds.c (where up to 16 entries can be lined up). This is a
case-by-case analysis. For example, in the case of the enums, the
number of elements is known in advance so it is possible to know the
number of slots that would be used and initialize them. But that's
not something you would do for the first tsearch bits where the data
is built upon a scan so the slot init should be delayed. The second
tsearch one can use a predictible approach, like the enums based on
the number of known elements to insert.

So I've given a try at all that, and finished with the attached. This
patch finishes with a list of bullet points, so this had better be
split into different commits, I guess.
Thoughts?
--
Michael

Attachments:

v2-0001-Avoid-some-overhead-with-some-catalog-inserts-and.patchtext/x-diff; charset=us-asciiDownload
From 4dd7d8a7670343132d8ba6fb032eaf86ec724378 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 15 Nov 2022 15:44:45 +0900
Subject: [PATCH v2] Avoid some overhead with some catalog inserts and updates

This commit makes lighter a couple of code paths that do catalog updates
and inserts.

First, CatalogTupleInsert() is costly when using it for the insertion or the
update of multiple tuples compared to CatalogTupleInsertWithInfo(), as
it would need to open and close the indexes of the catalog worked on
multiple times.  It happens that some code paths use
CatalogTupleInsert() while CatalogTupleInsertWithInfo() would be a
better choice, and the following ones are changed in this commit:
- REINDEX CONCURRENTLY when copying statistics from one index relation
to the other.  Multi-INSERTs are avoided here, as this would begin to
show benefits on indexes with multiple expressions, for example, which
may not be the most common pattern.  This happens to be noticeable on
profiles with indexes having many expressions in a worst case scenario.
- Update of statistics on ANALYZE, that mixes inserts and updates.
In each case, the catalog indexes are opened only if at least one
insertion and/or update is required, to minimize the cost of the
operation.

Second, a few code paths are switched from CatalogTupleInsert() to
a multi-insert approach through tuple slots:
- ALTER TEXT SEARCH CONFIGURATION ADD/ALTER MAPPING when inserting new
entries.  The replacement logic switches to CatalogTupleInsertWithInfo()
for the updates.
- CREATE TEXT SEARCH CONFIGURATION.
- CREATE/ALTER TYPE AS ENUM when adding a list of enum values.
These use a case-by-case logic, so as the initialization of tuple slots
is done only when necessary to minimize the cost of the operation.

Author: Ranier Vilela, Michael Paquier
Reviewed-by: Kyotaro Horiguchi, Michael Paquier
Discussion: https://postgr.es/m/CAEudQAqh0F9y6Di_Wc8xW4zkWm_5SDd-nRfVsCn=h0Nm1C_mrg@mail.gmail.com
---
 src/backend/catalog/heap.c         |  10 ++-
 src/backend/catalog/pg_enum.c      |  55 +++++++++----
 src/backend/commands/analyze.c     |  11 ++-
 src/backend/commands/tsearchcmds.c | 120 +++++++++++++++++++++++------
 4 files changed, 157 insertions(+), 39 deletions(-)

diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c
index 5b49cc5a09..bdd413f01b 100644
--- a/src/backend/catalog/heap.c
+++ b/src/backend/catalog/heap.c
@@ -2856,6 +2856,7 @@ CopyStatistics(Oid fromrelid, Oid torelid)
 	SysScanDesc scan;
 	ScanKeyData key[1];
 	Relation	statrel;
+	CatalogIndexState indstate = NULL;
 
 	statrel = table_open(StatisticRelationId, RowExclusiveLock);
 
@@ -2878,13 +2879,20 @@ CopyStatistics(Oid fromrelid, Oid torelid)
 
 		/* update the copy of the tuple and insert it */
 		statform->starelid = torelid;
-		CatalogTupleInsert(statrel, tup);
+
+		/* fetch index information when we know we need it */
+		if (indstate == NULL)
+			indstate = CatalogOpenIndexes(statrel);
+
+		CatalogTupleInsertWithInfo(statrel, tup, indstate);
 
 		heap_freetuple(tup);
 	}
 
 	systable_endscan(scan);
 
+	if (indstate != NULL)
+		CatalogCloseIndexes(indstate);
 	table_close(statrel, RowExclusiveLock);
 }
 
diff --git a/src/backend/catalog/pg_enum.c b/src/backend/catalog/pg_enum.c
index 114715498d..ee11f5f6f1 100644
--- a/src/backend/catalog/pg_enum.c
+++ b/src/backend/catalog/pg_enum.c
@@ -61,14 +61,14 @@ void
 EnumValuesCreate(Oid enumTypeOid, List *vals)
 {
 	Relation	pg_enum;
-	NameData	enumlabel;
 	Oid		   *oids;
 	int			elemno,
 				num_elems;
-	Datum		values[Natts_pg_enum];
-	bool		nulls[Natts_pg_enum];
 	ListCell   *lc;
-	HeapTuple	tup;
+	int			slotCount = 0;
+	int			nslots;
+	CatalogIndexState indstate;
+	TupleTableSlot **slot;
 
 	num_elems = list_length(vals);
 
@@ -111,12 +111,21 @@ EnumValuesCreate(Oid enumTypeOid, List *vals)
 	qsort(oids, num_elems, sizeof(Oid), oid_cmp);
 
 	/* and make the entries */
-	memset(nulls, false, sizeof(nulls));
+	indstate = CatalogOpenIndexes(pg_enum);
+
+	/* allocate the slots to use and initialize them */
+	nslots = Min(num_elems,
+				 MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_enum));
+	slot = palloc(sizeof(TupleTableSlot *) * nslots);
+	for (int i = 0; i < nslots; i++)
+		slot[i] = MakeSingleTupleTableSlot(RelationGetDescr(pg_enum),
+										   &TTSOpsHeapTuple);
 
 	elemno = 0;
 	foreach(lc, vals)
 	{
 		char	   *lab = strVal(lfirst(lc));
+		NameData   *enumlabel = palloc0(NAMEDATALEN);
 
 		/*
 		 * labels are stored in a name field, for easier syscache lookup, so
@@ -129,22 +138,42 @@ EnumValuesCreate(Oid enumTypeOid, List *vals)
 					 errdetail("Labels must be %d bytes or less.",
 							   NAMEDATALEN - 1)));
 
-		values[Anum_pg_enum_oid - 1] = ObjectIdGetDatum(oids[elemno]);
-		values[Anum_pg_enum_enumtypid - 1] = ObjectIdGetDatum(enumTypeOid);
-		values[Anum_pg_enum_enumsortorder - 1] = Float4GetDatum(elemno + 1);
-		namestrcpy(&enumlabel, lab);
-		values[Anum_pg_enum_enumlabel - 1] = NameGetDatum(&enumlabel);
+		ExecClearTuple(slot[slotCount]);
 
-		tup = heap_form_tuple(RelationGetDescr(pg_enum), values, nulls);
+		memset(slot[slotCount]->tts_isnull, false,
+			   slot[slotCount]->tts_tupleDescriptor->natts * sizeof(bool));
 
-		CatalogTupleInsert(pg_enum, tup);
-		heap_freetuple(tup);
+		slot[slotCount]->tts_values[Anum_pg_enum_oid - 1] = ObjectIdGetDatum(oids[elemno]);
+		slot[slotCount]->tts_values[Anum_pg_enum_enumtypid - 1] = ObjectIdGetDatum(enumTypeOid);
+		slot[slotCount]->tts_values[Anum_pg_enum_enumsortorder - 1] = Float4GetDatum(elemno + 1);
+
+		namestrcpy(enumlabel, lab);
+		slot[slotCount]->tts_values[Anum_pg_enum_enumlabel - 1] = NameGetDatum(enumlabel);
+
+		ExecStoreVirtualTuple(slot[slotCount]);
+		slotCount++;
+
+		/* if slots are full, insert a batch of tuples */
+		if (slotCount == nslots)
+		{
+			CatalogTuplesMultiInsertWithInfo(pg_enum, slot, slotCount,
+											 indstate);
+			slotCount = 0;
+		}
 
 		elemno++;
 	}
 
+	/* Insert any tuples left in the buffer */
+	if (slotCount > 0)
+		CatalogTuplesMultiInsertWithInfo(pg_enum, slot, slotCount,
+										 indstate);
+
 	/* clean up */
 	pfree(oids);
+	for (int i = 0; i < nslots; i++)
+		ExecDropSingleTupleTableSlot(slot[i]);
+	CatalogCloseIndexes(indstate);
 	table_close(pg_enum, RowExclusiveLock);
 }
 
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index ff1354812b..bf0ec8b374 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -1624,6 +1624,7 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 {
 	Relation	sd;
 	int			attno;
+	CatalogIndexState indstate = NULL;
 
 	if (natts <= 0)
 		return;					/* nothing to do */
@@ -1725,6 +1726,10 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 								 Int16GetDatum(stats->attr->attnum),
 								 BoolGetDatum(inh));
 
+		/* Open index information when we know we need it */
+		if (indstate == NULL)
+			indstate = CatalogOpenIndexes(sd);
+
 		if (HeapTupleIsValid(oldtup))
 		{
 			/* Yes, replace it */
@@ -1734,18 +1739,20 @@ update_attstats(Oid relid, bool inh, int natts, VacAttrStats **vacattrstats)
 									 nulls,
 									 replaces);
 			ReleaseSysCache(oldtup);
-			CatalogTupleUpdate(sd, &stup->t_self, stup);
+			CatalogTupleUpdateWithInfo(sd, &stup->t_self, stup, indstate);
 		}
 		else
 		{
 			/* No, insert new tuple */
 			stup = heap_form_tuple(RelationGetDescr(sd), values, nulls);
-			CatalogTupleInsert(sd, stup);
+			CatalogTupleInsertWithInfo(sd, stup, indstate);
 		}
 
 		heap_freetuple(stup);
 	}
 
+	if (indstate != NULL)
+		CatalogCloseIndexes(indstate);
 	table_close(sd, RowExclusiveLock);
 }
 
diff --git a/src/backend/commands/tsearchcmds.c b/src/backend/commands/tsearchcmds.c
index 9304c53d4b..67f723e908 100644
--- a/src/backend/commands/tsearchcmds.c
+++ b/src/backend/commands/tsearchcmds.c
@@ -1004,8 +1004,24 @@ DefineTSConfiguration(List *names, List *parameters, ObjectAddress *copied)
 		ScanKeyData skey;
 		SysScanDesc scan;
 		HeapTuple	maptup;
+		TupleDesc   mapDesc;
+		TupleTableSlot **slot;
+		CatalogIndexState indstate;
+		int         max_slots,
+			slot_init_count,
+			slot_stored_count;
 
 		mapRel = table_open(TSConfigMapRelationId, RowExclusiveLock);
+		mapDesc = RelationGetDescr(mapRel);
+
+		indstate = CatalogOpenIndexes(mapRel);
+
+		/*
+		 * Allocate the slots to use, but delay costly initialization until we
+		 * know that they will be used.
+		 */
+		max_slots = MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_ts_config_map);
+		slot = palloc(sizeof(TupleTableSlot *) * max_slots);
 
 		ScanKeyInit(&skey,
 					Anum_pg_ts_config_map_mapcfg,
@@ -1015,29 +1031,54 @@ DefineTSConfiguration(List *names, List *parameters, ObjectAddress *copied)
 		scan = systable_beginscan(mapRel, TSConfigMapIndexId, true,
 								  NULL, 1, &skey);
 
+		/* number of slots currently storing tuples */
+		slot_stored_count = 0;
+		/* number of slots currently initialized */
+		slot_init_count = 0;
+
 		while (HeapTupleIsValid((maptup = systable_getnext(scan))))
 		{
 			Form_pg_ts_config_map cfgmap = (Form_pg_ts_config_map) GETSTRUCT(maptup);
-			HeapTuple	newmaptup;
-			Datum		mapvalues[Natts_pg_ts_config_map];
-			bool		mapnulls[Natts_pg_ts_config_map];
 
-			memset(mapvalues, 0, sizeof(mapvalues));
-			memset(mapnulls, false, sizeof(mapnulls));
+			if (slot_init_count < max_slots)
+			{
+				slot[slot_stored_count] = MakeSingleTupleTableSlot(mapDesc,
+																   &TTSOpsHeapTuple);
+				slot_init_count++;
+			}
 
-			mapvalues[Anum_pg_ts_config_map_mapcfg - 1] = cfgOid;
-			mapvalues[Anum_pg_ts_config_map_maptokentype - 1] = cfgmap->maptokentype;
-			mapvalues[Anum_pg_ts_config_map_mapseqno - 1] = cfgmap->mapseqno;
-			mapvalues[Anum_pg_ts_config_map_mapdict - 1] = cfgmap->mapdict;
+			ExecClearTuple(slot[slot_stored_count]);
 
-			newmaptup = heap_form_tuple(mapRel->rd_att, mapvalues, mapnulls);
+			memset(slot[slot_stored_count]->tts_isnull, false,
+				   slot[slot_stored_count]->tts_tupleDescriptor->natts * sizeof(bool));
 
-			CatalogTupleInsert(mapRel, newmaptup);
+			slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapcfg - 1] = cfgOid;
+			slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_maptokentype - 1] = cfgmap->maptokentype;
+			slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapseqno - 1] = cfgmap->mapseqno;
+			slot[slot_stored_count]->tts_values[Anum_pg_ts_config_map_mapdict - 1] = cfgmap->mapdict;
 
-			heap_freetuple(newmaptup);
+			ExecStoreVirtualTuple(slot[slot_stored_count]);
+			slot_stored_count++;
+
+			/* If slots are full, insert a batch of tuples */
+			if (slot_stored_count == max_slots)
+			{
+				CatalogTuplesMultiInsertWithInfo(mapRel, slot, slot_stored_count,
+												 indstate);
+				slot_stored_count = 0;
+			}
 		}
 
+		/* Insert any tuples left in the buffer */
+		if (slot_stored_count > 0)
+			CatalogTuplesMultiInsertWithInfo(mapRel, slot, slot_stored_count,
+											 indstate);
+
+		for (int i = 0; i < slot_init_count; i++)
+			ExecDropSingleTupleTableSlot(slot[i]);
+
 		systable_endscan(scan);
+		CatalogCloseIndexes(indstate);
 	}
 
 	address = makeConfigurationDependencies(tup, false, mapRel);
@@ -1225,6 +1266,7 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt,
 	Oid		   *dictIds;
 	int			ndict;
 	ListCell   *c;
+	CatalogIndexState indstate;
 
 	tsform = (Form_pg_ts_config) GETSTRUCT(tup);
 	cfgId = tsform->oid;
@@ -1275,6 +1317,8 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt,
 		i++;
 	}
 
+	indstate = CatalogOpenIndexes(relMap);
+
 	if (stmt->replace)
 	{
 		/*
@@ -1334,7 +1378,7 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt,
 				newtup = heap_modify_tuple(maptup,
 										   RelationGetDescr(relMap),
 										   repl_val, repl_null, repl_repl);
-				CatalogTupleUpdate(relMap, &newtup->t_self, newtup);
+				CatalogTupleUpdateWithInfo(relMap, &newtup->t_self, newtup, indstate);
 			}
 		}
 
@@ -1342,6 +1386,18 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt,
 	}
 	else
 	{
+		TupleTableSlot **slot;
+		int			slotCount = 0;
+		int			nslots;
+
+		/* Allocate the slots to use and initialize them */
+		nslots = Min(ntoken * ndict,
+					 MAX_CATALOG_MULTI_INSERT_BYTES / sizeof(FormData_pg_ts_config_map));
+		slot = palloc(sizeof(TupleTableSlot *) * nslots);
+		for (i = 0; i < nslots; i++)
+			slot[i] = MakeSingleTupleTableSlot(RelationGetDescr(relMap),
+											   &TTSOpsHeapTuple);
+
 		/*
 		 * Insertion of new entries
 		 */
@@ -1349,23 +1405,41 @@ MakeConfigurationMapping(AlterTSConfigurationStmt *stmt,
 		{
 			for (j = 0; j < ndict; j++)
 			{
-				Datum		values[Natts_pg_ts_config_map];
-				bool		nulls[Natts_pg_ts_config_map];
+				ExecClearTuple(slot[slotCount]);
 
-				memset(nulls, false, sizeof(nulls));
-				values[Anum_pg_ts_config_map_mapcfg - 1] = ObjectIdGetDatum(cfgId);
-				values[Anum_pg_ts_config_map_maptokentype - 1] = Int32GetDatum(tokens[i]);
-				values[Anum_pg_ts_config_map_mapseqno - 1] = Int32GetDatum(j + 1);
-				values[Anum_pg_ts_config_map_mapdict - 1] = ObjectIdGetDatum(dictIds[j]);
+				memset(slot[slotCount]->tts_isnull, false,
+					   slot[slotCount]->tts_tupleDescriptor->natts * sizeof(bool));
 
-				tup = heap_form_tuple(relMap->rd_att, values, nulls);
-				CatalogTupleInsert(relMap, tup);
+				slot[slotCount]->tts_values[Anum_pg_ts_config_map_mapcfg - 1] = ObjectIdGetDatum(cfgId);
+				slot[slotCount]->tts_values[Anum_pg_ts_config_map_maptokentype - 1] = Int32GetDatum(tokens[i]);
+				slot[slotCount]->tts_values[Anum_pg_ts_config_map_mapseqno - 1] = Int32GetDatum(j + 1);
+				slot[slotCount]->tts_values[Anum_pg_ts_config_map_mapdict - 1] = ObjectIdGetDatum(dictIds[j]);
 
-				heap_freetuple(tup);
+				ExecStoreVirtualTuple(slot[slotCount]);
+				slotCount++;
+
+				/* If slots are full, insert a batch of tuples */
+				if (slotCount == nslots)
+				{
+					CatalogTuplesMultiInsertWithInfo(relMap, slot, slotCount,
+													 indstate);
+					slotCount = 0;
+				}
 			}
 		}
+
+		/* Insert any tuples left in the buffer */
+		if (slotCount > 0)
+			CatalogTuplesMultiInsertWithInfo(relMap, slot, slotCount,
+											 indstate);
+
+		for (i = 0; i < nslots; i++)
+			ExecDropSingleTupleTableSlot(slot[i]);
 	}
 
+	/* clean up */
+	CatalogCloseIndexes(indstate);
+
 	EventTriggerCollectAlterTSConfig(stmt, cfgId, dictIds, ndict);
 }
 
-- 
2.38.1

#10Ranier Vilela
ranier.vf@gmail.com
In reply to: Michael Paquier (#9)
Re: Avoid overhead open-close indexes (catalog updates)

Em ter., 15 de nov. de 2022 às 04:02, Michael Paquier <michael@paquier.xyz>
escreveu:

On Tue, Nov 15, 2022 at 09:57:26AM +0900, Michael Paquier wrote:

Anyway, multi-inserts are going to be solution better than
CatalogTupleInsertWithInfo() in some cases, because we would just
generate one WAL record of N inserts rather than N records with one
INSERT each.

Something that you did not consider in the initial patch is that we
may finish by opening catalog indexes even in cases where this would
not have happened on HEAD, as we may finish by doing nothing when
copying the stats or updating them during an analyze, and that's not
fine IMO. However it is easy enough to minimize the cost: just do a
CatalogOpenIndexes() when absolutely required, and close things only
if the indexes have been opened.

I find it very difficult not to have some tuple to be updated,
once inside CopyStatistics and the branch cost can get in the way,
but I don't object with your solution.

Then, there are the cases where it is worth switching to a
multi-insert logic as these are going to manipulate more than 2
entries all the time: enum list addition and two code paths of
tsearchcmds.c (where up to 16 entries can be lined up). This is a
case-by-case analysis. For example, in the case of the enums, the
number of elements is known in advance so it is possible to know the
number of slots that would be used and initialize them. But that's
not something you would do for the first tsearch bits where the data
is built upon a scan so the slot init should be delayed. The second
tsearch one can use a predictible approach, like the enums based on
the number of known elements to insert.

Makes sense.

So I've given a try at all that, and finished with the attached. This
patch finishes with a list of bullet points, so this had better be
split into different commits, I guess.
Thoughts?

Missed AddRoleMems?
Could you continue with CatalogTupleInsertWithInfo, what do you think?

regards,
Ranier Vilela

#11Michael Paquier
michael@paquier.xyz
In reply to: Ranier Vilela (#10)
Re: Avoid overhead open-close indexes (catalog updates)

On Tue, Nov 15, 2022 at 11:42:34AM -0300, Ranier Vilela wrote:

I find it very difficult not to have some tuple to be updated,
once inside CopyStatistics and the branch cost can get in the way,
but I don't object with your solution.

The code assumes that it is a possibility.

Missed AddRoleMems?
Could you continue with CatalogTupleInsertWithInfo, what do you think?

This one has been left out on purpose. I was tempting to use
WithInfo() with a CatalogIndexState opened optionally but I got the
impression that it makes the code a bit harder to follow and
AddRoleMems() is already complex on its own. Most DDL patterns
working on role would involve one role. More roles could be added of
course in one shot, but the extra logic complexity did not look that
appealing to me especially as some role updates are skipped.
--
Michael

#12Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#11)
Re: Avoid overhead open-close indexes (catalog updates)

On Wed, Nov 16, 2022 at 06:58:01AM +0900, Michael Paquier wrote:

This one has been left out on purpose. I was tempting to use
WithInfo() with a CatalogIndexState opened optionally but I got the
impression that it makes the code a bit harder to follow and
AddRoleMems() is already complex on its own. Most DDL patterns
working on role would involve one role. More roles could be added of
course in one shot, but the extra logic complexity did not look that
appealing to me especially as some role updates are skipped.

I have worked more on that today, and applied all that after splitting
the whole in three commits in total as different areas were touched.
It looks like we are good for this thread, then.

I have spotted more optimizations possible, particularly for operator
classes, but that could happen later.
--
Michael

#13Ranier Vilela
ranier.vf@gmail.com
In reply to: Michael Paquier (#12)
Re: Avoid overhead open-close indexes (catalog updates)

Em qua., 16 de nov. de 2022 às 04:23, Michael Paquier <michael@paquier.xyz>
escreveu:

On Wed, Nov 16, 2022 at 06:58:01AM +0900, Michael Paquier wrote:

This one has been left out on purpose. I was tempting to use
WithInfo() with a CatalogIndexState opened optionally but I got the
impression that it makes the code a bit harder to follow and
AddRoleMems() is already complex on its own. Most DDL patterns
working on role would involve one role. More roles could be added of
course in one shot, but the extra logic complexity did not look that
appealing to me especially as some role updates are skipped.

I have worked more on that today, and applied all that after splitting
the whole in three commits in total as different areas were touched.
It looks like we are good for this thread, then.

Thanks Michael.

I have spotted more optimizations possible, particularly for operator
classes, but that could happen later.

Good to know.

regards,
Ranier Vilela