pg.ddx.io  pgsql-hackers@postgresql.org mailing list archive  
help / color / mirror / Atom feed
From: Tomas Vondra <tomas.vondra@enterprisedb.com>
To: David Rowley <dgrowleyml@gmail.com>
Cc: Andres Freund <andres@anarazel.de>
Cc: Tomas Vondra <tv@fuzzy.cz>
Cc: PostgreSQL Developers <pgsql-hackers@lists.postgresql.org>
Subject: Re: Use generation context to speed up tuplesorts
Date: Tue, 3 Aug 2021 16:10:23 +0200
Message-ID: <36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com> (raw)
In-Reply-To: <CAApHDvp4cFZ6Qdw1Z2wrd1Uv5s6rPKP7FWC5jHFfzR=vU2Ox+w@mail.gmail.com>
References: <CAApHDvoH4ASzsAOyHcxkuY01Qf++8JJ0paw+03dk+W25tQEcNQ@mail.gmail.com>
	<20210730203853.utjj43f6zzn5e2hy@alap3.anarazel.de>
	<bc765deb-ca2b-838e-f980-16a77dd0922b@enterprisedb.com>
	<d987fd54-01f8-0f73-af6c-519f799a0ab8@enterprisedb.com>
	<CAApHDvp4cFZ6Qdw1Z2wrd1Uv5s6rPKP7FWC5jHFfzR=vU2Ox+w@mail.gmail.com>



On 8/2/21 1:17 PM, David Rowley wrote:
> On Sat, 31 Jul 2021 at 14:34, Tomas Vondra
> <tomas.vondra@enterprisedb.com> wrote:
>> I spent a bit of time hacking on the Generation context, adding the two
>> improvements discussed in this thread:
>>
>> 1) internal handling of block sizes, similar to what AllocSet does (it
>> pretty much just copies parts of it)
>>
>> 2) keeper block (we keep one empry block instead of freeing it)
>>
>> 3) I've also added allocChunkLimit, which makes it look a bit more like
>> AllocSet (instead of using just blockSize/8, which does not work too
>> well with dynamic blockSize)
>>
>> I haven't done any extensive tests on it, but it does pass check-world
>> with asserts etc. I haven't touched the comments, those need updating.
>> regards
> 
> Thanks for starting work on that. I've only had a quick look, but I
> can have a more detailed look once you've got it more complete.
> 

A review would be nice, although it can wait - It'd be interesting to 
know if those patches help with the workload(s) you've been looking at.

> For now it does not really look like the keeper block stuff is wired
> up the same way as in aset.c. I'd expect you to be allocating that in
> the same malloc as you're using to allocate the context struct itself
> in GenerationContextCreate().
> 

Yes, that difference is natural. The AllocSet works a bit differently, 
as it does not release the blocks (except during reset), while the 
Generation context frees the blocks. So it seems pointless to use the 
same "keeper" block as AllocSet - instead my intention was to keep one 
"allocated" block as a cache, which should help with tight pfree/palloc 
cycles. Maybe we should not call that "keeper" block?


> Also, likely as a result of the above, minContextSize does not seem to
> be wired up to anything apart from an Assert().
> 

Hmm, yeah. This is probably due to copying some of the block-growth and 
keeper block code from AllocSet. There should be just init/max block 
size, I think.

