Reducing relcache memory usage: deduping index shapes
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t253624psql -h localhost -U postgresBuilt from patchset v16 (message #16), September 09, 2026 at 10:12 AM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t253624_16 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t253624_16 && git checkout t253624_16Patchset v16 (message #16) is on t253624_16
Hi,
This is a part of the work I discussed in my PGConf.DEV talk of this year [0]https://www.youtube.com/watch?v=Q4w8LFWOwPY.
Many of Index's allocated fields in RelationData have the same (or
practically the same) contents for many indexes. E.g. a btree index on
a bigint column will always have the same contents in rd_opfamily,
rd_opcintype, and rd_support, and (given the same opclass options in
each column) will have equivalent rd_supportinfo.
The attached patchset adds a deduplication layer into the relcache,
which makes sure we only allocate one set of (rd_opfamily,
rd_opcintype, rd_support, rd_supportinfo) for indexes with equivalent
key definitions (so, a matching number of key attributes, opclasses,
and AM).
Additionally, it includes a patch by Andres (polished by me) that adds
a proxy context, which reduces the the overhead of small and
long-lived allocations in (what we expect to be) small memory contexts
by forwarding the allocations to malloc (after wrapping the struct).
Earlier versions of the patch adjusted aset.c to accept smaller memory
context sizes, but I abandoned that approach in favour of Andres'
ProxyContext -- it can outsource most the complexities of memory
management to the system allocator.
Patches in this patchset:
0001/0002: prepare relcache for deduplication.
0003: implements the deduplication
0004: Andres' ProxyContext patch
0005: Use proxy context in relcache for 'index data'
Kind regards,
Matthias van de Meent
Databricks (https://www.databricks.com)
Attachments:
t253624_1v1-0003-Deduplicate-some-index-attributes-in-the-relcache.patchapplication/octet-stream; name=v1-0003-Deduplicate-some-index-attributes-in-the-relcache.patchDownload+804-206
v1-0004-MemCTX-Add-minimal-proxy-context-type-that-just-d.patchapplication/octet-stream; name=v1-0004-MemCTX-Add-minimal-proxy-context-type-that-just-d.patchDownload+456-5
v1-0005-Relcache-Use-Proxy-context-for-index-info-context.patchapplication/octet-stream; name=v1-0005-Relcache-Use-Proxy-context-for-index-info-context.patchDownload+2-4
v1-0002-Move-allocations-of-relation-opclass-fields-to-In.patchapplication/octet-stream; name=v1-0002-Move-allocations-of-relation-opclass-fields-to-In.patchDownload+45-43
v1-0001-Mark-constant-opclass-related-fields-const-in-Rel.patchapplication/octet-stream; name=v1-0001-Mark-constant-opclass-related-fields-const-in-Rel.patchDownload+16-12
On Tue, 1 Sept 2026 at 08:57, Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:
Additionally, it includes a patch by Andres (polished by me) that adds
a proxy context, which reduces the the overhead of small and
long-lived allocations in (what we expect to be) small memory contexts
by forwarding the allocations to malloc (after wrapping the struct).
Interesting. I looked at 0004 and expected that with a name like
"Proxy" that the allocation would be diverted to another context, such
as the parent context. If all pallocs are going directly to malloc,
would "Direct" not be a more suitable name?
Also, just so it's written down somewhere, can you elaborate on the
choice not to have the code similar to as it is, but instead of malloc
directly with MemoryContextAlloc in the parent context? Is it just a
case of problems with double counting for the memory stats? Is there
some other reason why this would be bad?
From a quick read of 0004:
1. The new context type should get a mention in
src/backend/utils/mmgr/README under "Alternative Memory Context
Implementations"
2. I think it's worth expanding the following to maybe tag something
like "i.e. are directly malloc'ed" or "i.e one malloc per palloc" to
the end. The whole thing about other context types managing oversized
chunks and directly mallocing a block for them is really up to them.
+ * Proxy is a MemoryContext implementation designed for memory usages which
+ * require their own memory context, but which generally have few allocations
+ * that generally have a very long lifetime. Compared to ASet, every
+ * allocation of a Proxy memory context gets an External chunk.
3. I don't quite understand the following comment. IMO, there is no
initial block here. This is just the malloc for the context struct
itself.
+ /*
+ * Allocate the initial block. Unlike other proxy.c blocks, it starts
+ * with the context header and its block header follows that.
+ */
4. Per the discussion in [1]/messages/by-id/flat/CA+RLCQzSkLrwscci4+u3eqymzbozXPdFt_TsU_dXPHvU4Px0dg@mail.gmail.com, I think the preference is to use size_t
instead of Size.
+ Size totalspace;
+ Size nchunks = 0;
Shouldn't nchunks be uint64 anyway? Nothing guarantees Size is bigger
than int, even on 64-bit.
5. The following WARNING looks buggy:
+ if (total_allocated != ctx->header.mem_allocated)
+ {
+ elog(WARNING, "problem in Proxy %s: amount of memory allocated %d
does not match header %d",
+ name, (int) total_allocated, ctx->chunks_allocated);
+ }
Why cast to int?
Why ctx->chunks_allocated and not ctx->header.mem_allocated?
Earlier versions of the patch adjusted aset.c to accept smaller memory
context sizes, but I abandoned that approach in favour of Andres'
ProxyContext -- it can outsource most the complexities of memory
management to the system allocator.
Can you share more about this choice? What are the advantages of this
new context type over doing something like modifying aset.c to allow
passing of a 0 maxBlockSize so that all chunks are external? If it
were done that way, the while loop at the end of
AllocSetContextCreateInternal() could calculate allocChunkLimit to be
0 and that would result in AllocSetAlloc() always going with the
AllocSetAllocLarge() path. I currently can't see beyond this only
saving the "if (size > set->allocChunkLimit)" precheck. Or is it a
case of AllocSetContext being overly large due to the freelist array?
Can you provide information about how much memory is being saved from 0004+0005?
David
[1]: /messages/by-id/flat/CA+RLCQzSkLrwscci4+u3eqymzbozXPdFt_TsU_dXPHvU4Px0dg@mail.gmail.com
On Tue, 1 Sept 2026 at 00:04, David Rowley <dgrowleyml@gmail.com> wrote:
On Tue, 1 Sept 2026 at 08:57, Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:Additionally, it includes a patch by Andres (polished by me) that adds
a proxy context, which reduces the the overhead of small and
long-lived allocations in (what we expect to be) small memory contexts
by forwarding the allocations to malloc (after wrapping the struct).Interesting. I looked at 0004 and expected that with a name like
"Proxy" that the allocation would be diverted to another context, such
as the parent context.
If all pallocs are going directly to malloc,
would "Direct" not be a more suitable name?
That might be. Andres' patch named it Proxy, and I didn't have a
sufficiently better name that would fit as alternative.
Also, just so it's written down somewhere, can you elaborate on the
choice not to have the code similar to as it is, but instead of malloc
directly with MemoryContextAlloc in the parent context? Is it just a
case of problems with double counting for the memory stats? Is there
some other reason why this would be bad?
Allocating into another context has some big issues, and some smaller
but also important issues:
1. Memory lifetime management becomes more complicated, given that you
have to forward memory context resets to the proxied contexts.
Allocating into the parent is insufficient, as parent contexts are not
constant for the lifetime of all contexts.
2. None of PG's own memory contexts (or, memory allocators) are
optimized for packing small, long-lived, mixed-lifetime allocations.
3. Allocating into other contexts would likely cause us to double-count memory.
From a quick read of 0004:
1. The new context type should get a mention in
src/backend/utils/mmgr/README under "Alternative Memory Context
Implementations"
Will adjust.
2. I think it's worth expanding the following to maybe tag something
like "i.e. are directly malloc'ed" or "i.e one malloc per palloc" to
the end. The whole thing about other context types managing oversized
chunks and directly mallocing a block for them is really up to them.+ * Proxy is a MemoryContext implementation designed for memory usages which + * require their own memory context, but which generally have few allocations + * that generally have a very long lifetime. Compared to ASet, every + * allocation of a Proxy memory context gets an External chunk.
Will do.
3. I don't quite understand the following comment. IMO, there is no
initial block here. This is just the malloc for the context struct
itself.+ /* + * Allocate the initial block. Unlike other proxy.c blocks, it starts + * with the context header and its block header follows that. + */
Yeah, that's a part I failed to polish. I'll adjust it in my next patch.
4. Per the discussion in [1], I think the preference is to use size_t
instead of Size.
Yes, much of the patch is from a WIP patch of Andres'. I'd hoped I
polished all rough edges, but I didn't get as far as I'd hoped before
the deadline of the September commitfest start.
+ Size totalspace;
+ Size nchunks = 0;Shouldn't nchunks be uint64 anyway? Nothing guarantees Size is bigger
than int, even on 64-bit.
True. But AFAIK, Size must be able to contain the largest possible
pointer difference, and that implies that we can't have more than
SIZE_MAX allocated chunks. Using Size for maths in those cases seems
appropriate, to avoid requiring expensive oversized registers on
32-bit builds.
5. The following WARNING looks buggy:
+ if (total_allocated != ctx->header.mem_allocated) + { + elog(WARNING, "problem in Proxy %s: amount of memory allocated %d does not match header %d", + name, (int) total_allocated, ctx->chunks_allocated); + }Why cast to int?
I've had some trouble finding the right format specifier, so cast to
int was a simple hack on that.
Earlier versions of the patch adjusted aset.c to accept smaller memory
context sizes, but I abandoned that approach in favour of Andres'
ProxyContext -- it can outsource most the complexities of memory
management to the system allocator.Can you share more about this choice? What are the advantages of this
new context type over doing something like modifying aset.c to allow
passing of a 0 maxBlockSize so that all chunks are external?
The AllocSet context requires a relatively large allocation, with
larger overheads than the Proxy context (200B vs 108B). For the
contexts I'm interested in (the relcache 'index info' contexts) this
is a very significant difference: An aset context would increase
memory usage of the contexts by ~35% (assuming initdb's catalogs are
is a good sample).
In this patch, the average 'index info' context uses about 329 bytes
of memory (vs 2086 bytes on master). If an aset context was used,
with its larger context and block structs, that average would be 447
bytes.
If it
were done that way, the while loop at the end of
AllocSetContextCreateInternal() could calculate allocChunkLimit to be
0 and that would result in AllocSetAlloc() always going with the
AllocSetAllocLarge() path. I currently can't see beyond this only
saving the "if (size > set->allocChunkLimit)" precheck. Or is it a
case of AllocSetContext being overly large due to the freelist array?
Exactly, AllocSet adds significantly more overhead than Proxy does;
the context itself is 85% larger, and its Block is 25% larger than its
ProxyContext equivalent ProxyChunk.
Can you provide information about how much memory is being saved from 0004+0005?
Attached the output for a query on catcache/relcache memory context
data, with output of master and the full patchset.
From Master to 0003, we go from 287872 bytes total to 141312 bytes
total spent on "index info" contexts (some of which moved into other
contexts, some new, but a net saving of 130kB). 0004+0005 bring that
down all the way to 45392 bytes; most of which is just avoiding the
large overhead of the pre-allocated Blocks of memory, saving another
95kB.
Kind regards,
Matthias van de Meent
Databricks (https://www.databricks.com)
Attachments:
scratch_61.txttext/plain; charset=US-ASCII; name=scratch_61.txtDownload
On Sep 1, 2026, at 07:10, Matthias van de Meent <boekewurm+postgres@gmail.com> wrote:
On Tue, 1 Sept 2026 at 00:04, David Rowley <dgrowleyml@gmail.com> wrote:
On Tue, 1 Sept 2026 at 08:57, Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:Additionally, it includes a patch by Andres (polished by me) that adds
a proxy context, which reduces the the overhead of small and
long-lived allocations in (what we expect to be) small memory contexts
by forwarding the allocations to malloc (after wrapping the struct).Interesting. I looked at 0004 and expected that with a name like
"Proxy" that the allocation would be diverted to another context, such
as the parent context.If all pallocs are going directly to malloc,
would "Direct" not be a more suitable name?That might be. Andres' patch named it Proxy, and I didn't have a
sufficiently better name that would fit as alternative.Also, just so it's written down somewhere, can you elaborate on the
choice not to have the code similar to as it is, but instead of malloc
directly with MemoryContextAlloc in the parent context? Is it just a
case of problems with double counting for the memory stats? Is there
some other reason why this would be bad?Allocating into another context has some big issues, and some smaller
but also important issues:
1. Memory lifetime management becomes more complicated, given that you
have to forward memory context resets to the proxied contexts.
Allocating into the parent is insufficient, as parent contexts are not
constant for the lifetime of all contexts.
2. None of PG's own memory contexts (or, memory allocators) are
optimized for packing small, long-lived, mixed-lifetime allocations.
3. Allocating into other contexts would likely cause us to double-count memory.From a quick read of 0004:
1. The new context type should get a mention in
src/backend/utils/mmgr/README under "Alternative Memory Context
Implementations"Will adjust.
2. I think it's worth expanding the following to maybe tag something
like "i.e. are directly malloc'ed" or "i.e one malloc per palloc" to
the end. The whole thing about other context types managing oversized
chunks and directly mallocing a block for them is really up to them.+ * Proxy is a MemoryContext implementation designed for memory usages which + * require their own memory context, but which generally have few allocations + * that generally have a very long lifetime. Compared to ASet, every + * allocation of a Proxy memory context gets an External chunk.Will do.
3. I don't quite understand the following comment. IMO, there is no
initial block here. This is just the malloc for the context struct
itself.+ /* + * Allocate the initial block. Unlike other proxy.c blocks, it starts + * with the context header and its block header follows that. + */Yeah, that's a part I failed to polish. I'll adjust it in my next patch.
4. Per the discussion in [1], I think the preference is to use size_t
instead of Size.Yes, much of the patch is from a WIP patch of Andres'. I'd hoped I
polished all rough edges, but I didn't get as far as I'd hoped before
the deadline of the September commitfest start.+ Size totalspace;
+ Size nchunks = 0;Shouldn't nchunks be uint64 anyway? Nothing guarantees Size is bigger
than int, even on 64-bit.True. But AFAIK, Size must be able to contain the largest possible
pointer difference, and that implies that we can't have more than
SIZE_MAX allocated chunks. Using Size for maths in those cases seems
appropriate, to avoid requiring expensive oversized registers on
32-bit builds.5. The following WARNING looks buggy:
+ if (total_allocated != ctx->header.mem_allocated) + { + elog(WARNING, "problem in Proxy %s: amount of memory allocated %d does not match header %d", + name, (int) total_allocated, ctx->chunks_allocated); + }Why cast to int?
I've had some trouble finding the right format specifier, so cast to
int was a simple hack on that.Earlier versions of the patch adjusted aset.c to accept smaller memory
context sizes, but I abandoned that approach in favour of Andres'
ProxyContext -- it can outsource most the complexities of memory
management to the system allocator.Can you share more about this choice? What are the advantages of this
new context type over doing something like modifying aset.c to allow
passing of a 0 maxBlockSize so that all chunks are external?The AllocSet context requires a relatively large allocation, with
larger overheads than the Proxy context (200B vs 108B). For the
contexts I'm interested in (the relcache 'index info' contexts) this
is a very significant difference: An aset context would increase
memory usage of the contexts by ~35% (assuming initdb's catalogs are
is a good sample).In this patch, the average 'index info' context uses about 329 bytes
of memory (vs 2086 bytes on master). If an aset context was used,
with its larger context and block structs, that average would be 447
bytes.If it
were done that way, the while loop at the end of
AllocSetContextCreateInternal() could calculate allocChunkLimit to be
0 and that would result in AllocSetAlloc() always going with the
AllocSetAllocLarge() path. I currently can't see beyond this only
saving the "if (size > set->allocChunkLimit)" precheck. Or is it a
case of AllocSetContext being overly large due to the freelist array?Exactly, AllocSet adds significantly more overhead than Proxy does;
the context itself is 85% larger, and its Block is 25% larger than its
ProxyContext equivalent ProxyChunk.Can you provide information about how much memory is being saved from 0004+0005?
Attached the output for a query on catcache/relcache memory context
data, with output of master and the full patchset.From Master to 0003, we go from 287872 bytes total to 141312 bytes
total spent on "index info" contexts (some of which moved into other
contexts, some new, but a net saving of 130kB). 0004+0005 bring that
down all the way to 45392 bytes; most of which is just avoiding the
large overhead of the pre-allocated Blocks of memory, saving another
95kB.Kind regards,
Matthias van de Meent
Databricks (https://www.databricks.com)
<scratch_61.txt>
Hi Matthias,
Thanks for the patch, the idea is interesting. I have just gone through the commits, and got a suspicion.
Basically, the idea is to share common data via a hash table in each backend process, thereby saving some memory. However, sharing rd_supportinfo seems unsafe. In index_getprocinfo(), an entry is initialized lazily by calling fmgr_info_cxt(procId, locinfo, irel->rd_indexcxt);
This stores the current index's private context in locinfo->fn_mcxt. Support functions may then allocate fn_extra in that context. If this index's relcache entry is destroyed while another index still references the shared rd_supportinfo, the first index's rd_indexcxt is deleted, leaving the shared FmgrInfo with a dangling fn_mcxt and possibly a dangling fn_extra.
Am I missing something that guarantees the original rd_indexcxt remains valid for as long as the shared rd_supportinfo is referenced?
Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/
On Tue, 1 Sept 2026 at 11:10, Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:
On Tue, 1 Sept 2026 at 00:04, David Rowley <dgrowleyml@gmail.com> wrote:
Shouldn't nchunks be uint64 anyway? Nothing guarantees Size is bigger
than int, even on 64-bit.True. But AFAIK, Size must be able to contain the largest possible
pointer difference, and that implies that we can't have more than
SIZE_MAX allocated chunks. Using Size for maths in those cases seems
appropriate, to avoid requiring expensive oversized registers on
32-bit builds.
Pointer difference relates to the size of a chunk. We're talking about
the number of chunks. If a 64-bit platform exists with a 32-bit
size_t, and you create > 2^32 chunks, then the counter will wrap and
you'll mistakenly raise a WARNING.
I also think it's better to just remove the chunks_allocated struct
field. It's not testing any code that exists in
non-MEMORY_CONTEXT_CHECKING. All that's happening is you're adding a
bunch of new code in MEMORY_CONTEXT_CHECKING builds and verifying that
new code is ok. If you didn't add it, you wouldn't need to check it.
More reviewing of 0004:
1. These Asserts also seem strange as they check something that's
already been checked:
+ Assert(total_allocated == context->mem_allocated);
+ Assert(chunks_allocated == ctx->chunks_allocated);
2. I think ExternalChunkGetBlock should be called something else as it
returns a pointer to a ProxyChunk. Maybe MemoryChunkGetProxyChunk? You
should also document what the parameter is to that macro.
3. Is "#include <limits.h>" just for INT_MAX? Normally we'd use
PG_INT32_MAX from c.h.
4. If you are keen to save more memory, you could move away from using
MemoryChunk and write your own version that maintains the lower 4-bits
for the MemoryContextMethodID and encodes the size in the remaining 60
bits. That might be more trouble than it's worth, however.
5. It would be good to see proxy.c using set_sentinel() and sentinel_ok(),
David
Hi Matthias,
This is a part of the work I discussed in my PGConf.DEV talk of this year [0].
Many of Index's allocated fields in RelationData have the same (or
practically the same) contents for many indexes. E.g. a btree index on
a bigint column will always have the same contents in rd_opfamily,
rd_opcintype, and rd_support, and (given the same opclass options in
each column) will have equivalent rd_supportinfo.
I'm glad to see work on reducing relcache's memory footprint. I've seen
production workloads with 10-100k tables and many hundred thousand indexes.
The attached patchset adds a deduplication layer into the relcache,
which makes sure we only allocate one set of (rd_opfamily,
rd_opcintype, rd_support, rd_supportinfo) for indexes with equivalent
key definitions (so, a matching number of key attributes, opclasses,
and AM).
Why did you specifically worked on deduplicating the index fields in
RelationData? Is that consuming most out of all of RelationData?
Additionally, it includes a patch by Andres (polished by me) that adds
a proxy context, which reduces the the overhead of small and
long-lived allocations in (what we expect to be) small memory contexts
by forwarding the allocations to malloc (after wrapping the struct).Earlier versions of the patch adjusted aset.c to accept smaller memory
context sizes, but I abandoned that approach in favour of Andres'
ProxyContext -- it can outsource most the complexities of memory
management to the system allocator.Patches in this patchset:
0001/0002: prepare relcache for deduplication.
0003: implements the deduplication
0004: Andres' ProxyContext patch
0005: Use proxy context in relcache for 'index data'
On the testing end: if this effort makes more progress, I could run this
against the aforementioned database to get some real-world numbers.
One more micro optimization that we could do is better packing
RelationData. On my AMD64 system sizeof(RelationData) == 488 bytes. By
reordering the members we could get it down to 440 bytes which is about
10% savings.
--
David Geier
Hi!
One more micro optimization that we could do is better packing
RelationData. On my AMD64 system sizeof(RelationData) == 488 bytes. By
reordering the members we could get it down to 440 bytes which is about
10% savings.
Here are a few more things we could do to shrink the size of RelationData:
1. Pack ten bool members into a bitmask and add some convience getter
inline functions. Saves ~8 bytes.
2. rd_index can be derived from rd_indextuple. Saves 8 bytes.
3. I didn't dig too deep but it seems to me that the members from
rd_lockInfo can be also derived: rel_id == rd_id and dbId ==
MyDatabaseId. Saves 8 bytes.
3. Split members by relation type: two disjoint groups of fields are
always NULL depending on whether the entry is a table or an index:
Index only (~128 bytes):
rd_index, rd_indextuple, rd_indexcxt, rd_indam, rd_opfamily,
rd_opcintype, rd_support, rd_supportinfo, rd_indoption, rd_indexprs,
rd_indpred, rd_exclops, rd_exclprocs, rd_exclstrats, rd_indcollation,
rd_opcoptions
Table only (~170 bytes):
rd_rules, rd_rulescxt, trigdesc, rd_rsdesc, rd_fkeylist, rd_fkeyvalid,
rd_partkey*, rd_partdesc*, rd_partcheck*, rd_keyattr, rd_pkattr,
rd_idattr, rd_hotblockingattr, rd_summarizedattr, rd_pubdesc, rd_fdwroutine
We could put these members into their own struct and replace each group
with a single lazily-allocated pointer to the corresponding struct,
depending on the relation type.
By packing the struct and additionally doing these changes we would
roughly half the size of RelationData. Unfortunately, all of these
changes would require patching a lot of usage sites.
--
David Geier
On Tue, 1 Sept 2026 at 10:06, David Geier <geidav.pg@gmail.com> wrote:
The attached patchset adds a deduplication layer into the relcache,
which makes sure we only allocate one set of (rd_opfamily,
rd_opcintype, rd_support, rd_supportinfo) for indexes with equivalent
key definitions (so, a matching number of key attributes, opclasses,
and AM).Why did you specifically worked on deduplicating the index fields in
RelationData? Is that consuming most out of all of RelationData?
Because indexes allocate an array of nkeyatts * indam->amsupport
FmgrInfos, which for some index shapes can result in huge allocations
(BRIN: 15, GiST: 12, btree: 6). There is certainly space for further
optimization in other places, but this required changes in fewer code
areas than if I'd started changing the data types and shape of
Relation itself, given the spread of Relation across the codebase.
Additionally, it includes a patch by Andres (polished by me) that adds
a proxy context, which reduces the the overhead of small and
long-lived allocations in (what we expect to be) small memory contexts
by forwarding the allocations to malloc (after wrapping the struct).Earlier versions of the patch adjusted aset.c to accept smaller memory
context sizes, but I abandoned that approach in favour of Andres'
ProxyContext -- it can outsource most the complexities of memory
management to the system allocator.Patches in this patchset:
0001/0002: prepare relcache for deduplication.
0003: implements the deduplication
0004: Andres' ProxyContext patch
0005: Use proxy context in relcache for 'index data'On the testing end: if this effort makes more progress, I could run this
against the aforementioned database to get some real-world numbers.
Partitioned tables are fairly common, and frequently have the
equivalent index definitions that this patch optimizes. I'd love to
see real-world data on this optimization, and given the lack of churn
in this part of the code I'm fairly confident this can be applied on
older versions for educational purposes.
One more micro optimization that we could do is better packing
RelationData. On my AMD64 system sizeof(RelationData) == 488 bytes. By
reordering the members we could get it down to 440 bytes which is about
10% savings.
Yeah, packing RelationData would save some in struct size, but given
that these are mostly still allocated as separate allocations in an
aset context, shaving bytes off of RelationData won't help much until
we get below 256 bytes. I would love to get the struct size that far
down, but I don't expect that to be achievable with the current
contents of the struct.
I've considered allocating Relations in a slab context to avoid aset's
alignment overhead, but never got far enough with a prototype to get
it to pass all tests.
Kind regards,
Matthias van de Meent
Databricks (https://www.databricks.com)
On Tue, 1 Sept 2026 at 08:02, Chao Li <li.evan.chao@gmail.com> wrote:
Hi Matthias,
Thanks for the patch, the idea is interesting. I have just gone through the commits, and got a suspicion.
Basically, the idea is to share common data via a hash table in each backend process, thereby saving some memory. However, sharing rd_supportinfo seems unsafe. In index_getprocinfo(), an entry is initialized lazily by calling fmgr_info_cxt(procId, locinfo, irel->rd_indexcxt);
This stores the current index's private context in locinfo->fn_mcxt. Support functions may then allocate fn_extra in that context. If this index's relcache entry is destroyed while another index still references the shared rd_supportinfo, the first index's rd_indexcxt is deleted, leaving the shared FmgrInfo with a dangling fn_mcxt and possibly a dangling fn_extra.
Am I missing something that guarantees the original rd_indexcxt remains valid for as long as the shared rd_supportinfo is referenced?
That's a good point. It looks like I'll have to make sure to make
that work, because right now that indeed has context lifetime issues.
Thanks for the report, I'll fix it in the next patch version.
Kind regards,
Matthias van de Meent
Databricks (https://www.databricks.com)
Hi Matthias!
Because indexes allocate an array of nkeyatts * indam->amsupport
FmgrInfos, which for some index shapes can result in huge allocations
(BRIN: 15, GiST: 12, btree: 6). There is certainly space for further
optimization in other places, but this required changes in fewer code
areas than if I'd started changing the data types and shape of
Relation itself, given the spread of Relation across the codebase.
I see.
On the testing end: if this effort makes more progress, I could run this
against the aforementioned database to get some real-world numbers.Partitioned tables are fairly common, and frequently have the
equivalent index definitions that this patch optimizes. I'd love to
see real-world data on this optimization, and given the lack of churn
in this part of the code I'm fairly confident this can be applied on
older versions for educational purposes.
The use case with partitioned tables makes a lot of sense.
The workload I have uses very few partitioned tables but has a huge
number of partially very wide unpartitioned tables with large numbers of
indexes. I'll collect baseline numbers without the patch and figure out
how we can get numbers with the patch.
One more micro optimization that we could do is better packing
RelationData. On my AMD64 system sizeof(RelationData) == 488 bytes. By
reordering the members we could get it down to 440 bytes which is about
10% savings.Yeah, packing RelationData would save some in struct size, but given
that these are mostly still allocated as separate allocations in an
aset context, shaving bytes off of RelationData won't help much until
we get below 256 bytes. I would love to get the struct size that far
down, but I don't expect that to be achievable with the current
contents of the struct.I've considered allocating Relations in a slab context to avoid aset's
alignment overhead, but never got far enough with a prototype to get
it to pass all tests.
With the other optimizations from [1]/messages/by-id/2bd41932-24fa-477a-a213-02fda4555835@gmail.com we should be able to get below 256
bytes. If you want I can give this a try.
--
David Geier
[1]: /messages/by-id/2bd41932-24fa-477a-a213-02fda4555835@gmail.com
/messages/by-id/2bd41932-24fa-477a-a213-02fda4555835@gmail.com
Can you provide information about how much memory is being saved from 0004+0005?
Attached the output for a query on catcache/relcache memory context
data, with output of master and the full patchset.From Master to 0003, we go from 287872 bytes total to 141312 bytes
total spent on "index info" contexts (some of which moved into other
contexts, some new, but a net saving of 130kB). 0004+0005 bring that
down all the way to 45392 bytes; most of which is just avoiding the
large overhead of the pre-allocated Blocks of memory, saving another
95kB.
Did you just startup the server and run that query or did you run some
something first to populate the relcache?
Maybe I'm missing something but in my understanding the relcache is
populated as relations are accessed.
--
David Geier
On Wed, 2 Sept 2026 at 10:09, David Geier <geidav.pg@gmail.com> wrote:
Can you provide information about how much memory is being saved from 0004+0005?
Attached the output for a query on catcache/relcache memory context
data, with output of master and the full patchset.From Master to 0003, we go from 287872 bytes total to 141312 bytes
total spent on "index info" contexts (some of which moved into other
contexts, some new, but a net saving of 130kB). 0004+0005 bring that
down all the way to 45392 bytes; most of which is just avoiding the
large overhead of the pre-allocated Blocks of memory, saving another
95kB.Did you just startup the server and run that query or did you run some
something first to populate the relcache?
I ran both "\d+ pg_catalog.*" and "\di+ pg_catalog.*" in the session
before running the query, so it should be a representative sample of
all indexes in a freshly initdb'ed system.
Maybe I'm missing something but in my understanding the relcache is
populated as relations are accessed.
Correct.
Kind regards,
Matthias van de Meent
Databricks (https://www.databricks.com)
On Wed, 2 Sept 2026 at 10:06, David Geier <geidav.pg@gmail.com> wrote:
Because indexes allocate an array of nkeyatts * indam->amsupport
FmgrInfos, which for some index shapes can result in huge allocations
(BRIN: 15, GiST: 12, btree: 6). There is certainly space for further
optimization in other places, but this required changes in fewer code
areas than if I'd started changing the data types and shape of
Relation itself, given the spread of Relation across the codebase.I see.
Yep. That's anywhere from 288 bytes to 720 bytes per key attribute.
It's easy to see how that can impact the total session memory usage by
a significant amount, especially when combined with small aset
contexts' allocated-size rounding and block allocations.
One more micro optimization that we could do is better packing
RelationData. On my AMD64 system sizeof(RelationData) == 488 bytes. By
reordering the members we could get it down to 440 bytes which is about
10% savings.Yeah, packing RelationData would save some in struct size, but given
that these are mostly still allocated as separate allocations in an
aset context, shaving bytes off of RelationData won't help much until
we get below 256 bytes. I would love to get the struct size that far
down, but I don't expect that to be achievable with the current
contents of the struct.I've considered allocating Relations in a slab context to avoid aset's
alignment overhead, but never got far enough with a prototype to get
it to pass all tests.With the other optimizations from [1] we should be able to get below 256
bytes. If you want I can give this a try.
Back-of-the-envelope calculation: Currently, RelationData is 488
bytes, of which 50 wasted on alignment (so, 48 bytes recoverable). If
we somehow get the index and table fields to not be used at the same
time, we'd gain another 128 bytes (because the larger of the two must
remain). This leaves us with 312 bytes, which is still 56 bytes over
the target, and I don't think we can trivially find 7 pointers' worth
of fields to remove.
But, feel free to try.
- Matthias
I've considered allocating Relations in a slab context to avoid aset's
alignment overhead, but never got far enough with a prototype to get
it to pass all tests.With the other optimizations from [1] we should be able to get below 256
bytes. If you want I can give this a try.Back-of-the-envelope calculation: Currently, RelationData is 488
bytes, of which 50 wasted on alignment (so, 48 bytes recoverable). If
we somehow get the index and table fields to not be used at the same
time, we'd gain another 128 bytes (because the larger of the two must
remain). This leaves us with 312 bytes, which is still 56 bytes over
the target, and I don't think we can trivially find 7 pointers' worth
of fields to remove.But, feel free to try.
I've managed to crunch sizeof(RelationData) down to 256 bytes and
there's some more room for improvement, see below. Attached is the patch
set. It passes tests.
I had to move RelationData into a new header because the script that
extracts node metadata doesn't like the anonymous unions.
With the patch, GetMemoryChunkSpace(rel) == 264 because the allocation
falls into the 256 bytes size class and additionally has an 8 byte
header. Previously it was 520 bytes.
We could additionally:
1) Change all the arrays of length nkeys to a single array of structs.
That would save a bunch of pointers in the index union elg. It's not
clear though if that might regress performance somewhere because of
cache locality but would be worth a try.
2) Put rules and trigger related members into separately allocated
structured referenced by a single pointer. These members are rarely used
and shouldn't be performance critical.
3) Pack booleans into bitfield.
That might save another 24 or more bytes (depends on the biggest union
leg and padding at the end of the struct). But for the PoC I didn't do that.
--
David Geier
Attachments:
t253624_14v1-0008-Add-static-assert-for-size.patchtext/x-patch; charset=UTF-8; name=v1-0008-Add-static-assert-for-size.patchDownload+3-1
v1-0007-Remove-rd_index.patchtext/x-patch; charset=UTF-8; name=v1-0007-Remove-rd_index.patchDownload+152-140
v1-0006-Remove-rd_fkeyvalid.patchtext/x-patch; charset=UTF-8; name=v1-0006-Remove-rd_fkeyvalid.patchDownload+25-13
v1-0005-Move-out-partition-members.patchtext/x-patch; charset=UTF-8; name=v1-0005-Move-out-partition-members.patchDownload+156-122
v1-0004-Remove-rd_lockinfo.patchtext/x-patch; charset=UTF-8; name=v1-0004-Remove-rd_lockinfo.patchDownload+92-102
v1-0003-Packing-RelationData.patchtext/x-patch; charset=UTF-8; name=v1-0003-Packing-RelationData.patchDownload+49-40
v1-0002-Use-union.patchtext/x-patch; charset=UTF-8; name=v1-0002-Use-union.patchDownload+268-153
v1-0001-Move-RelationData-to-new-include.patchtext/x-patch; charset=UTF-8; name=v1-0001-Move-RelationData-to-new-include.patchDownload+245-228
On Thu, 3 Sept 2026 at 18:02, David Geier <geidav.pg@gmail.com> wrote:
I've considered allocating Relations in a slab context to avoid aset's
alignment overhead, but never got far enough with a prototype to get
it to pass all tests.With the other optimizations from [1] we should be able to get below 256
bytes. If you want I can give this a try.Back-of-the-envelope calculation: Currently, RelationData is 488
bytes, of which 50 wasted on alignment (so, 48 bytes recoverable). If
we somehow get the index and table fields to not be used at the same
time, we'd gain another 128 bytes (because the larger of the two must
remain). This leaves us with 312 bytes, which is still 56 bytes over
the target, and I don't think we can trivially find 7 pointers' worth
of fields to remove.But, feel free to try.
I've managed to crunch sizeof(RelationData) down to 256 bytes and
there's some more room for improvement, see below. Attached is the patch
set. It passes tests.
That's cool. I'm not fully on board with all the techniques you
applied, but it does seem like some of these can be useful. If you're
up to getting this from POC to a workable state, please start a
separate thread with its own CF entry, then we can discuss over there.
I think allocating the relcache Relations in a slab context would help
reduce memory usage whenever we shave bytes off, vs aset's hard
power-of-two barriers.
1) Change all the arrays of length nkeys to a single array of structs.
That would save a bunch of pointers in the index union elg. It's not
clear though if that might regress performance somewhere because of
cache locality but would be worth a try.2) Put rules and trigger related members into separately allocated
structured referenced by a single pointer. These members are rarely used
and shouldn't be performance critical.3) Pack booleans into bitfield.
My experience with bitfields is that they're not optimized very well
by compilers, so I'd like to avoid using bitpacking whenever possible.
Kind regards,
Matthias van de Meent
Databricks (https://www.databricks.com)
On Tue, 1 Sept 2026 at 09:18, David Rowley <dgrowleyml@gmail.com> wrote:
On Tue, 1 Sept 2026 at 11:10, Matthias van de Meent
<boekewurm+postgres@gmail.com> wrote:On Tue, 1 Sept 2026 at 00:04, David Rowley <dgrowleyml@gmail.com> wrote:
Shouldn't nchunks be uint64 anyway? Nothing guarantees Size is bigger
than int, even on 64-bit.True. But AFAIK, Size must be able to contain the largest possible
pointer difference, and that implies that we can't have more than
SIZE_MAX allocated chunks. Using Size for maths in those cases seems
appropriate, to avoid requiring expensive oversized registers on
32-bit builds.Pointer difference relates to the size of a chunk. We're talking about
the number of chunks. If a 64-bit platform exists with a 32-bit
size_t, and you create > 2^32 chunks, then the counter will wrap and
you'll mistakenly raise a WARNING.
If size_t is 32-bit, then that means all possible object sizes
(including array sizes) for the platform fit in those 32 bits. I'm
fairly sure that we don't support 64-bit addressing platforms that
don't also support objects larger than 2^32, though I'm happy to be
pointed to a (supported!) counterexample.
I also think it's better to just remove the chunks_allocated struct
field. It's not testing any code that exists in
non-MEMORY_CONTEXT_CHECKING. All that's happening is you're adding a
bunch of new code in MEMORY_CONTEXT_CHECKING builds and verifying that
new code is ok. If you didn't add it, you wouldn't need to check it.
I used it to verify that the linked list is maintained correctly. I'd
have used dclist instead if it was simple to switch to that
implementation in only MEMORY_CONTEXT_TRACKING mode.
More reviewing of 0004:
1. These Asserts also seem strange as they check something that's
already been checked:+ Assert(total_allocated == context->mem_allocated); + Assert(chunks_allocated == ctx->chunks_allocated);
I've adjusted this.
2. I think ExternalChunkGetBlock should be called something else as it
returns a pointer to a ProxyChunk. Maybe MemoryChunkGetProxyChunk? You
should also document what the parameter is to that macro.
I've adjusted the name of the struct to match its role -- it's more
comparable to aset's Block, so I updated its name.
3. Is "#include <limits.h>" just for INT_MAX? Normally we'd use
PG_INT32_MAX from c.h.
I've seen various uses of INT_MAX from <limits.h> directly around the
codebase, so I don't see much of an issue with it. But, in v2 this
include has become redundant I reworked the code a bit in v2, and now
this check isn't present anymore.
4. If you are keen to save more memory, you could move away from using
MemoryChunk and write your own version that maintains the lower 4-bits
for the MemoryContextMethodID and encodes the size in the remaining 60
bits. That might be more trouble than it's worth, however.
I don't think that exact scheme is allowed, because the most bits you
can consume is 59: Four bits are used by the MemoryContextMethodID,
and one bit stores the "external" bit. But when the external bit is
set, mctx infra expects these 59 bits to contain MEMORYCHUNK_MAGIC;
and when the external bit isn't set, it should contain a valid
`length` field less than .
And yes, I think that'd be more trouble than it's worth.
5. It would be good to see proxy.c using set_sentinel() and sentinel_ok(),
Added.
-------
Attached is v2 of the patchset. Changelist below:
0001/0002: Unchanged.
0003 (Deduplication):
* New memory context "Relation shape cache" to hold all shape-related
allocations and contexts,
This is a child context under CacheMemoryContext
* Memory context per "relation shape" in the RelShapeHash
This allows faster freeing of all associated data
* Optimized default shape entry data allocations per shape
The minimum is now down to 2 allocations, from >3. This is
primarily useful once Proxy contexts are used; Key data is still
bulk-allocated.
* Some varlena macro-related fixes.
Some SIZE/SIZE_EXHDR confusion and related issues, identified by
the sanitizer CF builds.
0004 (Proxy context):
* Consistency checks have been adjusted, and sentinel checks have been
introduced.
* Code has been updated with aset as template for naming and flow
This should clean up David's comments.
Name change from Proxy to anything else gets a 0-vote from me: I'd
like to avoid the churn, but if people have strong feelings about it
I'll go through the motions.
0005 (Apply proxy):
* Added Proxy to the new per-"index shape" contexts.
Question for the crowd: Most memory contexts often get a text
identifier which describes their contents in more detail when we have
many of the same name. "index shape" contexts don't have a simple
natural identifier. Whilst they do have the shape key, formatting
that into a name would be a bit of effort (and quite a bit of effort
if we want to capture the whole key), and we'd spend more bytes per
index shape. Do we want/need this identifier even with the increase
in memory usage?
Kind regards,
Matthias van de Meent
Databricks (https://www.databricks.com)
Attachments:
t253624_16v2-0003-Deduplicate-some-index-attributes-in-the-relcache.patchapplication/octet-stream; name=v2-0003-Deduplicate-some-index-attributes-in-the-relcache.patchDownload+858-217
v2-0001-rel.h-const-ify-opclass-related-fields-of-Relatio.patchapplication/octet-stream; name=v2-0001-rel.h-const-ify-opclass-related-fields-of-Relatio.patchDownload+16-12
v2-0002-relcache-refactor-RelationInitIndexAccessInfo.patchapplication/octet-stream; name=v2-0002-relcache-refactor-RelationInitIndexAccessInfo.patchDownload+45-43
v2-0005-Relcache-Use-Proxy-contexts-for-index-info-and-in.patchapplication/octet-stream; name=v2-0005-Relcache-Use-Proxy-contexts-for-index-info-and-in.patchDownload+5-10
v2-0004-MemCTX-Add-minimal-proxy-context-type-that-just-d.patchapplication/octet-stream; name=v2-0004-MemCTX-Add-minimal-proxy-context-type-that-just-d.patchDownload+559-5
Hi Matthias!
I've managed to crunch sizeof(RelationData) down to 256 bytes and
there's some more room for improvement, see below. Attached is the patch
set. It passes tests.That's cool. I'm not fully on board with all the techniques you
applied, but it does seem like some of these can be useful. If you're
up to getting this from POC to a workable state, please start a
separate thread with its own CF entry, then we can discuss over there.
Sounds good. Will do.
I think allocating the relcache Relations in a slab context would help
reduce memory usage whenever we shave bytes off, vs aset's hard
power-of-two barriers.
Yes, especially as long as RelationData is so big. The next smaller
allocation size is 128 bytes. Until then shaving off more bytes doesn't
truly save anything with ASET. But I'm also not sure how much we can
truly still get rid of without causing more indirections which are
bad for performance.
Any idea why ASET doesn't have more fine grained allocation classes?
1) Change all the arrays of length nkeys to a single array of structs.
That would save a bunch of pointers in the index union elg. It's not
clear though if that might regress performance somewhere because of
cache locality but would be worth a try.2) Put rules and trigger related members into separately allocated
structured referenced by a single pointer. These members are rarely used
and shouldn't be performance critical.3) Pack booleans into bitfield.
My experience with bitfields is that they're not optimized very well
by compilers, so I'd like to avoid using bitpacking whenever possible.
If that's the case we could alternatively do it manually and provide
some getter macros.
--
David Geier
On Tue, 8 Sept 2026 at 17:30, David Geier <geidav.pg@gmail.com> wrote:
I think allocating the relcache Relations in a slab context would help
reduce memory usage whenever we shave bytes off, vs aset's hard
power-of-two barriers.Yes, especially as long as RelationData is so big. The next smaller
allocation size is 128 bytes. Until then shaving off more bytes doesn't
truly save anything with ASET. But I'm also not sure how much we can
truly still get rid of without causing more indirections which are
bad for performance.Any idea why ASET doesn't have more fine grained allocation classes?
Performance, and memory overhead?
Right now, we spend 88 of 200 bytes of the AllocSetContext itself on
11 freelists, and finer grained classes would mean larger freelists
and larger overheads (assuming we don't want to shrink the current
8B-8kB range of freelist-supported chunk sizes).
Additionally, powers of two are cheap to calculate vs arbitrary
numbers, and this reduces the computational overhead and improves the
branch-predictability of aset.
1) Change all the arrays of length nkeys to a single array of structs.
That would save a bunch of pointers in the index union elg. It's not
clear though if that might regress performance somewhere because of
cache locality but would be worth a try.2) Put rules and trigger related members into separately allocated
structured referenced by a single pointer. These members are rarely used
and shouldn't be performance critical.3) Pack booleans into bitfield.
My experience with bitfields is that they're not optimized very well
by compilers, so I'd like to avoid using bitpacking whenever possible.If that's the case we could alternatively do it manually and provide
some getter macros.
Possible, yes, but at the cost of (possibly) large code changes to
migrate to the new macros or inline functions, from direct field
accesses.
Whilst I do think saving bytes is worth something, I don't think we
should be shaving bytes down at the cost of readability and/or
backpatchability, especially when it's "just" a few bytes per
relation. Even with 100s of 1000s of relations that'll "just" be a few
MBs.
Kind regards,
Matthias van de Meent
Databricks (https://www.databricks.com)