[PATCH] Add pg_lfind8_nonzero()

Started by cca550729 days ago5 messages
#1cca5507
cca5507@qq.com
3 attachment(s)

Hi hackers,

I'd like to add pg_lfind8_nonzero() to optimize some code like this:

```
for (i = 0; i < numberOfAttributes; i++)
{
if (isnull[i])
{
hasnull = true;
break;
}
}
```

With pg_lfind8_nonzero(), we can write the code like this:

```
if (likely(numberOfAttributes > 0))
hasnull = pg_lfind8_nonzero((uint8 *) isnull, numberOfAttributes);
```

The pg_lfind8_nonzero() is faster because we can handle 8 bool values at a time.

v1-0001 add the pg_lfind8_nonzero().

v1-0002 use the pg_lfind8_nonzero() in some places.

v1-0003 add a extension "test_patch" only for testing, the test like this:

```
for (i = 0; i < 1000; i++)
{
for (int j = 0; j < natts; j++)
{
if (isnull[j])
{
hasnull = true;
break;
}
}
}
=======================
for (i = 0; i < 1000; i++)
{
if (likely(natts > 0))
hasnull = pg_lfind8_nonzero((uint8 *) isnull, natts);
}
```

create extension test_patch;
# 4 is the natts
select test_head(4);
select test_patch(4);

Test result:
natts: 4 head: 1984ns patch: 2094ns
natts: 8 head: 3196ns patch: 641ns
natts: 16 head: 4589ns patch: 752ns
natts: 32 head: 8036ns patch: 1152ns
natts: 64 head: 19367ns patch: 2455ns
natts: 128 head: 33445ns patch: 4018ns

Looking forward to your comments!

--
Regards,
ChangAo Chen

Attachments:

v1-0001-Add-pg_lfind8_nonzero.patchapplication/octet-stream; charset=utf-8; name=v1-0001-Add-pg_lfind8_nonzero.patchDownload
From ee5da25dbba924c1c9a399568c27db8ac603e2d8 Mon Sep 17 00:00:00 2001
From: ChangAo Chen <cca5507@qq.com>
Date: Sun, 14 Dec 2025 18:13:31 +0800
Subject: [PATCH v1 1/3] Add pg_lfind8_nonzero().

---
 src/include/port/pg_lfind.h                   | 40 +++++++++++++++++++
 .../test_lfind/expected/test_lfind.out        |  6 +++
 .../modules/test_lfind/sql/test_lfind.sql     |  1 +
 .../modules/test_lfind/test_lfind--1.0.sql    |  4 ++
 src/test/modules/test_lfind/test_lfind.c      | 17 ++++++++
 5 files changed, 68 insertions(+)

diff --git a/src/include/port/pg_lfind.h b/src/include/port/pg_lfind.h
index 20f7497dcb7..8dd7b81ebba 100644
--- a/src/include/port/pg_lfind.h
+++ b/src/include/port/pg_lfind.h
@@ -80,6 +80,46 @@ pg_lfind8_le(uint8 key, uint8 *base, uint32 nelem)
 	return false;
 }
 
