v4-0001-Hash-lookup-const-arrays-in-OR-d-ScalarArrayOps.patch
application/octet-stream
Filename: v4-0001-Hash-lookup-const-arrays-in-OR-d-ScalarArrayOps.patch
Type: application/octet-stream
Part: 0
Patch
Format: format-patch
Series: patch v4-0001
Subject: Hash lookup const arrays in OR'd ScalarArrayOps
| File | + | − |
|---|---|---|
| src/backend/executor/execExpr.c | 74 | 10 |
| src/backend/executor/execExprInterp.c | 234 | 0 |
| src/include/executor/execExpr.h | 41 | 0 |
| src/test/regress/expected/expressions.out | 45 | 0 |
| src/test/regress/sql/expressions.sql | 12 | 0 |
From c68fe7488be6d93f29a2f2bdef91da7c8fd357ef Mon Sep 17 00:00:00 2001
From: jcoleman <jtc331@gmail.com>
Date: Fri, 10 Apr 2020 21:40:50 +0000
Subject: [PATCH v4] Hash lookup const arrays in OR'd ScalarArrayOps
Currently all scalar array op expressions execute as a linear search
through the array argument. However when OR semantics are desired it's
possible to instead use a hash lookup. Here we apply that optimization
to constant arrays (so we don't need to worry about teaching expression
execution when params change) of at least length 9 (since very short
arrays average to the same number of comparisons for linear searches and
thus avoid the preprocessing necessary to build the hash).
---
src/backend/executor/execExpr.c | 84 +++++++-
src/backend/executor/execExprInterp.c | 234 ++++++++++++++++++++++
src/include/executor/execExpr.h | 41 ++++
src/test/regress/expected/expressions.out | 45 +++++
src/test/regress/sql/expressions.sql | 12 ++
5 files changed, 406 insertions(+), 10 deletions(-)
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 2e463f5499..4b9714f071 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -31,6 +31,7 @@
#include "postgres.h"
#include "access/nbtree.h"
+#include "access/hash.h"
#include "catalog/objectaccess.h"
#include "catalog/pg_type.h"
#include "executor/execExpr.h"
@@ -50,6 +51,7 @@
#include "utils/lsyscache.h"
#include "utils/typcache.h"
+#define MIN_ARRAY_SIZE_FOR_SAOP_HASH 9
typedef struct LastAttnumInfo
{
@@ -944,11 +946,13 @@ ExecInitExprRec(Expr *node, ExprState *state,
case T_ScalarArrayOpExpr:
{
ScalarArrayOpExpr *opexpr = (ScalarArrayOpExpr *) node;
+ Oid func;
Expr *scalararg;
Expr *arrayarg;
FmgrInfo *finfo;
FunctionCallInfo fcinfo;
AclResult aclresult;
+ bool useHash = false;
Assert(list_length(opexpr->args) == 2);
scalararg = (Expr *) linitial(opexpr->args);
@@ -961,12 +965,62 @@ ExecInitExprRec(Expr *node, ExprState *state,
if (aclresult != ACLCHECK_OK)
aclcheck_error(aclresult, OBJECT_FUNCTION,
get_func_name(opexpr->opfuncid));
- InvokeFunctionExecuteHook(opexpr->opfuncid);
/* Set up the primary fmgr lookup information */
finfo = palloc0(sizeof(FmgrInfo));
fcinfo = palloc0(SizeForFunctionCallInfo(2));
- fmgr_info(opexpr->opfuncid, finfo);
+ func = opexpr->opfuncid;
+
+ /*
+ * If we have a constant array and want OR semantics, then we
+ * implement the op with a hash lookup instead of looping
+ * through the entire array for each execution.
+ */
+ if (opexpr->useOr && arrayarg && IsA(arrayarg, Const) &&
+ !((Const *) arrayarg)->constisnull)
+ {
+ Datum arrdatum = ((Const *) arrayarg)->constvalue;
+ ArrayType *arr = (ArrayType *) DatumGetPointer(arrdatum);
+ int nitems;
+
+ /*
+ * Only do the optimization if we have a large enough
+ * array to make it worth it.
+ */
+ nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr));
+ if (nitems >= MIN_ARRAY_SIZE_FOR_SAOP_HASH)
+ {
+ Oid hash_func;
+
+ /*
+ * Find the hash op that matches the originally planned
+ * equality op. If we don't have one, we'll just fall
+ * back to the default linear scan implementation.
+ */
+ useHash = get_op_hash_functions(opexpr->opno, NULL, &hash_func);
+
+ if (useHash)
+ {
+ FmgrInfo *hash_finfo;
+ FunctionCallInfo hash_fcinfo;
+
+ hash_finfo = palloc0(sizeof(FmgrInfo));
+ hash_fcinfo = palloc0(SizeForFunctionCallInfo(2));
+ fmgr_info(hash_func, hash_finfo);
+ fmgr_info_set_expr((Node *) node, hash_finfo);
+ InitFunctionCallInfoData(*hash_fcinfo, hash_finfo, 2,
+ opexpr->inputcollid, NULL, NULL);
+ InvokeFunctionExecuteHook(hash_func);
+
+ scratch.d.scalararrayhashedop.hash_finfo = hash_finfo;
+ scratch.d.scalararrayhashedop.hash_fcinfo_data = hash_fcinfo;
+ scratch.d.scalararrayhashedop.hash_fn_addr = hash_finfo->fn_addr;
+ }
+ }
+ }
+
+ InvokeFunctionExecuteHook(func);
+ fmgr_info(func, finfo);
fmgr_info_set_expr((Node *) node, finfo);
InitFunctionCallInfoData(*fcinfo, finfo, 2,
opexpr->inputcollid, NULL, NULL);
@@ -978,18 +1032,28 @@ ExecInitExprRec(Expr *node, ExprState *state,
/*
* Evaluate array argument into our return value. There's no
* danger in that, because the return value is guaranteed to
- * be overwritten by EEOP_SCALARARRAYOP, and will not be
- * passed to any other expression.
+ * be overwritten by EEOP_SCALARARRAYOP[_HASHED], and will
+ * not be passed to any other expression.
*/
ExecInitExprRec(arrayarg, state, resv, resnull);
/* And perform the operation */
- scratch.opcode = EEOP_SCALARARRAYOP;
- scratch.d.scalararrayop.element_type = InvalidOid;
- scratch.d.scalararrayop.useOr = opexpr->useOr;
- scratch.d.scalararrayop.finfo = finfo;
- scratch.d.scalararrayop.fcinfo_data = fcinfo;
- scratch.d.scalararrayop.fn_addr = finfo->fn_addr;
+ if (useHash)
+ {
+ scratch.opcode = EEOP_SCALARARRAYOP_HASHED;
+ scratch.d.scalararrayhashedop.finfo = finfo;
+ scratch.d.scalararrayhashedop.fcinfo_data = fcinfo;
+ scratch.d.scalararrayhashedop.fn_addr = finfo->fn_addr;
+ }
+ else
+ {
+ scratch.opcode = EEOP_SCALARARRAYOP;
+ scratch.d.scalararrayop.element_type = InvalidOid;
+ scratch.d.scalararrayop.useOr = opexpr->useOr;
+ scratch.d.scalararrayop.finfo = finfo;
+ scratch.d.scalararrayop.fcinfo_data = fcinfo;
+ scratch.d.scalararrayop.fn_addr = finfo->fn_addr;
+ }
ExprEvalPushStep(state, &scratch);
break;
}
diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 6308286d8c..d7968b299d 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -76,6 +76,7 @@
#include "utils/timestamp.h"
#include "utils/typcache.h"
#include "utils/xml.h"
+#include "lib/qunique.h"
/*
* Use computed-goto-based opcode dispatch when computed gotos are available.
@@ -178,6 +179,27 @@ static pg_attribute_always_inline void ExecAggPlainTransByRef(AggState *aggstate
ExprContext *aggcontext,
int setno);
+static bool
+saop_hash_element_match(struct saophash_hash *tb, Datum key1, Datum key2);
+static uint32 saop_element_hash(struct saophash_hash *tb, Datum key);
+
+/*
+ * Define parameters for ScalarArrayOpExpr hash table code generation. The interface is
+ * *also* declared in execnodes.h (to generate the types, which are externally
+ * visible).
+ */
+#define SH_PREFIX saophash
+#define SH_ELEMENT_TYPE ScalarArrayOpExprHashEntryData
+#define SH_KEY_TYPE Datum
+#define SH_KEY key
+#define SH_HASH_KEY(tb, key) saop_element_hash(tb, key)
+#define SH_EQUAL(tb, a, b) saop_hash_element_match(tb, a, b)
+#define SH_SCOPE extern
+#define SH_STORE_HASH
+#define SH_GET_HASH(tb, a) a->hash
+#define SH_DEFINE
+#include "lib/simplehash.h"
+
/*
* Prepare ExprState for interpreted execution.
*/
@@ -426,6 +448,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
&&CASE_EEOP_DOMAIN_CHECK,
&&CASE_EEOP_CONVERT_ROWTYPE,
&&CASE_EEOP_SCALARARRAYOP,
+ &&CASE_EEOP_SCALARARRAYOP_HASHED,
&&CASE_EEOP_XMLEXPR,
&&CASE_EEOP_AGGREF,
&&CASE_EEOP_GROUPING_FUNC,
@@ -1436,6 +1459,14 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
EEO_NEXT();
}
+ EEO_CASE(EEOP_SCALARARRAYOP_HASHED)
+ {
+ /* too complex for an inline implementation */
+ ExecEvalScalarArrayOpHashed(state, op, econtext);
+
+ EEO_NEXT();
+ }
+
EEO_CASE(EEOP_DOMAIN_NOTNULL)
{
/* too complex for an inline implementation */
@@ -3345,6 +3376,209 @@ ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op)
*op->resnull = resultnull;
}
+/*
+ * Hash function for scalar array hash op elements.
+ *
+ * We use the element type's default hash opclass, and the column collation
+ * if the type is collation-sensitive.
+ */
+static uint32
+saop_element_hash(struct saophash_hash *tb, Datum key)
+{
+ ScalarArrayOpExprHashTable elements_tab = (ScalarArrayOpExprHashTable) tb->private_data;
+ FunctionCallInfo fcinfo = elements_tab->op->d.scalararrayhashedop.hash_fcinfo_data;
+ Datum hash;
+
+ fcinfo->args[0].value = key;
+ fcinfo->args[0].isnull = false;
+
+ hash = elements_tab->op->d.scalararrayhashedop.hash_fn_addr(fcinfo);
+
+ return DatumGetUInt32(hash);
+}
+
+/*
+ * Matching function for scalar array hash op elements, to be used in hashtable
+ * lookups.
+ */
+static bool
+saop_hash_element_match(struct saophash_hash *tb, Datum key1, Datum key2)
+{
+ Datum result;
+
+ ScalarArrayOpExprHashTable elements_tab = (ScalarArrayOpExprHashTable) tb->private_data;
+ FunctionCallInfo fcinfo = elements_tab->op->d.scalararrayhashedop.fcinfo_data;
+
+ fcinfo->args[0].value = key1;
+ fcinfo->args[0].isnull = false;
+ fcinfo->args[1].value = key2;
+ fcinfo->args[1].isnull = false;
+
+ result = elements_tab->op->d.scalararrayhashedop.fn_addr(fcinfo);
+
+ return DatumGetBool(result);
+}
+
+/*
+ * Evaluate "scalar op ANY (const array)".
+ *
+ * This is an optimized version of ExecEvalScalarArrayOp that only supports
+ * ANY (i.e., OR semantics) because it performs a hashtable lookup to determine
+ * if the array contains the value.
+ *
+ * Source array is in our result area, scalar arg is already evaluated into
+ * fcinfo->args[0].
+ *
+ * The operator always yields boolean.
+ */
+void
+ExecEvalScalarArrayOpHashed(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
+{
+ ScalarArrayOpExprHashTable elements_tab = op->d.scalararrayhashedop.elements_tab;
+ FunctionCallInfo fcinfo = op->d.scalararrayhashedop.fcinfo_data;
+ bool strictfunc = op->d.scalararrayhashedop.finfo->fn_strict;
+ ArrayType *arr;
+ Datum scalar = fcinfo->args[0].value;
+ bool scalar_isnull = fcinfo->args[0].isnull;
+ Datum result;
+ bool resultnull;
+ bool hashfound;
+
+ /* We don't setup a hashed scalar array op if the array const is null. */
+ Assert(!*op->resnull);
+
+ /*
+ * If the scalar is NULL, and the function is strict, return NULL; no
+ * point in executing the search.
+ */
+ if (fcinfo->args[0].isnull && strictfunc)
+ {
+ *op->resnull = true;
+ return;
+ }
+
+ /* Preprocess the array the first time we execute the op. */
+ if (elements_tab == NULL)
+ {
+ int16 typlen;
+ bool typbyval;
+ char typalign;
+ int nitems;
+ int num_nulls = 0;
+ char *s;
+ bits8 *bitmap;
+ int bitmask;
+ MemoryContext oldcontext;
+
+ arr = DatumGetArrayTypeP(*op->resvalue);
+ nitems = ArrayGetNItems(ARR_NDIM(arr), ARR_DIMS(arr));
+
+ get_typlenbyvalalign(ARR_ELEMTYPE(arr),
+ &typlen,
+ &typbyval,
+ &typalign);
+
+ oldcontext = MemoryContextSwitchTo(econtext->ecxt_per_query_memory);
+
+ elements_tab = (ScalarArrayOpExprHashTable) palloc(sizeof(ScalarArrayOpExprHashTableData));
+ op->d.scalararrayhashedop.elements_tab = elements_tab;
+ elements_tab->op = op;
+ elements_tab->hashtab = saophash_create(CurrentMemoryContext, nitems, elements_tab);
+
+ MemoryContextSwitchTo(oldcontext);
+
+ s = (char *) ARR_DATA_PTR(arr);
+ bitmap = ARR_NULLBITMAP(arr);
+ bitmask = 1;
+ for (int i = 0; i < nitems; i++)
+ {
+ Datum element;
+
+ /* Get array element, checking for NULL. */
+ if (bitmap && (*bitmap & bitmask) == 0)
+ {
+ num_nulls++;
+ }
+ else
+ {
+ element = fetch_att(s, typbyval, typlen);
+ s = att_addlength_pointer(s, typlen, s);
+ s = (char *) att_align_nominal(s, typalign);
+
+ saophash_insert(elements_tab->hashtab, element, &hashfound);
+ }
+
+ /* Advance bitmap pointer if any. */
+ if (bitmap)
+ {
+ bitmask <<= 1;
+ if (bitmask == 0x100)
+ {
+ bitmap++;
+ bitmask = 1;
+ }
+ }
+ }
+
+ /*
+ * Remember if we had any nulls so that we know if we need to execute
+ * non-strict functions with a null lhs value if no match is found.
+ */
+ op->d.scalararrayhashedop.has_nulls = num_nulls > 0;
+
+ /*
+ * We only setup a binary search op if we have > 8 elements, so we don't
+ * need to worry about adding an optimization for the empty array case.
+ */
+ Assert(nitems > 0);
+ }
+
+ /* Check the hash to see if we have a match. */
+ hashfound = NULL != saophash_lookup(elements_tab->hashtab, scalar);
+
+ result = BoolGetDatum(hashfound);
+ resultnull = false;
+
+ /*
+ * If we didn't find a match in the array, we still might need to handle
+ * the possibility of null values (we've previously removed them from the
+ * array).
+ */
+ if (!DatumGetBool(result) && op->d.scalararrayhashedop.has_nulls)
+ {
+ if (strictfunc)
+ {
+ /*
+ * Had nulls and is a strict function, so instead of executing the
+ * function onces with a null rhs, we can assume null.
+ */
+ result = (Datum) 0;
+ resultnull = true;
+ }
+ else
+ {
+ /* TODO: No regression test for this branch. */
+ /*
+ * Execute function will null rhs just once.
+ *
+ * The hash lookup path will have scribbled on the lhs argument so
+ * we need to set it up also (even though we entered this function
+ * with it already set).
+ */
+ fcinfo->args[0].value = scalar;
+ fcinfo->args[0].isnull = scalar_isnull;
+ fcinfo->args[1].value = (Datum) 0;
+ fcinfo->args[1].isnull = true;
+
+ result = op->d.scalararrayhashedop.fn_addr(fcinfo);
+ resultnull = fcinfo->isnull;
+ }
+ }
+
+ *op->resvalue = result;
+ *op->resnull = resultnull;
+}
+
/*
* Evaluate a NOT NULL domain constraint.
*/
diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h
index 1b7f9865b0..dc298e3597 100644
--- a/src/include/executor/execExpr.h
+++ b/src/include/executor/execExpr.h
@@ -21,6 +21,30 @@
struct ExprEvalStep;
struct SubscriptingRefState;
+typedef struct ScalarArrayOpExprHashEntryData *ScalarArrayOpExprHashEntry;
+typedef struct ScalarArrayOpExprHashTableData *ScalarArrayOpExprHashTable;
+
+typedef struct ScalarArrayOpExprHashEntryData
+{
+ Datum key;
+ uint32 status; /* hash status */
+ uint32 hash; /* hash value (cached) */
+} ScalarArrayOpExprHashEntryData;
+
+/* define parameters necessary to generate the tuple hash table interface */
+#define SH_PREFIX saophash
+#define SH_ELEMENT_TYPE ScalarArrayOpExprHashEntryData
+#define SH_KEY_TYPE Datum
+#define SH_SCOPE extern
+#define SH_DECLARE
+#include "lib/simplehash.h"
+
+typedef struct ScalarArrayOpExprHashTableData
+{
+ saophash_hash *hashtab; /* underlying hash table */
+ struct ExprEvalStep *op;
+} ScalarArrayOpExprHashTableData;
+
/* Bits in ExprState->flags (see also execnodes.h for public flag bits): */
/* expression's interpreter has been initialized */
#define EEO_FLAG_INTERPRETER_INITIALIZED (1 << 1)
@@ -218,6 +242,7 @@ typedef enum ExprEvalOp
/* evaluate assorted special-purpose expression types */
EEOP_CONVERT_ROWTYPE,
EEOP_SCALARARRAYOP,
+ EEOP_SCALARARRAYOP_HASHED,
EEOP_XMLEXPR,
EEOP_AGGREF,
EEOP_GROUPING_FUNC,
@@ -554,6 +579,21 @@ typedef struct ExprEvalStep
PGFunction fn_addr; /* actual call address */
} scalararrayop;
+ /* for EEOP_SCALARARRAYOP_HASHED */
+ struct
+ {
+ bool has_nulls;
+ ScalarArrayOpExprHashTable elements_tab;
+ FmgrInfo *finfo; /* function's lookup data */
+ FunctionCallInfo fcinfo_data; /* arguments etc */
+ /* faster to access without additional indirection: */
+ PGFunction fn_addr; /* actual call address */
+ FmgrInfo *hash_finfo; /* function's lookup data */
+ FunctionCallInfo hash_fcinfo_data; /* arguments etc */
+ /* faster to access without additional indirection: */
+ PGFunction hash_fn_addr; /* actual call address */
+ } scalararrayhashedop;
+
/* for EEOP_XMLEXPR */
struct
{
@@ -725,6 +765,7 @@ extern void ExecEvalFieldStoreForm(ExprState *state, ExprEvalStep *op,
extern void ExecEvalConvertRowtype(ExprState *state, ExprEvalStep *op,
ExprContext *econtext);
extern void ExecEvalScalarArrayOp(ExprState *state, ExprEvalStep *op);
+extern void ExecEvalScalarArrayOpHashed(ExprState *state, ExprEvalStep *op, ExprContext *econtext);
extern void ExecEvalConstraintNotNull(ExprState *state, ExprEvalStep *op);
extern void ExecEvalConstraintCheck(ExprState *state, ExprEvalStep *op);
extern void ExecEvalXmlExpr(ExprState *state, ExprEvalStep *op);
diff --git a/src/test/regress/expected/expressions.out b/src/test/regress/expected/expressions.out
index 05a6eb07b2..42e1c0d1f2 100644
--- a/src/test/regress/expected/expressions.out
+++ b/src/test/regress/expected/expressions.out
@@ -158,3 +158,48 @@ select count(*) from date_tbl
13
(1 row)
+--
+-- Tests for ScalarArrayOpExpr hash optimization
+--
+select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1);
+ ?column?
+----------
+ t
+(1 row)
+
+select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, null);
+ ?column?
+----------
+
+(1 row)
+
+select 1 in (null, null, null, null, null, null, null, null, null, null, null);
+ ?column?
+----------
+
+(1 row)
+
+select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1, null);
+ ?column?
+----------
+ t
+(1 row)
+
+select null::int in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1);
+ ?column?
+----------
+
+(1 row)
+
+select null::int in (10, 9, 2, 8, 3, 7, 4, 6, 5, null);
+ ?column?
+----------
+
+(1 row)
+
+select 'a' in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j');
+ ?column?
+----------
+ t
+(1 row)
+
diff --git a/src/test/regress/sql/expressions.sql b/src/test/regress/sql/expressions.sql
index 1ca8bb151c..ef2738067b 100644
--- a/src/test/regress/sql/expressions.sql
+++ b/src/test/regress/sql/expressions.sql
@@ -65,3 +65,15 @@ select count(*) from date_tbl
where f1 not between symmetric '1997-01-01' and '1998-01-01';
select count(*) from date_tbl
where f1 not between symmetric '1997-01-01' and '1998-01-01';
+
+--
+-- Tests for ScalarArrayOpExpr hash optimization
+--
+
+select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1);
+select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, null);
+select 1 in (null, null, null, null, null, null, null, null, null, null, null);
+select 1 in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1, null);
+select null::int in (10, 9, 2, 8, 3, 7, 4, 6, 5, 1);
+select null::int in (10, 9, 2, 8, 3, 7, 4, 6, 5, null);
+select 'a' in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j');
--
2.20.1