Re: CopyReadLineText optimization - Mailing list pgsql-patches

From Heikki Linnakangas
Subject Re: CopyReadLineText optimization
Date
Msg-id 47D55D03.7010108@enterprisedb.com
Whole thread Raw
In response to Re: CopyReadLineText optimization  (Andrew Dunstan <andrew@dunslane.net>)
Responses Re: CopyReadLineText optimization
List pgsql-patches
Andrew Dunstan wrote:
> Another question that occurred to me - did you try using strpbrk() to
> look for the next interesting character rather than your homegrown
> searcher gadget? If so, how did that perform?

It looks like strpbrk() performs poorly:

unpatched:
  testname |  min duration
----------+-----------------
  all      | 00:00:08.099656
  1/2      | 00:00:06.734241
  1/4      | 00:00:06.016946
  1/8      | 00:00:05.622122
  1/16     | 00:00:05.304252
  none     | 00:00:05.155755
(6 rows)

strpbrk:

  testname |  min duration
----------+-----------------
  all      | 00:00:22.980997
  1/2      | 00:00:13.724834
  1/4      | 00:00:08.980246
  1/8      | 00:00:06.582357
  1/16     | 00:00:05.291485
  none     | 00:00:06.239468
(6 rows)

memchr:

  testname |  min duration
----------+-----------------
  all      | 00:00:13.684479
  1/2      | 00:00:09.509213
  1/4      | 00:00:06.921959
  1/8      | 00:00:05.654761
  1/16     | 00:00:04.719027
  none     | 00:00:03.718361
(6 rows)

Attached is the test script and patches I used, if someone wants to test
this on another platform.

--
   Heikki Linnakangas
   EnterpriseDB   http://www.enterprisedb.com
Index: src/backend/commands/copy.c
===================================================================
RCS file: /home/hlinnaka/pgcvsrepository/pgsql/src/backend/commands/copy.c,v
retrieving revision 1.295
diff -c -r1.295 copy.c
*** src/backend/commands/copy.c    1 Jan 2008 19:45:48 -0000    1.295
--- src/backend/commands/copy.c    5 Mar 2008 14:44:58 -0000
***************
*** 67,72 ****
--- 67,275 ----
      EOL_CRNL
  } EolType;