+/*
+ * pg_lfind8_nonzero
+ *
+ * Return true if there is an element in 'base' that is not equal to
+ * zero, otherwise return false.
+ */
+static inline bool
+pg_lfind8_nonzero(uint8 *base, uint32 nelem)
+{
+	uint32      i;
+	uint32		tail_idx;
+	uint64      chunk;
+
+	/* Search the array one-by-one if there aren't enough elements. */
+	if (nelem < sizeof(chunk))
+	{
+		for (i = 0; i < nelem; i++)
+		{
+			if (base[i] != 0)
+				return true;
+		}
+		return false;
+	}
+
+	/* Round down to multiple of chunk length. */
+	tail_idx = nelem & ~(sizeof(chunk) - 1);
+
+	for (i = 0; i < tail_idx; i += sizeof(chunk))
+	{
+		/* memcpy() to avoid unaligned access. */
+		memcpy(&chunk, &base[i], sizeof(chunk));
+		if (chunk != 0)
+			return true;
+	}
+
+	/* Process the last chunk in the array. */
+	memcpy(&chunk, &base[nelem - sizeof(chunk)], sizeof(chunk));
+	return chunk != 0;
+}
+
 /*
  * pg_lfind32_one_by_one_helper
  *
diff --git a/src/test/modules/test_lfind/expected/test_lfind.out b/src/test/modules/test_lfind/expected/test_lfind.out
index 1d4b14e7032..4f43a729b51 100644
--- a/src/test/modules/test_lfind/expected/test_lfind.out
+++ b/src/test/modules/test_lfind/expected/test_lfind.out
@@ -16,6 +16,12 @@ SELECT test_lfind8_le();
  
 (1 row)
 
+SELECT test_lfind8_nonzero();
+ test_lfind8_nonzero 
+---------------------
+ 
+(1 row)
+
 SELECT test_lfind32();
  test_lfind32 
 --------------
diff --git a/src/test/modules/test_lfind/sql/test_lfind.sql b/src/test/modules/test_lfind/sql/test_lfind.sql
index 766c640831f..7894fa61e78 100644
--- a/src/test/modules/test_lfind/sql/test_lfind.sql
+++ b/src/test/modules/test_lfind/sql/test_lfind.sql
@@ -7,4 +7,5 @@ CREATE EXTENSION test_lfind;
 --
 SELECT test_lfind8();
 SELECT test_lfind8_le();
+SELECT test_lfind8_nonzero();
 SELECT test_lfind32();
diff --git a/src/test/modules/test_lfind/test_lfind--1.0.sql b/src/test/modules/test_lfind/test_lfind--1.0.sql
index 81801926ae8..bb72cc6e56a 100644
--- a/src/test/modules/test_lfind/test_lfind--1.0.sql
+++ b/src/test/modules/test_lfind/test_lfind--1.0.sql
@@ -14,3 +14,7 @@ CREATE FUNCTION test_lfind8()
 CREATE FUNCTION test_lfind8_le()
 	RETURNS pg_catalog.void
 	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION test_lfind8_nonzero()
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/src/test/modules/test_lfind/test_lfind.c b/src/test/modules/test_lfind/test_lfind.c
index 8dcaa8f9fda..52ae6783ec9 100644
--- a/src/test/modules/test_lfind/test_lfind.c
+++ b/src/test/modules/test_lfind/test_lfind.c
@@ -115,6 +115,23 @@ test_lfind8_le(PG_FUNCTION_ARGS)
 	PG_RETURN_VOID();
 }
 
+PG_FUNCTION_INFO_V1(test_lfind8_nonzero);
+Datum
+test_lfind8_nonzero(PG_FUNCTION_ARGS)
+{
+	uint8		charbuf[64] = {0};
+
+	charbuf[32] = 1;
+
+	if (pg_lfind8_nonzero(charbuf, 32))
+		elog(ERROR, "pg_lfind8_nonzero() found nonexistent nonzero");
+
+	if (!pg_lfind8_nonzero(charbuf, 64))
+		elog(ERROR, "pg_lfind8_nonzero() did not find existing nonzero");
+
+	PG_RETURN_VOID();
+}
+
 PG_FUNCTION_INFO_V1(test_lfind32);
 Datum
 test_lfind32(PG_FUNCTION_ARGS)
-- 
2.52.0

v1-0002-Use-pg_lfind8_nonzero.patchapplication/octet-stream; charset=utf-8; name=v1-0002-Use-pg_lfind8_nonzero.patchDownload
From f465c2609f5be4500145293d42a5704fa6e61dec Mon Sep 17 00:00:00 2001
From: ChangAo Chen <cca5507@qq.com>
Date: Sun, 14 Dec 2025 18:43:40 +0800
Subject: [PATCH v1 2/3] Use pg_lfind8_nonzero().

---
 src/backend/access/common/heaptuple.c  | 23 +++++------------------
 src/backend/access/common/indextuple.c | 11 +++--------
 src/backend/utils/adt/expandedrecord.c | 11 +++--------
 3 files changed, 11 insertions(+), 34 deletions(-)

diff --git a/src/backend/access/common/heaptuple.c b/src/backend/access/common/heaptuple.c
index b7820d692e2..77d2a6f9751 100644
--- a/src/backend/access/common/heaptuple.c
+++ b/src/backend/access/common/heaptuple.c
@@ -61,6 +61,7 @@
 #include "access/sysattr.h"
 #include "access/tupdesc_details.h"
 #include "common/hashfn.h"
+#include "port/pg_lfind.h"
 #include "utils/datum.h"
 #include "utils/expandeddatum.h"
 #include "utils/hsearch.h"
@@ -1125,7 +1126,6 @@ heap_form_tuple(TupleDesc tupleDescriptor,
 	int			hoff;
 	bool		hasnull = false;
 	int			numberOfAttributes = tupleDescriptor->natts;
-	int			i;
 
 	if (numberOfAttributes > MaxTupleAttributeNumber)
 		ereport(ERROR,
@@ -1136,14 +1136,8 @@ heap_form_tuple(TupleDesc tupleDescriptor,
 	/*
 	 * Check for nulls
 	 */
