Subject: Re: Speed up COPY FROM text/CSV parsing using SIMD

From: Neil Conway <[email-masked]>
Date: Wed, 21 Jan 2026 15:49:59 -0500

A few suggestions:

* I'm curious if we'll see better performance on large inputs if we flush
to `line_buf` periodically (e.g., at least every few thousand bytes or so).
Otherwise we might see poor data cache behavior if large inputs with no
control characters get evicted before we've copied them over. See the
approach taken in escape_json_with_len() in utils/adt/json.c

* Did you compare the approach taken in the patch with a simpler approach
that just does

if (!(vector8_has(chunk, '\\') ||
      vector8_has(chunk, '\r') ||
      vector8_has(chunk, '\n') /* and so on, accounting for CSV / escapec /
quotec stuff */))
{
    /* skip chunk */
}

That's roughly what we do elsewhere (e.g., escape_json_with_len). It has
the advantage of being more readable, along with potentially having fewer
data dependencies.

Neil

On Wed, Dec 10, 2025 at 7:00 AM Nazir Bilal Yavuz <[email-masked]>
wrote:

> Hi,
>
> On Wed, 10 Dec 2025 at 01:13, Manni Wood <[email-masked]>
> wrote:
> >
> > Bilal Yavuz (Nazir Bilal Yavuz?),
>
> It is Nazir Bilal Yavuz, I changed some settings on my phone and it
> seems that it affected my mail account, hopefully it should be fixed
> now.
>
> > I did not get a chance to do any work on this today, but wanted to thank
> you for finding my logic errors in counting special chars for CSV, and
> hacking on my naive solution to make it faster. By attempting Andrew
> Dunstan's suggestion, I got a better feel for the reality that the
> "housekeeping" code produces a significant amount of overhead.
>
> You are welcome! v4.1 has some problems with in_quote case in SIMD
> handling code and counting cstate->chars_processed variable. I fixed
> them in v4.2.
>
> --
> Regards,
> Nazir Bilal Yavuz
> Microsoft
>

---

From: KAZAR Ayoub <[email-masked]>
Date: Thu, 22 Jan 2026 20:32:43 +0100

> approach taken in escape_json_with_len() in utils/adt/json.c
Hello Neil, thanks for the suggestions!
This might be true as the json code considers it, i'll take a look later.

>       vector8_has(chunk, '\n') /* and so on, accounting for CSV / escapec
> / quotec stuff */))
> {
>     /* skip chunk */
> }
>
This is true and also similar to the concerns of the very first patches
where i tried to just replace the special chars detection by something less
dependent but I missed vector8_has.
I have tried this now and here are my thoughts and results:

v5: the simplest of all, we take a chunk, detect any special character, if
it's clean then skip it all, otherwise fallback to scalar WITHOUT advancing
to the first special char found, this is mentioned just for reference and
comparison.
Numbers compared to master:
text, no special: +16.8%
text, 1/3 special: -21.3%
csv, no special: +29.5%
csv, 1/3 special: -33.5%

v5.1: Take a chunk, if its clean of specials, skip it, if not process the
whole chunk byte-by-byte, retry the same thing for next chunk if it's fully
skippable and so on...
Numbers:
text, no special: +16.5%
text, 1/3 special: -20.1% (weird) ; but overall the idea is clear maybe.
csv, no special: +29.4%
csv, 1/3 special: -4.4%

For files of 1/3 specials it's almost always terrible to consider simd
because it will always fall back to scalar without even advancing to the
first special character, even for something less than 1/3 i think it would
be same case, i attached an extremely simple analysis on probability of
having a 16 byte clean chunk depending on data we have.

Attached also is an overall comparison graph between master, v3, v5.1 (with
patch)

Regards,
Ayoub
From 40cd491a8dcc2b90615ba76f7c4b16e4491fcd04 Mon Sep 17 00:00:00 2001
From: AyoubKAZ <[email-masked]>
Date: Thu, 22 Jan 2026 20:02:51 +0100
Subject: [PATCH] Simple heuristic for SIMD COPY FROM

---
 src/backend/commands/copyfromparse.c | 32 ++++++++++++++++++++++++++++
 1 file changed, 32 insertions(+)

diff --git a/src/backend/commands/copyfromparse.c b/src/backend/commands/copyfromparse.c
index 5868a7fa11f..d138dd3731c 100644
--- a/src/backend/commands/copyfromparse.c
+++ b/src/backend/commands/copyfromparse.c
@@ -71,7 +71,9 @@
 #include "mb/pg_wchar.h"
 #include "miscadmin.h"
 #include "pgstat.h"
+#include "port/pg_bitutils.h"
 #include "port/pg_bswap.h"
+#include "port/simd.h"
 #include "utils/builtins.h"
 #include "utils/rel.h"
 