+
+ /***** Fast byte searcher functions *****
+  *
+  * CopyReadLine needs to search for newlines, as well as quoting and escape
+  * characters to determine which newlines are real and which ones are escaped.
+  * Doing that in the naive way, looping through the input byte by byte and
+  * comparing against the interesting characters, can be slow.
+  *
+  * To speed that up, we provide a "searcher" interface, that can be used to
+  * search a piece of memory for up to 4 different bytes simultaneously. It's
+  * similar to memchr, but allows searching for multiple chars at the same
+  * time.
+  *
+  * To start a search on a new block of memory, call init_searcher. Then call
+  * next_searcher repeatedly to return all the occurrances of the bytes being
+  * searched for.
+  *
+  * The searcher implementation uses memchr to search for the bytes, and keeps
+  * track of where the next occurrance of each is. Using memchr allows us
+  * to take advantage of any platform-specific optimizations.
+  */
+
+ /*
+  * Struct used to store state of the current search between calls to
+  * next_searcher. Initialized by init_search.
+  */
+ typedef struct {
+     /* Each element in c corresponds the element in s. These are sorted
+      * by the pointers (ptr) */
+     int n;        /* elements used in the arrays */
+     char c[4];    /* bytes we're looking for */
+     char *ptr[4]; /* pointers to next occurances of the bytes */
+     char *end;    /* end of block being searched. Last byte to search is end-1 */
+ } searcher;
+
+ /* Functions internal to "searcher" */
+
+ #define swap_searcher_entries(searcher, a, b) do { \
+     char tmpc = (searcher)->c[a]; \
+     char *tmpptr = (searcher)->ptr[a]; \
+     (searcher)->c[a] = (searcher)->c[b]; \
+     (searcher)->ptr[a] = (searcher)->ptr[b]; \
+     (searcher)->c[b] = tmpc; \
+     (searcher)->ptr[b] = tmpptr; \
+ } while(0)
+
+ static void
+ sort_searcher(searcher *s)
+ {
+     switch(s->n)
+     {
+         case 0:
+         case 1:
+             /* Nothing to do */
+             break;
+         case 2:
+             if (s->ptr[0] > s->ptr[1])
+                 swap_searcher_entries(s, 0, 1);
+             break;
+         case 3:
+             if (s->ptr[0] > s->ptr[2])
+                 swap_searcher_entries(s, 0, 2);
+             if (s->ptr[0] > s->ptr[1])
+                 swap_searcher_entries(s, 0, 1);
+             if (s->ptr[1] > s->ptr[2])
+                 swap_searcher_entries(s, 1, 2);
+             break;
+         case 4:
+             if (s->ptr[0] > s->ptr[1])
+                 swap_searcher_entries(s, 0, 1);
+             if (s->ptr[2] > s->ptr[3])
+                 swap_searcher_entries(s, 2, 3);
+             if (s->ptr[1] > s->ptr[2])
+                 swap_searcher_entries(s, 2, 3);
+             if (s->ptr[0] > s->ptr[1])
+                 swap_searcher_entries(s, 0, 1);
+             if (s->ptr[2] > s->ptr[3])
+                 swap_searcher_entries(s, 2, 3);
+             break;
+     }
+ }
+
+ /* Remove the topmost element */
+ static void
+ remove_top(searcher *searcher)
+ {
+     int i;
+
+     searcher->n--;
+
+     /* Move up the remaining items */
+     for (i = 0; i < searcher->n; i++)
+     {
+         searcher->c[i] = searcher->c[i + 1];
+         searcher->ptr[i] = searcher->ptr[i + 1];
+     }
+ }
+
+ /*
+  * The first element in the list has been replaced by the caller.
+  * Move it to the right place in the list, to keep it sorted.
+  */
+ static void
+ sift_down(searcher *searcher)
+ {
+     if (searcher->n < 2 || searcher->ptr[0] <= searcher->ptr[1])
+         return;
+     swap_searcher_entries(searcher, 0, 1);
+
+     if (searcher->n < 3 || searcher->ptr[1] <= searcher->ptr[2])
+         return;
+     swap_searcher_entries(searcher, 1, 2);
+
+     if (searcher->n < 4 || searcher->ptr[2] <= searcher->ptr[3])
+         return;
+     swap_searcher_entries(searcher, 2, 3);
+ }
+
+ /*
+  * Starts a new search in a block of memory.
+  *
+  *  searcher    - for storing state between calls. Opaque to caller,
+  *                  init_searcher will initialize this.
+  *  str            - block of memory to search
+  *  len            - length of str
+  *  c            - bytes to search for, max 4.
+  *  n            - number of bytes in c
+  */
+ static void
+ init_searcher(searcher *searcher, char *str, int len, char c[4], int n)
+ {
+     int i;
+
+     Assert(n <= 4);
+
+     searcher->n = 0;
+     searcher->end = str + len;
+
+     /* Find first occurances of the searched-for bytes */
+     for (i = 0; i < n; i++)
+     {
+         char *hit = memchr(str, c[i], len);
+         if (hit != NULL)
+         {
+             searcher->c[searcher->n] = c[i];
+             searcher->ptr[searcher->n] = hit;
+             searcher->n++;
+         }
+         /*
+          * If the byte doesn't occur in the string at all, we don't
+          * need to put it in the list.
+          */
+     }
+     sort_searcher(searcher);
+ }
+
+ /*
+  * Look for the next interesting character
+  *
+  * searcher - state, opaque to caller
+  * str        - block to search.
+  *
+  * 'str' must be part of the block passed to init_searcher, and >=
+  * the return value of last call to next_searcher.
+  *
+  * Returns the offset of the next interesting location, relative to 'ss'
+  */
+ static char *
+ next_searcher(searcher *searcher, char *str)
+ {
+     Assert(str < searcher->end);
+
+     if (searcher->n == 0)
+         return NULL;
+
+     /*
+      * If the caller skipped over the next pointer we had memorized, we have to
+      * search for the one after that.
+      */
+     while (str > searcher->ptr[0])
+     {
+         char *s = memchr(str, searcher->c[0], searcher->end - str);
+
+         if (s == NULL)
+         {
+             remove_top(searcher);
+             if (searcher->n == 0)
+                 return NULL;
+         }
+         else
+         {
+             searcher->ptr[0] = s;
+             sift_down(searcher);
+         }
+     }
+
+     /*
+      * Return the next interesting location we had memorized.
+      */
+     return searcher->ptr[0];
+ }
+
+
  /*
   * This struct contains all the state variables used throughout a COPY
   * operation. For simplicity, we use the same struct for all variants of COPY,
***************
*** 159,164 ****
--- 362,369 ----
      char       *raw_buf;
      int            raw_buf_index;    /* next byte to process */
      int            raw_buf_len;    /* total # of bytes stored */