I did run the same set of benchmarks as for Slab, measuring some usual 
allocation patterns. The results for i5-2500k machine are attached (for 
the xeon it's almost exactly the same behavior). While running those 
tests I realized the last patch is wrong and sets allocChunkLimit=1, 
which is bogus and causes significant regression. So here's an updated 
version of the patch series too.


regards

-- 
Tomas Vondra
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company

Attachments:

  [text/x-patch] 0001-generation-bench-v2.patch (18.9K, ../36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com/2-0001-generation-bench-v2.patch)
  download | inline diff:
From bf127426d5ad7add41a49cc80348233aa49e7d4e Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 31 Jul 2021 22:55:36 +0200
Subject: [PATCH 1/4] generation bench

---
 contrib/generation_bench/.gitignore           |   4 +
 contrib/generation_bench/Makefile             |  21 +
 contrib/generation_bench/bench.sql            |  40 ++
 .../generation_bench--1.0.sql                 |  16 +
 contrib/generation_bench/generation_bench.c   | 438 ++++++++++++++++++
 .../generation_bench/generation_bench.control |   4 +
 6 files changed, 523 insertions(+)
 create mode 100644 contrib/generation_bench/.gitignore
 create mode 100644 contrib/generation_bench/Makefile
 create mode 100644 contrib/generation_bench/bench.sql
 create mode 100644 contrib/generation_bench/generation_bench--1.0.sql
 create mode 100644 contrib/generation_bench/generation_bench.c
 create mode 100644 contrib/generation_bench/generation_bench.control

diff --git a/contrib/generation_bench/.gitignore b/contrib/generation_bench/.gitignore
new file mode 100644
index 0000000000..5dcb3ff972
--- /dev/null
+++ b/contrib/generation_bench/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/generation_bench/Makefile b/contrib/generation_bench/Makefile
new file mode 100644
index 0000000000..0fee5b84db
--- /dev/null
+++ b/contrib/generation_bench/Makefile
@@ -0,0 +1,21 @@
+# contrib/generation_bench/Makefile
+
+MODULE_big = generation_bench
+OBJS = generation_bench.o
+
+EXTENSION = generation_bench
+DATA = generation_bench--1.0.sql
+PGFILEDESC = "generation_bench - slab context benchmarking functions"
+
+REGRESS = generation_bench
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/generation_bench
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/generation_bench/bench.sql b/contrib/generation_bench/bench.sql
new file mode 100644
index 0000000000..abf3511047
--- /dev/null
+++ b/contrib/generation_bench/bench.sql
@@ -0,0 +1,40 @@
+CREATE EXTENSION generation_bench;
+
+\o fifo-no-loops.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_fifo(1000000, block_size, chunk_size, 2*chunk_size, 0, 0, 0) x;
+
+\o lifo-no-loops.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_lifo(1000000, block_size, chunk_size, 2*chunk_size, 0, 0, 0) x;
+
+\o random-no-loops.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_random(1000000, block_size, chunk_size, 2*chunk_size, 0, 0, 0) x;
+
+
+\o fifo-increase.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_fifo(1000000, block_size, chunk_size, 2*chunk_size, 100, 10000, 15000) x;
+
+\o lifo-increase.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_lifo(1000000, block_size, chunk_size, 2*chunk_size, 100, 10000, 15000) x;
+
+\o random-increase.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_random(1000000, block_size, chunk_size, 2*chunk_size, 100, 10000, 15000) x;
+
+
+\o fifo-decrease.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_fifo(1000000, block_size, chunk_size, 2*chunk_size, 100, 10000, 5000) x;
+
+\o lifo-decrease.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_lifo(1000000, block_size, chunk_size, 2*chunk_size, 100, 10000, 5000) x;
+
+\o random-decrease.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_random(1000000, block_size, chunk_size, 2*chunk_size, 100, 10000, 5000) x;
+
+
+\o fifo-cycle.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_fifo(1000, block_size, chunk_size, 2*chunk_size, 10000, 1000, 1000) x;
+
+\o lifo-cycle.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_lifo(1000, block_size, chunk_size, 2*chunk_size, 10000, 1000, 1000) x;
+
+\o random-cycle.data
+select run, block_size, chunk_size, 2*chunk_size, 1000000 * chunk_size, x.* from generate_series(1,5) r(run), generate_series(32,512,32) a(chunk_size), (values (1024), (2048), (4096), (8192), (16384), (32768)) AS b(block_size), lateral generation_bench_random(1000, block_size, chunk_size, 2*chunk_size, 10000, 1000, 1000) x;
diff --git a/contrib/generation_bench/generation_bench--1.0.sql b/contrib/generation_bench/generation_bench--1.0.sql
new file mode 100644
index 0000000000..5f7c46eef2
--- /dev/null
+++ b/contrib/generation_bench/generation_bench--1.0.sql
@@ -0,0 +1,16 @@
+/* generation_bench--1.0.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION generation_bench" to load this file. \quit
+
+CREATE FUNCTION generation_bench_random(nallocs bigint, block_size bigint, min_alloc_size bigint, max_alloc_size bigint, loops int, free_cnt int, alloc_cnt int, out mem_allocated bigint, out alloc_ms bigint, out free_ms bigint)
+AS 'MODULE_PATHNAME', 'generation_bench_random'
+LANGUAGE C VOLATILE STRICT;
+
+CREATE FUNCTION generation_bench_fifo(nallocs bigint, block_size bigint, min_alloc_size bigint, max_alloc_size bigint, loops int, free_cnt int, alloc_cnt int, out mem_allocated bigint, out alloc_ms bigint, out free_ms bigint)
+AS 'MODULE_PATHNAME', 'generation_bench_fifo'
+LANGUAGE C VOLATILE STRICT;
+
+CREATE FUNCTION generation_bench_lifo(nallocs bigint, block_size bigint, min_alloc_size bigint, max_alloc_size bigint, loops int, free_cnt int, alloc_cnt int, out mem_allocated bigint, out alloc_ms bigint, out free_ms bigint)
+AS 'MODULE_PATHNAME', 'generation_bench_lifo'
+LANGUAGE C VOLATILE STRICT;
diff --git a/contrib/generation_bench/generation_bench.c b/contrib/generation_bench/generation_bench.c
new file mode 100644
index 0000000000..1cff55b751
--- /dev/null
+++ b/contrib/generation_bench/generation_bench.c
@@ -0,0 +1,438 @@
+/*-------------------------------------------------------------------------
+ *
+ * generation_bench.c
+ *
+ * helper functions to benchmark generation context with different workloads
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <sys/time.h>
+
+#include "funcapi.h"
+#include "miscadmin.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(generation_bench_random);
+PG_FUNCTION_INFO_V1(generation_bench_fifo);
+PG_FUNCTION_INFO_V1(generation_bench_lifo);
+
+typedef struct Chunk {
+	int		random;
+	void   *ptr;
+} Chunk;
+
+static int
+chunk_index_cmp(const void *a, const void *b)
+{
+	Chunk *ca = (Chunk *) a;
+	Chunk *cb = (Chunk *) b;
+
+	if (ca->random < cb->random)
+		return -1;
+	else if (ca->random > cb->random)
+		return 1;
+
+	return 0;
+}
+
+Datum
+generation_bench_random(PG_FUNCTION_ARGS)
+{
+	MemoryContext	cxt,
+					oldcxt;
+	Chunk		   *chunks;
+	int64			i, j;
+	int64			nallocs = PG_GETARG_INT64(0);
+	int64			blockSize = PG_GETARG_INT64(1);
+	int64			minChunkSize = PG_GETARG_INT64(2);
+	int64			maxChunkSize = PG_GETARG_INT64(3);
+
+	int				nloops = PG_GETARG_INT32(4);
+	int				free_cnt = PG_GETARG_INT32(5);
+	int				alloc_cnt = PG_GETARG_INT32(6);
+
+	struct timeval	start_time,
+					end_time;
+	int64			alloc_time = 0,
+					free_time = 0;
+	int64			mem_allocated;
+
+	TupleDesc		tupdesc;
+	Datum			result;
+	HeapTuple		tuple;
+	Datum			values[9];
+	bool			nulls[9];
+
+	int				maxchunks;
+
+	maxchunks = nallocs + nloops * Max(0, alloc_cnt - free_cnt);
+
+	cxt = GenerationContextCreate(CurrentMemoryContext, "generation_bench", blockSize);
+
+	chunks = (Chunk *) palloc(maxchunks * sizeof(Chunk));
+
+	/* allocate the chunks in random order */
+	oldcxt = MemoryContextSwitchTo(cxt);
+
+	gettimeofday(&start_time, NULL);
+
+	for (i = 0; i < nallocs; i++)
+	{
+		int chunkSize = minChunkSize + random() % (maxChunkSize - minChunkSize);
+
+		chunks[i].ptr = palloc(chunkSize);
+	}
+
+	gettimeofday(&end_time, NULL);
+
+	alloc_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+				  (end_time.tv_usec - start_time.tv_usec);
+
+	MemoryContextSwitchTo(oldcxt);
+
+	mem_allocated = MemoryContextMemAllocated(cxt, true);
+
+	/* do the requested number of free/alloc loops */
+	for (j = 0; j < nloops; j++)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		/* randomize the indexes */
+		for (i = 0; i < nallocs; i++)
+			chunks[i].random = random();
+
+		qsort(chunks, nallocs, sizeof(Chunk), chunk_index_cmp);
+
+		oldcxt = MemoryContextSwitchTo(cxt);
+
+		gettimeofday(&start_time, NULL);
+
+		/* free the first free_cnt chunks */
+		for (i = 0; i < Min(nallocs, free_cnt); i++)
+			pfree(chunks[i].ptr);
+
+		gettimeofday(&end_time, NULL);
+
+		nallocs -= Min(nallocs, free_cnt);
+
+		free_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+					 (end_time.tv_usec - start_time.tv_usec);
+
+		memmove(chunks, &chunks[free_cnt], nallocs * sizeof(Chunk));
+
+
+		/* allocate alloc_cnt chunks at the end */
+		gettimeofday(&start_time, NULL);
+
+		/* free the first free_cnt chunks */
+		for (i = 0; i < alloc_cnt; i++)
+		{
+			int chunkSize = minChunkSize + random() % (maxChunkSize - minChunkSize);
+
+			chunks[nallocs + i].ptr = palloc(chunkSize);
+		}
+
+		gettimeofday(&end_time, NULL);
+
+		nallocs += alloc_cnt;
+
+		alloc_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+					  (end_time.tv_usec - start_time.tv_usec);
+
+		MemoryContextSwitchTo(oldcxt);
+
+		mem_allocated = Max(mem_allocated, MemoryContextMemAllocated(cxt, true));
+	}
+
+	/* release the chunks in random order */
+	for (i = 0; i < nallocs; i++)
+		chunks[i].random = random();
+
+	qsort(chunks, nallocs, sizeof(Chunk), chunk_index_cmp);
+
+	gettimeofday(&start_time, NULL);
+
+	for (i = 0; i < nallocs; i++)
+		pfree(chunks[i].ptr);
+
+	gettimeofday(&end_time, NULL);
+
+	free_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+				 (end_time.tv_usec - start_time.tv_usec);
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	values[0] = Int64GetDatum(mem_allocated);
+	values[1] = Int64GetDatum(alloc_time);
+	values[2] = Int64GetDatum(free_time);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+Datum
+generation_bench_fifo(PG_FUNCTION_ARGS)
+{
+	MemoryContext	cxt,
+					oldcxt;
+	Chunk		   *chunks;
+	int64			i, j;
+	int64			nallocs = PG_GETARG_INT64(0);
+	int64			blockSize = PG_GETARG_INT64(1);
+	int64			minChunkSize = PG_GETARG_INT64(2);
+	int64			maxChunkSize = PG_GETARG_INT64(3);
+
+	int				nloops = PG_GETARG_INT32(4);
+	int				free_cnt = PG_GETARG_INT32(5);
+	int				alloc_cnt = PG_GETARG_INT32(6);
+
+	struct timeval	start_time,
+					end_time;
+	int64			alloc_time = 0,
+					free_time = 0;
+	int64			mem_allocated;
+
+	TupleDesc		tupdesc;
+	Datum			result;
+	HeapTuple		tuple;
+	Datum			values[9];
+	bool			nulls[9];
+
+	int				maxchunks;
+
+	maxchunks = nallocs + nloops * Max(0, alloc_cnt - free_cnt);
+
+	cxt = GenerationContextCreate(CurrentMemoryContext, "generation_bench", blockSize);
+
+	chunks = (Chunk *) palloc(maxchunks * sizeof(Chunk));
+
+	oldcxt = MemoryContextSwitchTo(cxt);
+
+	gettimeofday(&start_time, NULL);
+
+	for (i = 0; i < nallocs; i++)
+	{
+		int chunkSize = minChunkSize + random() % (maxChunkSize - minChunkSize);
+
+		chunks[i].ptr = palloc(chunkSize);
+	}
+
+	gettimeofday(&end_time, NULL);
+
+	alloc_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+				  (end_time.tv_usec - start_time.tv_usec);
+
+	MemoryContextSwitchTo(oldcxt);
+
+	mem_allocated = MemoryContextMemAllocated(cxt, true);
+
+
+	/* do the requested number of free/alloc loops */
+	for (j = 0; j < nloops; j++)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		oldcxt = MemoryContextSwitchTo(cxt);
+
+		gettimeofday(&start_time, NULL);
+
+		/* free the first free_cnt chunks */
+		for (i = 0; i < Min(nallocs, free_cnt); i++)
+			pfree(chunks[i].ptr);
+
+		gettimeofday(&end_time, NULL);
+
+		nallocs -= Min(nallocs, free_cnt);
+
+		free_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+					 (end_time.tv_usec - start_time.tv_usec);
+
+		memmove(chunks, &chunks[free_cnt], nallocs * sizeof(Chunk));
+
+		/* allocate alloc_cnt chunks at the end */
+		gettimeofday(&start_time, NULL);
+
+		/* free the first free_cnt chunks */
+		for (i = 0; i < alloc_cnt; i++)
+		{
+			int chunkSize = minChunkSize + random() % (maxChunkSize - minChunkSize);
+
+			chunks[nallocs + i].ptr = palloc(chunkSize);
+		}
+
+		gettimeofday(&end_time, NULL);
+
+		nallocs += alloc_cnt;
+
+		alloc_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+					  (end_time.tv_usec - start_time.tv_usec);
+
+		MemoryContextSwitchTo(oldcxt);
+
+		mem_allocated = Max(mem_allocated, MemoryContextMemAllocated(cxt, true));
+	}
+
+
+	gettimeofday(&start_time, NULL);
+
+	for (i = 0; i < nallocs; i++)
+		pfree(chunks[i].ptr);
+
+	gettimeofday(&end_time, NULL);
+
+	free_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+				 (end_time.tv_usec - start_time.tv_usec);
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	values[0] = Int64GetDatum(mem_allocated);
+	values[1] = Int64GetDatum(alloc_time);
+	values[2] = Int64GetDatum(free_time);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+Datum
+generation_bench_lifo(PG_FUNCTION_ARGS)
+{
+	MemoryContext	cxt,
+					oldcxt;
+	Chunk		  *chunks;
+	int64			i, j;
+	int64			nallocs = PG_GETARG_INT64(0);
+	int64			blockSize = PG_GETARG_INT64(1);
+	int64			minChunkSize = PG_GETARG_INT64(2);
+	int64			maxChunkSize = PG_GETARG_INT64(3);
+
+	int				nloops = PG_GETARG_INT32(4);
+	int				free_cnt = PG_GETARG_INT32(5);
+	int				alloc_cnt = PG_GETARG_INT32(6);
+
+	struct timeval	start_time,
+					end_time;
+	int64			alloc_time = 0,
+					free_time = 0;
+	int64			mem_allocated;
+
+	TupleDesc		tupdesc;
+	Datum			result;
+	HeapTuple		tuple;
+	Datum			values[9];
+	bool			nulls[9];
+
+	int				maxchunks;
+
+	maxchunks = nallocs + nloops * Max(0, alloc_cnt - free_cnt);
+
+	cxt = GenerationContextCreate(CurrentMemoryContext, "generation_bench", blockSize);
+
+	chunks = (Chunk *) palloc(maxchunks * sizeof(Chunk));
+
+	oldcxt = MemoryContextSwitchTo(cxt);
+
+	/* palloc benchmark */
+	gettimeofday(&start_time, NULL);
+
+	for (i = 0; i < nallocs; i++)
+	{
+		int chunkSize = minChunkSize + random() % (maxChunkSize - minChunkSize);
+
+		chunks[i].ptr = palloc(chunkSize);
+	}
+
+	gettimeofday(&end_time, NULL);
+
+	alloc_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+				  (end_time.tv_usec - start_time.tv_usec);
+
+	MemoryContextSwitchTo(oldcxt);
+
+	mem_allocated = MemoryContextMemAllocated(cxt, true);
+
+
+	/* do the requested number of free/alloc loops */
+	for (j = 0; j < nloops; j++)
+	{
+		CHECK_FOR_INTERRUPTS();
+
+		oldcxt = MemoryContextSwitchTo(cxt);
+
+		gettimeofday(&start_time, NULL);
+
+		/* free the first free_cnt chunks */
+		for (i = 1; i <= Min(nallocs, free_cnt); i++)
+			pfree(chunks[nallocs - i].ptr);
+
+		gettimeofday(&end_time, NULL);
+
+		nallocs -= Min(nallocs, free_cnt);
+
+		free_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+					 (end_time.tv_usec - start_time.tv_usec);
+
+		/* allocate alloc_cnt chunks at the end */
+		gettimeofday(&start_time, NULL);
+
+		/* free the first free_cnt chunks */
+		for (i = 0; i < alloc_cnt; i++)
+		{
+			int chunkSize = minChunkSize + random() % (maxChunkSize - minChunkSize);
+
+			chunks[nallocs + i].ptr = palloc(chunkSize);
+		}
+
+		gettimeofday(&end_time, NULL);
+
+		nallocs += alloc_cnt;
+
+		alloc_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+					  (end_time.tv_usec - start_time.tv_usec);
+
+		MemoryContextSwitchTo(oldcxt);
+
+		mem_allocated = Max(mem_allocated, MemoryContextMemAllocated(cxt, true));
+	}
+
+
+	gettimeofday(&start_time, NULL);
+
+	for (i = (nallocs - 1); i >= 0; i--)
+		pfree(chunks[i].ptr);
+
+	gettimeofday(&end_time, NULL);
+
+	free_time += (end_time.tv_sec - start_time.tv_sec) * 1000000L +
+				 (end_time.tv_usec - start_time.tv_usec);
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	values[0] = Int64GetDatum(mem_allocated);
+	values[1] = Int64GetDatum(alloc_time);
+	values[2] = Int64GetDatum(free_time);
+
+	memset(nulls, 0, sizeof(nulls));
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	MemoryContextDelete(cxt);
+
+	PG_RETURN_DATUM(result);
+}
diff --git a/contrib/generation_bench/generation_bench.control b/contrib/generation_bench/generation_bench.control
new file mode 100644
index 0000000000..8c81e22e6b
--- /dev/null
+++ b/contrib/generation_bench/generation_bench.control
@@ -0,0 +1,4 @@
+# generation_bench extension
+comment = 'functions for benchmarking generation context'
+default_version = '1.0'
+module_pathname = '$libdir/generation_bench'
-- 
2.31.1

  [text/x-patch] 0002-Generation-grow-blocks-v2.patch (7.5K, ../36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com/3-0002-Generation-grow-blocks-v2.patch)
  download | inline diff:
