Optimise numeric division for 3 and 4 base-NBASE digit divisors.
authorDean Rasheed <dean.a.rasheed@gmail.com>
Mon, 23 Jan 2023 11:56:00 +0000 (11:56 +0000)
committerDean Rasheed <dean.a.rasheed@gmail.com>
Mon, 23 Jan 2023 11:58:28 +0000 (11:58 +0000)
On platforms with 128-bit integer support, introduce a new function
div_var_int64(), along the same lines as div_var_int() added in
d1b307eef2 for divisors with 1 or 2 base-NBASE digits, and use it to
speed up div_var() and div_var_fast() in a similar way when the
divisor has 3 or 4 base-NBASE digits.

This gives significant performance gains for divisors with 9-16
decimal digits.

Joel Jacobson.

Discussion:
  https://postgr.es/m/b7a5893d-af18-4c0b-8918-96de5f1bbf39%40app.fastmail.com
  https://postgr.es/m/CAEZATCXGm%3DDyTq%3DFrcOqC0gPMVveKUYTaD5KRRoajrUTiWxVMw%40mail.gmail.com

src/backend/utils/adt/numeric.c

index a6409ecbee8e7f0d96887e4132e600d88108d4b2..67edb70ab820f6beabb0237c6f4f5be7dfc581af 100644 (file)
@@ -554,6 +554,10 @@ static void div_var_fast(const NumericVar *var1, const NumericVar *var2,
                                                 NumericVar *result, int rscale, bool round);
 static void div_var_int(const NumericVar *var, int ival, int ival_weight,
                                                NumericVar *result, int rscale, bool round);
+#ifdef HAVE_INT128
+static void div_var_int64(const NumericVar *var, int64 ival, int ival_weight,
+                                                 NumericVar *result, int rscale, bool round);
+#endif
 static int     select_div_scale(const NumericVar *var1, const NumericVar *var2);
 static void mod_var(const NumericVar *var1, const NumericVar *var2,
                                        NumericVar *result);