-	for (i = 0; i < numberOfAttributes; i++)
-	{
-		if (isnull[i])
-		{
-			hasnull = true;
-			break;
-		}
-	}
+	if (likely(numberOfAttributes > 0))
+		hasnull = pg_lfind8_nonzero((uint8 *) isnull, numberOfAttributes);
 
 	/*
 	 * Determine total space needed
@@ -1462,7 +1456,6 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor,
 	int			hoff;
 	bool		hasnull = false;
 	int			numberOfAttributes = tupleDescriptor->natts;
-	int			i;
 
 	Assert(extra == MAXALIGN(extra));
 
@@ -1475,14 +1468,8 @@ heap_form_minimal_tuple(TupleDesc tupleDescriptor,
 	/*
 	 * Check for nulls
 	 */
-	for (i = 0; i < numberOfAttributes; i++)
-	{
-		if (isnull[i])
-		{
-			hasnull = true;
-			break;
-		}
-	}
+	if (likely(numberOfAttributes > 0))
+		hasnull = pg_lfind8_nonzero((uint8 *) isnull, numberOfAttributes);
 
 	/*
 	 * Determine total space needed
diff --git a/src/backend/access/common/indextuple.c b/src/backend/access/common/indextuple.c
index 3efa3889c6f..eac509fd580 100644
--- a/src/backend/access/common/indextuple.c
+++ b/src/backend/access/common/indextuple.c
@@ -21,6 +21,7 @@
 #include "access/htup_details.h"
 #include "access/itup.h"
 #include "access/toast_internals.h"
+#include "port/pg_lfind.h"
 
 /*
  * This enables de-toasting of index entries.  Needed until VACUUM is
@@ -139,14 +140,8 @@ index_form_tuple_context(TupleDesc tupleDescriptor,
 	}
 #endif
 
-	for (i = 0; i < numberOfAttributes; i++)
-	{
-		if (isnull[i])
-		{
-			hasnull = true;
-			break;
-		}
-	}
+	if (likely(numberOfAttributes > 0))
+		hasnull = pg_lfind8_nonzero((uint8 *) isnull, numberOfAttributes);
 
 	if (hasnull)
 		infomask |= INDEX_NULL_MASK;
diff --git a/src/backend/utils/adt/expandedrecord.c b/src/backend/utils/adt/expandedrecord.c
index 495d48fb581..13709e204a6 100644
--- a/src/backend/utils/adt/expandedrecord.c
+++ b/src/backend/utils/adt/expandedrecord.c
@@ -23,6 +23,7 @@
 #include "access/htup_details.h"
 #include "catalog/heap.h"
 #include "catalog/pg_type.h"
+#include "port/pg_lfind.h"
 #include "utils/builtins.h"
 #include "utils/datum.h"
 #include "utils/expandedrecord.h"
@@ -727,14 +728,8 @@ ER_get_flat_size(ExpandedObjectHeader *eohptr)
 
 	/* Test if we currently have any null values */
 	hasnull = false;