From 3f858dfa27bd4faca7898d5edda1be5e60bb60cc Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Fri, 30 Jul 2021 23:53:52 +0200
Subject: [PATCH 2/4] Generation: grow blocks

---
 contrib/generation_bench/generation_bench.c   |  6 +--
 src/backend/access/gist/gistvacuum.c          |  2 +-
 .../replication/logical/reorderbuffer.c       |  2 +-
 src/backend/utils/mmgr/generation.c           | 53 ++++++++++++++-----
 src/include/utils/memutils.h                  |  4 +-
 5 files changed, 47 insertions(+), 20 deletions(-)

diff --git a/contrib/generation_bench/generation_bench.c b/contrib/generation_bench/generation_bench.c
index 1cff55b751..bc3d631205 100644
--- a/contrib/generation_bench/generation_bench.c
+++ b/contrib/generation_bench/generation_bench.c
@@ -69,7 +69,7 @@ generation_bench_random(PG_FUNCTION_ARGS)
 
 	maxchunks = nallocs + nloops * Max(0, alloc_cnt - free_cnt);
 
-	cxt = GenerationContextCreate(CurrentMemoryContext, "generation_bench", blockSize);
+	cxt = GenerationContextCreate(CurrentMemoryContext, "generation_bench", blockSize, blockSize, 1024L * 1024L);
 
 	chunks = (Chunk *) palloc(maxchunks * sizeof(Chunk));
 