@@ -8484,6 +8488,9 @@ div_var(const NumericVar *var1, const NumericVar *var2, NumericVar *result,
        /*
         * If the divisor has just one or two digits, delegate to div_var_int(),
         * which uses fast short division.
+        *
+        * Similarly, on platforms with 128-bit integer support, delegate to
+        * div_var_int64() for divisors with three or four digits.
         */
        if (var2ndigits <= 2)
        {
@@ -8503,6 +8510,26 @@ div_var(const NumericVar *var1, const NumericVar *var2, NumericVar *result,
                div_var_int(var1, idivisor, idivisor_weight, result, rscale, round);
                return;
        }
+#ifdef HAVE_INT128
+       if (var2ndigits <= 4)
+       {
+               int64           idivisor;
+               int                     idivisor_weight;
+
+               idivisor = var2->digits[0];
+               idivisor_weight = var2->weight;
+               for (i = 1; i < var2ndigits; i++)
+               {
+                       idivisor = idivisor * NBASE + var2->digits[i];
+                       idivisor_weight--;
+               }
+               if (var2->sign == NUMERIC_NEG)
+                       idivisor = -idivisor;
+
+               div_var_int64(var1, idivisor, idivisor_weight, result, rscale, round);
+               return;
+       }
+#endif
 
        /*
         * Otherwise, perform full long division.
@@ -8774,6 +8801,9 @@ div_var_fast(const NumericVar *var1, const NumericVar *var2,
        /*
         * If the divisor has just one or two digits, delegate to div_var_int(),
         * which uses fast short division.
+        *
+        * Similarly, on platforms with 128-bit integer support, delegate to
+        * div_var_int64() for divisors with three or four digits.
         */
        if (var2ndigits <= 2)
        {
@@ -8793,6 +8823,26 @@ div_var_fast(const NumericVar *var1, const NumericVar *var2,
                div_var_int(var1, idivisor, idivisor_weight, result, rscale, round);
                return;
        }
+#ifdef HAVE_INT128
+       if (var2ndigits <= 4)
+       {
+               int64           idivisor;
+               int                     idivisor_weight;
+
+               idivisor = var2->digits[0];
+               idivisor_weight = var2->weight;
+               for (i = 1; i < var2ndigits; i++)
+               {
+                       idivisor = idivisor * NBASE + var2->digits[i];
+                       idivisor_weight--;
+               }
+               if (var2->sign == NUMERIC_NEG)
+                       idivisor = -idivisor;
+
+               div_var_int64(var1, idivisor, idivisor_weight, result, rscale, round);
+               return;
+       }
+#endif
 
        /*
         * Otherwise, perform full long division.
@@ -9182,6 +9232,123 @@ div_var_int(const NumericVar *var, int ival, int ival_weight,
 }
 
 
+#ifdef HAVE_INT128
+/*
+ * div_var_int64() -
+ *
+ *     Divide a numeric variable by a 64-bit integer with the specified weight.
+ *     The quotient var / (ival * NBASE^ival_weight) is stored in result.
+ *
+ *     This duplicates the logic in div_var_int(), so any changes made there
+ *     should be made here too.
+ */
+static void
+div_var_int64(const NumericVar *var, int64 ival, int ival_weight,
+                         NumericVar *result, int rscale, bool round)
+{
+       NumericDigit *var_digits = var->digits;
+       int                     var_ndigits = var->ndigits;
+       int                     res_sign;
+       int                     res_weight;
+       int                     res_ndigits;
+       NumericDigit *res_buf;
+       NumericDigit *res_digits;
+       uint64          divisor;
+       int                     i;
+
+       /* Guard against division by zero */
+       if (ival == 0)
+               ereport(ERROR,
+                               errcode(ERRCODE_DIVISION_BY_ZERO),
+                               errmsg("division by zero"));
+
+       /* Result zero check */
+       if (var_ndigits == 0)
+       {
+               zero_var(result);
+               result->dscale = rscale;
+               return;
+       }
+
+       /*
+        * Determine the result sign, weight and number of digits to calculate.
+        * The weight figured here is correct if the emitted quotient has no
+        * leading zero digits; otherwise strip_var() will fix things up.
+        */
+       if (var->sign == NUMERIC_POS)
+               res_sign = ival > 0 ? NUMERIC_POS : NUMERIC_NEG;
+       else
+               res_sign = ival > 0 ? NUMERIC_NEG : NUMERIC_POS;
+       res_weight = var->weight - ival_weight;
+       /* The number of accurate result digits we need to produce: */
+       res_ndigits = res_weight + 1 + (rscale + DEC_DIGITS - 1) / DEC_DIGITS;
+       /* ... but always at least 1 */
+       res_ndigits = Max(res_ndigits, 1);
+       /* If rounding needed, figure one more digit to ensure correct result */
+       if (round)
+               res_ndigits++;
+
+       res_buf = digitbuf_alloc(res_ndigits + 1);
+       res_buf[0] = 0;                         /* spare digit for later rounding */
+       res_digits = res_buf + 1;
+
+       /*
+        * Now compute the quotient digits.  This is the short division algorithm
+        * described in Knuth volume 2, section 4.3.1 exercise 16, except that we
+        * allow the divisor to exceed the internal base.
+        *
+        * In this algorithm, the carry from one digit to the next is at most
+        * divisor - 1.  Therefore, while processing the next digit, carry may
+        * become as large as divisor * NBASE - 1, and so it requires a 128-bit
+        * integer if this exceeds PG_UINT64_MAX.
+        */
+       divisor = i64abs(ival);
+
+       if (divisor <= PG_UINT64_MAX / NBASE)
+       {
+               /* carry cannot overflow 64 bits */
+               uint64          carry = 0;
+
+               for (i = 0; i < res_ndigits; i++)
+               {
+                       carry = carry * NBASE + (i < var_ndigits ? var_digits[i] : 0);
+                       res_digits[i] = (NumericDigit) (carry / divisor);
+                       carry = carry % divisor;
+               }
+       }
+       else
+       {
+               /* carry may exceed 64 bits */
+               uint128         carry = 0;
+
+               for (i = 0; i < res_ndigits; i++)
+               {
+                       carry = carry * NBASE + (i < var_ndigits ? var_digits[i] : 0);
+                       res_digits[i] = (NumericDigit) (carry / divisor);
+                       carry = carry % divisor;
+               }
+       }
+
+       /* Store the quotient in result */
+       digitbuf_free(result->buf);
+       result->ndigits = res_ndigits;
+       result->buf = res_buf;
+       result->digits = res_digits;
+       result->weight = res_weight;
+       result->sign = res_sign;
+
+       /* Round or truncate to target rscale (and set result->dscale) */
+       if (round)
+               round_var(result, rscale);
+       else
+               trunc_var(result, rscale);
+
+       /* Strip leading/trailing zeroes */
+       strip_var(result);
+}
+#endif
+
+
 /*
  * Default scale selection for division
  *