-	for (i = 0; i < erh->nfields; i++)
-	{
-		if (erh->dnulls[i])
-		{
-			hasnull = true;
-			break;
-		}
-	}
+	if (likely(erh->nfields > 0))
+		hasnull = pg_lfind8_nonzero((uint8 *) erh->dnulls, erh->nfields);
 
 	/* Determine total space needed */
 	len = offsetof(HeapTupleHeaderData, t_bits);
-- 
2.52.0

v1-0003-Test-pg_lfind8_nonzero.patch.tmpapplication/octet-stream; charset=utf-8; name=v1-0003-Test-pg_lfind8_nonzero.patch.tmpDownload
From 4da2c4c88cc3a42ea04cefd9ebf70bffcbfd04e1 Mon Sep 17 00:00:00 2001
From: ChangAo Chen <cca5507@qq.com>
Date: Sun, 14 Dec 2025 20:19:16 +0800
Subject: [PATCH v1 3/3] Test pg_lfind8_nonzero().

---
 contrib/test_patch/Makefile            | 19 +++++++
 contrib/test_patch/test_patch--1.0.sql |  9 ++++
 contrib/test_patch/test_patch.c        | 71 ++++++++++++++++++++++++++
 contrib/test_patch/test_patch.control  |  3 ++
 4 files changed, 102 insertions(+)
 create mode 100644 contrib/test_patch/Makefile
 create mode 100644 contrib/test_patch/test_patch--1.0.sql
 create mode 100644 contrib/test_patch/test_patch.c
 create mode 100644 contrib/test_patch/test_patch.control

diff --git a/contrib/test_patch/Makefile b/contrib/test_patch/Makefile
new file mode 100644
index 00000000000..310157fdf84
--- /dev/null
+++ b/contrib/test_patch/Makefile
@@ -0,0 +1,19 @@
+MODULE_big = test_patch
+OBJS = \
+	$(WIN32RES) \
+	test_patch.o
+PGFILEDESC = "test_patch"
+
+EXTENSION = test_patch
+DATA = test_patch--1.0.sql
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = contrib/test_patch
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+endif
diff --git a/contrib/test_patch/test_patch--1.0.sql b/contrib/test_patch/test_patch--1.0.sql
new file mode 100644
index 00000000000..0d062381a6e
--- /dev/null
+++ b/contrib/test_patch/test_patch--1.0.sql
@@ -0,0 +1,9 @@
+\echo Use "CREATE EXTENSION test_patch" to load this file. \quit
+
+CREATE FUNCTION test_head(natts int)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
+
+CREATE FUNCTION test_patch(natts int)
+	RETURNS pg_catalog.void
+	AS 'MODULE_PATHNAME' LANGUAGE C;
diff --git a/contrib/test_patch/test_patch.c b/contrib/test_patch/test_patch.c
new file mode 100644
index 00000000000..5f3eea4895c
--- /dev/null
+++ b/contrib/test_patch/test_patch.c
@@ -0,0 +1,71 @@
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "port/pg_lfind.h"
+#include "portability/instr_time.h"
+
+static bool isnull[2048];
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(test_head);
+Datum
+test_head(PG_FUNCTION_ARGS)
+{
+	int natts = PG_GETARG_INT32(0);
+    int loops = 1000;
+    bool hasnull = false;
+    int i;
+    instr_time start;
+    instr_time end;
+
+    INSTR_TIME_SET_CURRENT(start);
+
+    for (i = 0; i < loops; i++)
+    {
+        for (int j = 0; j < natts; j++)
+        {
+            if (isnull[j])
+            {
+                hasnull = true;
+                break;
+            }
+        }
+    }
+
+    INSTR_TIME_SET_CURRENT(end);
+
+    elog(INFO, "[HEAD] natts: %d, hasnull: %d, duration: %ld ns",
+         natts, hasnull,
+         INSTR_TIME_GET_NANOSEC(end) - INSTR_TIME_GET_NANOSEC(start));
+
+	PG_RETURN_VOID();
+}
+
+PG_FUNCTION_INFO_V1(test_patch);
+Datum
+test_patch(PG_FUNCTION_ARGS)
+{
+	int natts = PG_GETARG_INT32(0);
+    int loops = 1000;
+    bool hasnull = false;
+    int i;
+    instr_time start;
+    instr_time end;
+
+    INSTR_TIME_SET_CURRENT(start);
+
+    for (i = 0; i < loops; i++)
+    {
+        if (likely(natts > 0))
+            hasnull = pg_lfind8_nonzero((uint8 *) isnull, natts);
+    }
+
+    INSTR_TIME_SET_CURRENT(end);
+
+    elog(INFO, "[PATCH] natts: %d, hasnull: %d, duration: %ld ns",
+         natts, hasnull,
+         INSTR_TIME_GET_NANOSEC(end) - INSTR_TIME_GET_NANOSEC(start));
+
+	PG_RETURN_VOID();
+}
diff --git a/contrib/test_patch/test_patch.control b/contrib/test_patch/test_patch.control
new file mode 100644
index 00000000000..588092007ed
--- /dev/null
+++ b/contrib/test_patch/test_patch.control
@@ -0,0 +1,3 @@
+default_version = '1.0'
+module_pathname = '$libdir/test_patch'
+relocatable = true
-- 
2.52.0