@@ -210,7 +210,7 @@ generation_bench_fifo(PG_FUNCTION_ARGS)
 
 	maxchunks = nallocs + nloops * Max(0, alloc_cnt - free_cnt);
 
-	cxt = GenerationContextCreate(CurrentMemoryContext, "generation_bench", blockSize);
+	cxt = GenerationContextCreate(CurrentMemoryContext, "generation_bench", blockSize, blockSize, 1024L * 1024L);
 
 	chunks = (Chunk *) palloc(maxchunks * sizeof(Chunk));
 
@@ -339,7 +339,7 @@ generation_bench_lifo(PG_FUNCTION_ARGS)
 
 	maxchunks = nallocs + nloops * Max(0, alloc_cnt - free_cnt);
 
-	cxt = GenerationContextCreate(CurrentMemoryContext, "generation_bench", blockSize);
+	cxt = GenerationContextCreate(CurrentMemoryContext, "generation_bench", blockSize, blockSize, 1024L * 1024L);
 
 	chunks = (Chunk *) palloc(maxchunks * sizeof(Chunk));
 
diff --git a/src/backend/access/gist/gistvacuum.c b/src/backend/access/gist/gistvacuum.c
index 0663193531..1818ed06fc 100644
--- a/src/backend/access/gist/gistvacuum.c
+++ b/src/backend/access/gist/gistvacuum.c
@@ -161,7 +161,7 @@ gistvacuumscan(IndexVacuumInfo *info, IndexBulkDeleteResult *stats,
 	 */
 	vstate.page_set_context = GenerationContextCreate(CurrentMemoryContext,
 													  "GiST VACUUM page set context",
-													  16 * 1024);
+													  ALLOCSET_DEFAULT_SIZES);
 	oldctx = MemoryContextSwitchTo(vstate.page_set_context);
 	vstate.internal_page_set = intset_create();
 	vstate.empty_leaf_set = intset_create();