@@ -1248,6 +1250,7 @@ CopyReadLineText(CopyFromState cstate, bool is_csv)
 	bool		need_data = false;
 	bool		hit_eof = false;
 	bool		result = false;
+	int 		simd_boundary;
 
 	/* CSV variables */
 	bool		in_quote = false,
@@ -1292,6 +1295,7 @@ CopyReadLineText(CopyFromState cstate, bool is_csv)
 	copy_input_buf = cstate->input_buf;
 	input_buf_ptr = cstate->input_buf_index;
 	copy_buf_len = cstate->input_buf_len;
+	simd_boundary = input_buf_ptr;
 
 	for (;;)
 	{
@@ -1315,6 +1319,7 @@ CopyReadLineText(CopyFromState cstate, bool is_csv)
 			hit_eof = cstate->input_reached_eof;
 			input_buf_ptr = cstate->input_buf_index;
 			copy_buf_len = cstate->input_buf_len;
+			simd_boundary = input_buf_ptr;
 
 			/*
 			 * If we are completely out of data, break out of the loop,
@@ -1328,6 +1333,33 @@ CopyReadLineText(CopyFromState cstate, bool is_csv)
 			need_data = false;
 		}
 
+
+#ifndef USE_NO_SIMD
+
+		if (input_buf_ptr >= simd_boundary && !last_was_esc && (copy_buf_len - input_buf_ptr) >= sizeof(Vector8))
+		{
+			Vector8 chunk;
+
+			vector8_load(&chunk, (const uint8 *) &copy_input_buf[input_buf_ptr]);
+
+			if (!(vector8_has(chunk, '\\') ||
+				  vector8_has(chunk, '\r') ||
+				  vector8_has(chunk, '\n') ||
+				  (is_csv && vector8_has(chunk, quotec)) ||
+				  (is_csv && escapec != '\0' && vector8_has(chunk, escapec))))
+			{
+				input_buf_ptr += sizeof(Vector8);
+				simd_boundary = input_buf_ptr;
+				continue;
+			}
+			else
+			{
+
+				simd_boundary = input_buf_ptr + sizeof(Vector8);
+			}
+		}
+#endif
+
 		/* OK to fetch a character */
 		prev_raw_ptr = input_buf_ptr;
 		c = copy_input_buf[input_buf_ptr++];
-- 
2.34.1

---

From: KAZAR Ayoub <[email-masked]>
Date: Sat, 31 Jan 2026 17:20:58 +0100

Hello,

On Wed, Jan 21, 2026 at 9:50 PM Neil Conway <[email-masked]> wrote:

> So i gave this a try, attached is the small patch that has v3 + the
suggestion added, here are the results with different threshold for
line_buf refill:

Execution time compared to master:
Workload v3 v3.1 (2k) v3.1 (4k) v3.1 (8k) v3.1 (16k) v3.1 (20k) v3.1 (28k)
text/none -16.5% -17.4% -14.3% -12.6% -13.6% -10.5% -16.3%
text/esc +5.6% +11.1% +3.1% +7.6% +3.0% +4.9% +4.2%
csv/none -31.0% -29.9% -26.7% -30.1% -27.9% -30.2% -29.6%
csv/quote +0.2% -0.6% -0.4% -1.0% +0.1% +2.5% -1.0%

L1d cache miss rates:
Workload Master v3 v3.1 (2k) v3.1 (4k) v3.1 (8k) v3.1 (16k) v3.1 (20k) v3.1
(28k)
text/none 0.20% 0.23% 0.21% 0.22% 0.21% 0.21% 0.21% 0.22%
text/esc 0.21% 0.22% 0.22% 0.22% 0.22% 0.21% 0.22% 0.22%
csv/none 0.17% 0.22% 0.21% 0.22% 0.21% 0.21% 0.22% 0.22%
csv/quote 0.18% 0.22% 0.19% 0.20% 0.20% 0.19% 0.20% 0.20%
On my laptop I have 32KB L1 cache per core.
Results are super close, it is hard to see in the cache misses numbers but
execution times are saying other things, doing the periodic filling of
line_buf seems good to do.
If Manni can rerun the benchmarks on these too, it would be nice to confirm
this.

Regards,
Ayoub

---

From: Manni Wood <[email-masked]>
Date: Mon, 2 Feb 2026 08:17:55 -0600

On Sat, Jan 31, 2026 at 10:21 AM KAZAR Ayoub <[email-masked]> wrote:

> If Manni can rerun the benchmarks on these too, it would be nice to
> confirm this.
>
>
> Regards,
> Ayoub
>

Hello, All!

Ayoub, I will try to benchmark v3.1 this week on my standalone x86 and arm
PCs. Sadly, other work has been taking priority these last couple weeks,
but I will carve out some time.

Neil, thanks so much for looking at this patch!

-Manni
-- 
-- Manni Wood EDB: https://www.enterprisedb.com