#2Andres Freund
andres@anarazel.de
In reply to: cca5507 (#1)
Re: [PATCH] Add pg_lfind8_nonzero()

Hi,

On 2025-12-14 21:33:00 +0800, cca5507 wrote:

Hi hackers,

I'd like to add pg_lfind8_nonzero() to optimize some code like this:

```
for (i = 0; i < numberOfAttributes; i++)
{
if (isnull[i])
{
hasnull = true;
break;
}
}
```

Is code like this ever actually relevant for performance? In cases where the
compiler can't also optimize the code? Unless it is a legitimate bottleneck,
adding more complicated code to optimize the case doesn't make sense.

create extension test_patch;
# 4 is the natts
select test_head(4);
select test_patch(4);

Test result:
natts: 4 head: 1984ns patch: 2094ns
natts: 8 head: 3196ns patch: 641ns
natts: 16 head: 4589ns patch: 752ns
natts: 32 head: 8036ns patch: 1152ns
natts: 64 head: 19367ns patch: 2455ns
natts: 128 head: 33445ns patch: 4018ns

Looking forward to your comments!

The benchmark really should show performance benefits in at least somewhat
realistic cases (i.e. real users of the functions, even if the workload is a
bit more extreme than it'd be most of the time).

Greetings,

Andres

#3cca5507
cca5507@qq.com
In reply to: Andres Freund (#2)
Re: [PATCH] Add pg_lfind8_nonzero()

Hi,

Thank you for your reply!

Is code like this ever actually relevant for performance? In cases where the
compiler can't also optimize the code? Unless it is a legitimate bottleneck,
adding more complicated code to optimize the case doesn't make sense.

I just think the patch isn't that complicated.

The benchmark really should show performance benefits in at least somewhat
realistic cases (i.e. real users of the functions, even if the workload is a
bit more extreme than it'd be most of the time).

Yeah, this makes sense to me. Maybe I need to think more on it.

--
Regards,
ChangAo Chen

#4Peter Eisentraut
peter@eisentraut.org
In reply to: cca5507 (#1)
Re: [PATCH] Add pg_lfind8_nonzero()

On 14.12.25 14:33, cca5507 wrote:

With pg_lfind8_nonzero(), we can write the code like this:

```
if (likely(numberOfAttributes > 0))
hasnull = pg_lfind8_nonzero((uint8 *) isnull, numberOfAttributes);
```

The pg_lfind8_nonzero() is faster because we can handle 8 bool values at a time.

Maybe you could try popcount for this?

hasnull = (pg_popcount((char *) isnull, numberOfAttributes) > 0);

#5cca5507
cca5507@qq.com
In reply to: Peter Eisentraut (#4)
Re: [PATCH] Add pg_lfind8_nonzero()

Hi,

Maybe you could try popcount for this?

     hasnull = (pg_popcount((char *) isnull, numberOfAttributes) > 0);

It seems that pg_popcount() cannot early return when we find a nonzero value, so I don't
think it's a good choice for this case.

--
Regards,
ChangAo Chen