diff --git a/src/backend/replication/logical/reorderbuffer.c b/src/backend/replication/logical/reorderbuffer.c
index 7378beb684..308d833292 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -329,7 +329,7 @@ ReorderBufferAllocate(void)
 
 	buffer->tup_context = GenerationContextCreate(new_ctx,
 												  "Tuples",
-												  SLAB_LARGE_BLOCK_SIZE);
+												  ALLOCSET_DEFAULT_SIZES);
 
 	hash_ctl.keysize = sizeof(TransactionId);
 	hash_ctl.entrysize = sizeof(ReorderBufferTXNByIdEnt);
diff --git a/src/backend/utils/mmgr/generation.c b/src/backend/utils/mmgr/generation.c
index 584cd614da..771a2525ca 100644
--- a/src/backend/utils/mmgr/generation.c
+++ b/src/backend/utils/mmgr/generation.c
@@ -60,7 +60,9 @@ typedef struct GenerationContext
 	MemoryContextData header;	/* Standard memory-context fields */
 
 	/* Generational context parameters */
-	Size		blockSize;		/* standard block size */
+	Size		initBlockSize;	/* initial block size */
+	Size		maxBlockSize;	/* maximum block size */
+	Size		nextBlockSize;	/* next block size to allocate */
 
 	GenerationBlock *block;		/* current (most recently allocated) block */
 	dlist_head	blocks;			/* list of blocks */