+
+     searcher    searcher;
  } CopyStateData;

  typedef CopyStateData *CopyState;
***************
*** 2285,2299 ****
--- 2490,2516 ----
                  last_was_esc = false;
      char        quotec = '\0';
      char        escapec = '\0';
+     char        search_chars[4];
+     int            nsearch_chars = 0;
+
+     search_chars[nsearch_chars++] = '\n';
+     search_chars[nsearch_chars++] = '\r';

      if (cstate->csv_mode)
      {
          quotec = cstate->quote[0];
          escapec = cstate->escape[0];
+
+         search_chars[nsearch_chars++] = quotec;
+
          /* ignore special escape processing if it's the same as quotec */
          if (quotec == escapec)
              escapec = '\0';
+         else
+             search_chars[nsearch_chars++] = escapec;
      }
+     else
+         search_chars[nsearch_chars++] = '\\';

      mblen_str[1] = '\0';

***************
*** 2360,2368 ****
--- 2577,2603 ----
                  break;
              }
              need_data = false;
+
+             if (!cstate->encoding_embeds_ascii)
+                 init_searcher(&cstate->searcher, copy_raw_buf, copy_buf_len,
+                               search_chars, nsearch_chars);
          }

          /* OK to fetch a character */
+         if ((!cstate->csv_mode || !first_char_in_line)
+             && !cstate->encoding_embeds_ascii
+             && (raw_buf_ptr + 4 <= copy_buf_len))
+         {
+             char *next = next_searcher(&cstate->searcher,
+                                        ©_raw_buf[raw_buf_ptr]);
+
+             if (next != NULL)
+             {
+                 raw_buf_ptr = next - copy_raw_buf;
+                 first_char_in_line = false;
+             }
+         }
+
          prev_raw_ptr = raw_buf_ptr;
          c = copy_raw_buf[raw_buf_ptr++];

Index: src/backend/commands/copy.c
===================================================================
RCS file: /home/hlinnaka/pgcvsrepository/pgsql/src/backend/commands/copy.c,v
retrieving revision 1.296
diff -c -r1.296 copy.c
*** src/backend/commands/copy.c    8 Mar 2008 01:16:26 -0000    1.296
--- src/backend/commands/copy.c    9 Mar 2008 09:34:13 -0000
***************
*** 2285,2299 ****
--- 2285,2311 ----
                  last_was_esc = false;
      char        quotec = '\0';
      char        escapec = '\0';
+     char        search_chars[5] = { 0, 0, 0, 0, 0 };
+     int            nsearch_chars = 0;
+
+     search_chars[nsearch_chars++] = '\n';
+     search_chars[nsearch_chars++] = '\r';

      if (cstate->csv_mode)
      {
          quotec = cstate->quote[0];
          escapec = cstate->escape[0];
+
+         search_chars[nsearch_chars++] = quotec;
+
          /* ignore special escape processing if it's the same as quotec */
          if (quotec == escapec)
              escapec = '\0';
+         else
+             search_chars[nsearch_chars++] = escapec;
      }
+     else
+         search_chars[nsearch_chars++] = '\\';

      mblen_str[1] = '\0';

***************
*** 2363,2368 ****
--- 2375,2394 ----
          }

          /* OK to fetch a character */
+         if ((!cstate->csv_mode || !first_char_in_line)
+             && !cstate->encoding_embeds_ascii
+             && (raw_buf_ptr + 4 <= copy_buf_len))
+         {
+
+             char *next = strpbrk(©_raw_buf[raw_buf_ptr], search_chars);
+
+             if (next != NULL)
+             {
+                 raw_buf_ptr = next - copy_raw_buf;
+                 first_char_in_line = false;
+             }
+         }
+
          prev_raw_ptr = raw_buf_ptr;
          c = copy_raw_buf[raw_buf_ptr++];


Attachment

pgsql-patches by date:

Previous
From: Teodor Sigaev
Date:
Subject: Re: [HACKERS] Include Lists for Text Search
Next
From: Andrew Dunstan
Date:
Subject: Re: CopyReadLineText optimization