@@ -196,7 +198,9 @@ static const MemoryContextMethods GenerationMethods = {
 MemoryContext
 GenerationContextCreate(MemoryContext parent,
 						const char *name,
-						Size blockSize)
+						Size minContextSize,
+						Size initBlockSize,
+						Size maxBlockSize)
 {
 	GenerationContext *set;
 
@@ -208,16 +212,20 @@ GenerationContextCreate(MemoryContext parent,
 					 "padding calculation in GenerationChunk is wrong");
 
 	/*
-	 * First, validate allocation parameters.  (If we're going to throw an
-	 * error, we should do so before the context is created, not after.)  We
-	 * somewhat arbitrarily enforce a minimum 1K block size, mostly because
-	 * that's what AllocSet does.
+	 * First, validate allocation parameters.  Once these were regular runtime
+	 * test and elog's, but in practice Asserts seem sufficient because nobody
+	 * varies their parameters at runtime.  We somewhat arbitrarily enforce a
+	 * minimum 1K block size.
 	 */
-	if (blockSize != MAXALIGN(blockSize) ||
-		blockSize < 1024 ||
-		!AllocHugeSizeIsValid(blockSize))
-		elog(ERROR, "invalid blockSize for memory context: %zu",
-			 blockSize);
+	Assert(initBlockSize == MAXALIGN(initBlockSize) &&
+		   initBlockSize >= 1024);
+	Assert(maxBlockSize == MAXALIGN(maxBlockSize) &&
+		   maxBlockSize >= initBlockSize &&
+		   AllocHugeSizeIsValid(maxBlockSize)); /* must be safe to double */
+	Assert(minContextSize == 0 ||
+		   (minContextSize == MAXALIGN(minContextSize) &&
+			minContextSize >= 1024 &&
+			minContextSize <= maxBlockSize));
 
 	/*
 	 * Allocate the context header.  Unlike aset.c, we never try to combine
@@ -242,7 +250,9 @@ GenerationContextCreate(MemoryContext parent,
 	 */
 
 	/* Fill in GenerationContext-specific header fields */
-	set->blockSize = blockSize;
+	set->initBlockSize = initBlockSize;
+	set->maxBlockSize = maxBlockSize;
+	set->nextBlockSize = initBlockSize;
 	set->block = NULL;
 	dlist_init(&set->blocks);
 
@@ -293,6 +303,9 @@ GenerationReset(MemoryContext context)
 
 	set->block = NULL;
 
+	/* Reset block size allocation sequence, too */
+	set->nextBlockSize = set->initBlockSize;
+
 	Assert(dlist_is_empty(&set->blocks));
 }
 
@@ -329,9 +342,12 @@ GenerationAlloc(MemoryContext context, Size size)
 	GenerationBlock *block;
 	GenerationChunk *chunk;
 	Size		chunk_size = MAXALIGN(size);
+	Size		blockSize;
+
+	blockSize = (set->block) ? set->block->blksize : set->nextBlockSize;
 
 	/* is it an over-sized chunk? if yes, allocate special block */
-	if (chunk_size > set->blockSize / 8)
+	if (chunk_size > (blockSize / 8))
 	{
 		Size		blksize = chunk_size + Generation_BLOCKHDRSZ + Generation_CHUNKHDRSZ;
 
@@ -387,7 +403,16 @@ GenerationAlloc(MemoryContext context, Size size)
 	if ((block == NULL) ||
 		(block->endptr - block->freeptr) < Generation_CHUNKHDRSZ + chunk_size)
 	{
-		Size		blksize = set->blockSize;
+		Size		blksize;
+
+		/*
+		 * The first such block has size initBlockSize, and we double the
+		 * space in each succeeding block, but not more than maxBlockSize.
+		 */
+		blksize = set->nextBlockSize;
+		set->nextBlockSize <<= 1;
+		if (set->nextBlockSize > set->maxBlockSize)
+			set->nextBlockSize = set->maxBlockSize;
 
 		block = (GenerationBlock *) malloc(blksize);
 
diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h
index ff872274d4..514c0bf75b 100644
--- a/src/include/utils/memutils.h
+++ b/src/include/utils/memutils.h
@@ -183,7 +183,9 @@ extern MemoryContext SlabContextCreate(MemoryContext parent,
 /* generation.c */
 extern MemoryContext GenerationContextCreate(MemoryContext parent,
 											 const char *name,
-											 Size blockSize);
+											 Size minContextSize,
+											 Size initBlockSize,
+											 Size maxBlockSize);
 
 /*
  * Recommended default alloc parameters, suitable for "ordinary" contexts
-- 
2.31.1

  [text/x-patch] 0003-Generation-keeper-block-v2.patch (4.4K, ../36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com/4-0003-Generation-keeper-block-v2.patch)
  download | inline diff:
From e27b30bfb7b179b6a1b47c4907e562521e6fb5cd Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 31 Jul 2021 02:54:36 +0200
Subject: [PATCH 3/4] Generation: keeper block

---
 src/backend/utils/mmgr/generation.c | 61 +++++++++++++++++++++++++----
 1 file changed, 54 insertions(+), 7 deletions(-)

diff --git a/src/backend/utils/mmgr/generation.c b/src/backend/utils/mmgr/generation.c
index 771a2525ca..6c90416e27 100644
--- a/src/backend/utils/mmgr/generation.c
+++ b/src/backend/utils/mmgr/generation.c
@@ -65,6 +65,7 @@ typedef struct GenerationContext
 	Size		nextBlockSize;	/* next block size to allocate */
 
 	GenerationBlock *block;		/* current (most recently allocated) block */
+	GenerationBlock *keeper;	/* keeper block */
 	dlist_head	blocks;			/* list of blocks */
 } GenerationContext;
 
@@ -254,6 +255,7 @@ GenerationContextCreate(MemoryContext parent,
 	set->maxBlockSize = maxBlockSize;
 	set->nextBlockSize = initBlockSize;
 	set->block = NULL;
+	set->keeper = NULL;
 	dlist_init(&set->blocks);
 
 	/* Finally, do the type-independent part of context creation */
@@ -302,6 +304,7 @@ GenerationReset(MemoryContext context)
 	}
 
 	set->block = NULL;
+	set->keeper = NULL;
 
 	/* Reset block size allocation sequence, too */
 	set->nextBlockSize = set->initBlockSize;
@@ -344,7 +347,7 @@ GenerationAlloc(MemoryContext context, Size size)
 	Size		chunk_size = MAXALIGN(size);
 	Size		blockSize;
 
-	blockSize = (set->block) ? set->block->blksize : set->nextBlockSize;
+	blockSize = set->initBlockSize;
 
 	/* is it an over-sized chunk? if yes, allocate special block */
 	if (chunk_size > (blockSize / 8))
@@ -400,6 +403,26 @@ GenerationAlloc(MemoryContext context, Size size)
 	 */
 	block = set->block;
 
+	/*
+	 * If we can't use the current block, and we have a keeper block with
+	 * enough free space in it, use it as the block.
+	 *
+	 * XXX We don't want to do this when there's not enough free space
+	 * (although the keeper block should be empty, so not sure if checking
+	 * the space in the keeper block is necessary).
+	 */
+	if (((block == NULL) ||
+		(block->endptr - block->freeptr) < Generation_CHUNKHDRSZ + chunk_size) &&
+		(set->keeper != NULL) &&
+		(set->keeper->endptr - set->keeper->freeptr) < Generation_CHUNKHDRSZ + chunk_size)
+	{
+		block = set->keeper;
+		set->keeper = NULL;
+
+		/* keeper block was not counted as allocated, so add it back */
+		context->mem_allocated += block->blksize;
+	}
+
 	if ((block == NULL) ||
 		(block->endptr - block->freeptr) < Generation_CHUNKHDRSZ + chunk_size)
 	{
@@ -524,16 +547,34 @@ GenerationFree(MemoryContext context, void *pointer)
 	if (block->nfree < block->nchunks)
 		return;
 
+	/* Also make sure the block is not marked as the current block. */
+	if (set->block == block)
+		set->block = NULL;
+
+	/* Keep the block for reuse, if we don't have one already. */
+	if (!set->keeper && block->blksize <= set->maxBlockSize)
+	{
+		/* reset the pointers before we use it as keeper block */
+		block->freeptr = ((char *) block) + Generation_BLOCKHDRSZ;
+		block->endptr = ((char *) block) + block->blksize;
+
+		block->nfree = 0;
+		block->nchunks = 0;
+
+		set->keeper = block;
+
+		/* keeper block is not counted as allocated */
+		context->mem_allocated -= block->blksize;
+
+		return;
+	}
+
 	/*
 	 * The block is empty, so let's get rid of it. First remove it from the
 	 * list of blocks, then return it to malloc().
 	 */
 	dlist_delete(&block->node);
 
-	/* Also make sure the block is not marked as the current block. */
-	if (set->block == block)
-		set->block = NULL;
-
 	context->mem_allocated -= block->blksize;
 	free(block);
 }
@@ -770,13 +811,19 @@ GenerationCheck(MemoryContext context)
 					nchunks;
 		char	   *ptr;
 
-		total_allocated += block->blksize;
+		/*
+		 * The keeper block in the list of blocks, but we don't consider it
+		 * as allocated in memory accounting. So don't include it in the sum.
+		 */
+		if (block != gen->keeper)
+			total_allocated += block->blksize;
 
 		/*
 		 * nfree > nchunks is surely wrong, and we don't expect to see
 		 * equality either, because such a block should have gotten freed.
 		 */
-		if (block->nfree >= block->nchunks)
+		if ((block->nfree > block->nchunks) &&
+			((block != gen->keeper) && (block->nfree == block->nchunks)))
 			elog(WARNING, "problem in Generation %s: number of free chunks %d in block %p exceeds %d allocated",
 				 name, block->nfree, block, block->nchunks);
 
-- 
2.31.1

  [text/x-patch] 0004-Generation-allocChunkLimit-v2.patch (3.0K, ../36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com/5-0004-Generation-allocChunkLimit-v2.patch)
  download | inline diff:
From 5de9bdbb42874a8b2706a80f4977c6009dfabbbf Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas.vondra@postgresql.org>
Date: Sat, 31 Jul 2021 03:20:56 +0200
Subject: [PATCH 4/4] Generation: allocChunkLimit

---
 src/backend/utils/mmgr/generation.c | 28 ++++++++++++++++++++++++----
 1 file changed, 24 insertions(+), 4 deletions(-)

diff --git a/src/backend/utils/mmgr/generation.c b/src/backend/utils/mmgr/generation.c
index 6c90416e27..2c98877953 100644
--- a/src/backend/utils/mmgr/generation.c
+++ b/src/backend/utils/mmgr/generation.c
@@ -46,6 +46,8 @@
 #define Generation_BLOCKHDRSZ	MAXALIGN(sizeof(GenerationBlock))
 #define Generation_CHUNKHDRSZ	sizeof(GenerationChunk)
 
+#define Generation_CHUNK_FRACTION	8
+
 typedef struct GenerationBlock GenerationBlock; /* forward reference */
 typedef struct GenerationChunk GenerationChunk;
 
@@ -63,6 +65,7 @@ typedef struct GenerationContext
 	Size		initBlockSize;	/* initial block size */
 	Size		maxBlockSize;	/* maximum block size */
 	Size		nextBlockSize;	/* next block size to allocate */
+	Size		allocChunkLimit;	/* effective chunk size limit */
 
 	GenerationBlock *block;		/* current (most recently allocated) block */
 	GenerationBlock *keeper;	/* keeper block */
@@ -254,6 +257,17 @@ GenerationContextCreate(MemoryContext parent,
 	set->initBlockSize = initBlockSize;
 	set->maxBlockSize = maxBlockSize;
 	set->nextBlockSize = initBlockSize;
+
+	/*
+	 * Compute the allocation chunk size limit for this context.
+	 *
+	 * Follows similar ideas as AllocSet, see aset.c for details ...
+	 */
+	set->allocChunkLimit = maxBlockSize;
+	while ((Size) (set->allocChunkLimit + Generation_CHUNKHDRSZ) >
+		   (Size) ((Size) (maxBlockSize - Generation_BLOCKHDRSZ) / Generation_CHUNK_FRACTION))
+		set->allocChunkLimit >>= 1;
+
 	set->block = NULL;
 	set->keeper = NULL;
 	dlist_init(&set->blocks);
@@ -345,12 +359,9 @@ GenerationAlloc(MemoryContext context, Size size)
 	GenerationBlock *block;
 	GenerationChunk *chunk;
 	Size		chunk_size = MAXALIGN(size);
-	Size		blockSize;
-
-	blockSize = set->initBlockSize;
 
 	/* is it an over-sized chunk? if yes, allocate special block */
-	if (chunk_size > (blockSize / 8))
+	if (chunk_size > set->allocChunkLimit)
 	{
 		Size		blksize = chunk_size + Generation_BLOCKHDRSZ + Generation_CHUNKHDRSZ;
 
@@ -427,6 +438,7 @@ GenerationAlloc(MemoryContext context, Size size)
 		(block->endptr - block->freeptr) < Generation_CHUNKHDRSZ + chunk_size)
 	{
 		Size		blksize;
+		Size		required_size;
 
 		/*
 		 * The first such block has size initBlockSize, and we double the
@@ -437,6 +449,14 @@ GenerationAlloc(MemoryContext context, Size size)
 		if (set->nextBlockSize > set->maxBlockSize)
 			set->nextBlockSize = set->maxBlockSize;
 
+		/*
+		 * If initBlockSize is less than ALLOC_CHUNK_LIMIT, we could need more
+		 * space... but try to keep it a power of 2.
+		 */
+		required_size = chunk_size + Generation_BLOCKHDRSZ + Generation_CHUNKHDRSZ;
+		while (blksize < required_size)
+			blksize <<= 1;
+
 		block = (GenerationBlock *) malloc(blksize);
 
 		if (block == NULL)
-- 
2.31.1

  [application/vnd.oasis.opendocument.spreadsheet] generation i5.ods (889.8K, ../36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com/6-generation%20i5.ods)
  download

view thread (50+ messages)  latest in thread

Message-ID: <36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com>
Permalink:  ../36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com/
Also on:    postgresql.org/message-id/36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com

 ·  · 

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: pgsql-hackers@postgresql.org
  Cc: tomas.vondra@enterprisedb.com, dgrowleyml@gmail.com, andres@anarazel.de, tv@fuzzy.cz, pgsql-hackers@lists.postgresql.org
  Subject: Re: Use generation context to speed up tuplesorts
  In-Reply-To: <36829a8a-63d0-c428-89d5-07e49561973e@enterprisedb.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox