You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ah...@apache.org on 2023/12/23 12:19:26 UTC

(commons-statistics) 03/06: STATISTICS-81: Add integer mean and variance implementation

This is an automated email from the ASF dual-hosted git repository.

aherbert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-statistics.git

commit 3fda89532f8d49953ce41f9d79155ebbcfdd68a7
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Wed Dec 20 23:14:36 2023 +0000

    STATISTICS-81: Add integer mean and variance implementation
    
    Requires specialised aggregator classes for exact integer arithmetic.
    
    Add initial benchmark of integer based statistics.
---
 .github/workflows/maven.yml                        |   14 +-
 .../commons/statistics/descriptive/Int128.java     |  237 +++
 .../commons/statistics/descriptive/IntMath.java    |  391 +++++
 .../commons/statistics/descriptive/IntMean.java    |  144 ++
 .../statistics/descriptive/IntVariance.java        |  248 +++
 .../commons/statistics/descriptive/LongMean.java   |  141 ++
 .../statistics/descriptive/LongVariance.java       |  226 +++
 .../commons/statistics/descriptive/UInt128.java    |  238 +++
 .../commons/statistics/descriptive/UInt192.java    |  247 +++
 .../commons/statistics/descriptive/UInt96.java     |  155 ++
 .../descriptive/BaseIntStatisticTest.java          |   20 +-
 .../descriptive/BaseLongStatisticTest.java         |   50 +-
 .../statistics/descriptive/BaseStatisticTest.java  |   32 +-
 .../commons/statistics/descriptive/Int128Test.java |  173 ++
 .../statistics/descriptive/IntMathTest.java        |  206 +++
 .../statistics/descriptive/IntMeanTest.java        |  116 ++
 .../statistics/descriptive/IntVarianceTest.java    |  191 +++
 .../statistics/descriptive/LongMeanTest.java       |  124 ++
 .../statistics/descriptive/LongVarianceTest.java   |  203 +++
 .../commons/statistics/descriptive/TestHelper.java |  128 ++
 .../statistics/descriptive/UInt128Test.java        |  239 +++
 .../statistics/descriptive/UInt192Test.java        |  208 +++
 .../commons/statistics/descriptive/UInt96Test.java |  140 ++
 commons-statistics-examples/examples-jmh/pom.xml   |   25 +-
 .../examples/jmh/descriptive/Int128.java           |  276 ++++
 .../examples/jmh/descriptive/IntMath.java          |  309 ++++
 .../jmh/descriptive/IntMomentPerformance.java      | 1719 ++++++++++++++++++++
 .../examples/jmh/descriptive/LongVariance2.java    |  196 +++
 .../examples/jmh/descriptive/UInt128.java          |  285 ++++
 .../examples/jmh/descriptive/UInt192.java          |  297 ++++
 .../examples/jmh/descriptive/UInt96.java           |  170 ++
 .../examples/jmh/descriptive/Int128Test.java       |  194 +++
 .../examples/jmh/descriptive/IntMathTest.java      |  152 ++
 .../examples/jmh/descriptive/LongVarianceTest.java |   47 +
 .../examples/jmh/descriptive/TestUtils.java        |  159 ++
 .../examples/jmh/descriptive/UInt128Test.java      |  238 +++
 .../examples/jmh/descriptive/UInt192Test.java      |  206 +++
 .../examples/jmh/descriptive/UInt96Test.java       |  150 ++
 src/conf/checkstyle/checkstyle-suppressions.xml    |    1 +
 39 files changed, 8240 insertions(+), 55 deletions(-)

diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index e033075..fbef6c1 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -38,6 +38,16 @@ jobs:
         distribution: temurin
         java-version: ${{ matrix.java }}
         cache: 'maven'
-    - name: Build with Maven including examples
+    - name: Build with Maven
       # Use the default goal
-      run: mvn --show-version --batch-mode --no-transfer-progress -P examples
+      if: matrix.java == 8
+      run: mvn --show-version --batch-mode --no-transfer-progress
+    - name: Build with Maven including examples
+      # Examples require Java 11+.
+      # Building javadoc errors when run with the package phase with an error about
+      # the module path.
+      # Here we run the build and javadoc generation separately (which requires install of sources)
+      if: matrix.java > 8
+      run: |
+        mvn --show-version --batch-mode --no-transfer-progress -P examples clean install -Dmaven.javadoc.skip
+        mvn -P examples javadoc:javadoc -Dmaven.javadoc.skip=false
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Int128.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Int128.java
new file mode 100644
index 0000000..5ae6357
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/Int128.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A mutable 128-bit signed integer.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values.
+ *
+ * <p>Note: This number uses a signed long integer representation of:
+ *
+ * <pre>value = 2<sup>64</sup> * hi64 + lo64</pre>
+ *
+ * <p>If the high value is zero then the low value is the long representation of the
+ * number including the sign bit. Otherwise the low value corresponds to a correction
+ * term for the scaled high value which contains the sign-bit of the number.
+ *
+ * @since 1.1
+ */
+final class Int128 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    /** low 64-bits. */
+    private long lo;
+    /** high 64-bits. */
+    private long hi;
+
+    /**
+     * Create an instance.
+     */
+    private Int128() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private Int128(long x) {
+        lo = x;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     */
+    Int128(long hi, long lo) {
+        this.lo = lo;
+        this.hi = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static Int128 create() {
+        return new Int128();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     *
+     * @param x Value.
+     * @return the instance
+     */
+    static Int128 of(long x) {
+        return new Int128(x);
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(long x) {
+        final long y = lo;
+        final long r = y + x;
+        // Overflow if the result has the opposite sign of both arguments
+        // (+,+) -> -
+        // (-,-) -> +
+        // Detect opposite sign:
+        if (((y ^ r) & (x ^ r)) < 0) {
+            // Carry overflow bit
+            hi += x < 0 ? -1 : 1;
+        }
+        lo = r;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(Int128 x) {
+        // Avoid issues adding to itself
+        final long l = x.lo;
+        final long h = x.hi;
+        add(l);
+        hi += h;
+    }
+
+    /**
+     * Compute the square of the low 64-bits of this number.
+     *
+     * <p>Warning: This ignores the upper 64-bits. Use with caution.
+     *
+     * @return the square
+     */
+    UInt128 squareLow() {
+        final long x = lo;
+        final long upper = IntMath.squareHigh(x);
+        return new UInt128(upper, x * x);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        long h = hi;
+        long l = lo;
+        // Special cases
+        if (h == 0) {
+            return BigInteger.valueOf(l);
+        }
+        if (l == 0) {
+            return BigInteger.valueOf(h).shiftLeft(64);
+        }
+
+        // The representation is 2^64 * hi64 + lo64.
+        // Here we avoid evaluating the addition:
+        // BigInteger.valueOf(l).add(BigInteger.valueOf(h).shiftLeft(64))
+        // It is faster to create from bytes.
+        // BigInteger bytes are an unsigned integer in BigEndian format, plus a sign.
+        // If both values are positive we can use the values unchanged.
+        // Otherwise selective negation is used to create a positive magnitude
+        // and we track the sign.
+        // Note: Negation of -2^63 is valid to create an unsigned 2^63.
+
+        int sign = 1;
+        if ((h ^ l) < 0) {
+            // Opposite signs and lo64 is not zero.
+            // The lo64 bits are an adjustment to the magnitude of hi64
+            // to make it smaller.
+            // Here we rearrange to [2^64 * (hi64-1)] + [2^64 - lo64].
+            // The second term [2^64 - lo64] can use lo64 as an unsigned 64-bit integer.
+            // The first term [2^64 * (hi64-1)] does not work if low is zero.
+            // It would work if zero was detected and we carried the overflow
+            // bit up to h to make it equal to: (h - 1) + 1 == h.
+            // Instead lo64 == 0 is handled as a special case above.
+
+            if (h >= 0) {
+                // Treat (unchanged) low as an unsigned add
+                h = h - 1;
+            } else {
+                // As above with negation
+                h = ~h; // -h - 1
+                l = -l;
+                sign = -1;
+            }
+        } else if (h < 0) {
+            // Invert negative values to create the equivalent positive magnitude.
+            h = -h;
+            l = -l;
+            sign = -1;
+        }
+
+        return new BigInteger(sign,
+            ByteBuffer.allocate(Long.BYTES * 2)
+                .putLong(h).putLong(l).array());
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Don't combine two 64-bit DD numbers:
+        // DD.of(hi).scalb(64).add(DD.of(lo))
+        // It is more accurate to create a 96-bit number and add the final 32-bits.
+        // Sum low to high.
+        // Note: Masking a negative hi number will create a small positive magnitude
+        // to add to a larger negative number:
+        // e.g. x = (x & 0xff) + ((x >> 8) << 8)
+        return DD.of(lo).add((hi & MASK32) * 0x1.0p64).add((hi >> Integer.SIZE) * 0x1.0p96);
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * <p>If the high value is zero then the low value is the long representation of the
+     * number including the sign bit. Otherwise this value corresponds to a correction
+     * term for the scaled high value which contains the sign-bit of the number
+     * (see {@link Int128}).
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return lo;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return the high 64-bits
+     * @see #lo64()
+     */
+    long hi64() {
+        return hi;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMath.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMath.java
new file mode 100644
index 0000000..714fc41
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMath.java
@@ -0,0 +1,391 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+/**
+ * Support class for integer math.
+ *
+ * @since 1.1
+ */
+final class IntMath {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+    /** Mask for the lower 52-bits of a long. */
+    private static final long MASK52 = 0xf_ffff_ffff_ffffL;
+    /** Bias offset for the exponent of a double. */
+    private static final int EXP_BIAS = 1023;
+    /** Shift for the exponent of a double. */
+    private static final int EXP_SHIFT = 52;
+    /** 0.5. */
+    private static final double HALF = 0.5;
+
+    /** No instances. */
+    private IntMath() {}
+
+    /**
+     * Square the values as if an unsigned 64-bit long to produce the high 64-bits
+     * of the 128-bit unsigned result.
+     *a
+     * <p>This method computes the equivalent of:
+     * <pre>{@code
+     * Math.multiplyHigh(x, x)
+     * Math.unsignedMultiplyHigh(x, x) - (((x >> 63) & x) << 1)
+     * }</pre>
+     *
+     * <p>Note: The method {@code Math.multiplyHigh} was added in JDK 9
+     * and should be used as above when the source code targets Java 11
+     * to exploit the intrinsic method.
+     *
+     * <p>Note: The method uses the unsigned multiplication. When the input is negative
+     * it can be adjusted to the signed result by subtracting the argument twice from the
+     * result.
+     *
+     * @param x Value
+     * @return the high 64-bits of the 128-bit result
+     */
+    static long squareHigh(long x) {
+        // Computation is based on the following observation about the upper (a and x)
+        // and lower (b and y) bits of unsigned big-endian integers:
+        //   ab * xy
+        // =  b *  y
+        // +  b * x0
+        // + a0 *  y
+        // + a0 * x0
+        // = b * y
+        // + b * x * 2^32
+        // + a * y * 2^32
+        // + a * x * 2^64
+        //
+        // Summation using a character for each byte:
+        //
+        //             byby byby
+        // +      bxbx bxbx 0000
+        // +      ayay ayay 0000
+        // + axax axax 0000 0000
+        //
+        // The summation can be rearranged to ensure no overflow given
+        // that the result of two unsigned 32-bit integers multiplied together
+        // plus two full 32-bit integers cannot overflow 64 bits:
+        // > long x = (1L << 32) - 1
+        // > x * x + x + x == -1 (all bits set, no overflow)
+        //
+        // The carry is a composed intermediate which will never overflow:
+        //
+        //             byby byby
+        // +           bxbx 0000
+        // +      ayay ayay 0000
+        //
+        // +      bxbx 0000 0000
+        // + axax axax 0000 0000
+
+        final long a = x >>> 32;
+        final long b = x & MASK32;
+
+        final long aa = a * a;
+        final long ab = a * b;
+        final long bb = b * b;
+
+        // Cannot overflow
+        final long carry = (bb >>> 32) +
+                           (ab & MASK32) +
+                            ab;
+        // Note:
+        // low = (carry << 32) | (bb & MASK32)
+        // Benchmarking shows outputting low to a long[] output argument
+        // has no benefit over computing 'low = value * value' separately.
+
+        final long hi = (ab >>> 32) + (carry >>> 32) + aa;
+        // Adjust to the signed result:
+        // if x < 0:
+        //    hi - 2 * x
+        return hi - (((x >> 63) & x) << 1);
+    }
+
+    /**
+     * Multiply the two values as if unsigned 64-bit longs to produce the high 64-bits
+     * of the 128-bit unsigned result.
+     *
+     * <p>This method computes the equivalent of:
+     * <pre>{@code
+     * Math.multiplyHigh(a, b) + ((a >> 63) & b) + ((b >> 63) & a)
+     * }</pre>
+     *
+     * <p>Note: The method {@code Math.multiplyHigh} was added in JDK 9
+     * and should be used as above when the source code targets Java 11
+     * to exploit the intrinsic method.
+     *
+     * <p>Note: The method {@code Math.unsignedMultiplyHigh} was added in JDK 18
+     * and should be used when the source code target allows.
+     *
+     * <p>Taken from {@code o.a.c.rng.core.source64.LXMSupport}.
+     *
+     * @param value1 the first value
+     * @param value2 the second value
+     * @return the high 64-bits of the 128-bit result
+     */
+    static long unsignedMultiplyHigh(long value1, long value2) {
+        // Computation is based on the following observation about the upper (a and x)
+        // and lower (b and y) bits of unsigned big-endian integers:
+        //   ab * xy
+        // =  b *  y
+        // +  b * x0
+        // + a0 *  y
+        // + a0 * x0
+        // = b * y
+        // + b * x * 2^32
+        // + a * y * 2^32
+        // + a * x * 2^64
+        //
+        // Summation using a character for each byte:
+        //
+        //             byby byby
+        // +      bxbx bxbx 0000
+        // +      ayay ayay 0000
+        // + axax axax 0000 0000
+        //
+        // The summation can be rearranged to ensure no overflow given
+        // that the result of two unsigned 32-bit integers multiplied together
+        // plus two full 32-bit integers cannot overflow 64 bits:
+        // > long x = (1L << 32) - 1
+        // > x * x + x + x == -1 (all bits set, no overflow)
+        //
+        // The carry is a composed intermediate which will never overflow:
+        //
+        //             byby byby
+        // +           bxbx 0000
+        // +      ayay ayay 0000
+        //
+        // +      bxbx 0000 0000
+        // + axax axax 0000 0000
+
+        final long a = value1 >>> 32;
+        final long b = value1 & MASK32;
+        final long x = value2 >>> 32;
+        final long y = value2 & MASK32;
+
+        final long by = b * y;
+        final long bx = b * x;
+        final long ay = a * y;
+        final long ax = a * x;
+
+        // Cannot overflow
+        final long carry = (by >>> 32) +
+                           (bx & MASK32) +
+                            ay;
+        // Note:
+        // low = (carry << 32) | (by & INT_TO_UNSIGNED_BYTE_MASK)
+        // Benchmarking shows outputting low to a long[] output argument
+        // has no benefit over computing 'low = value1 * value2' separately.
+
+        return (bx >>> 32) + (carry >>> 32) + ax;
+    }
+
+    /**
+     * Multiply the arguments as if unsigned integers to a {@code double} result.
+     *
+     * @param x Value.
+     * @param y Value.
+     * @return the double
+     */
+    static double unsignedMultiplyToDouble(long x, long y) {
+        final long lo = x * y;
+        // Fast case: check the arguments cannot overflow a long.
+        // This is true if neither has the upper 33-bits set.
+        if (((x | y) >>> 31) == 0) {
+            // Implicit conversion to a double.
+            return lo;
+        }
+        return uin128ToDouble(unsignedMultiplyHigh(x, y), lo);
+    }
+
+    /**
+     * Convert an unsigned 128-bit integer to a {@code double}.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     * @return the double
+     */
+    static double uin128ToDouble(long hi, long lo) {
+        // Require the representation:
+        // 2^exp * mantissa / 2^53
+        // The mantissa has an implied leading 1-bit.
+
+        // We have the mantissa final bit as xxx0 or xxx1.
+        // To perform correct rounding we maintain the 54-th bit (a) and
+        // a check bit (b) of remaining bits.
+        // Cases:
+        // xxx0 00 - round-down              [1]
+        // xxx0 0b - round-down              [1]
+        // xxx0 a0 - half-even, round-down   [4]
+        // xxx0 ab - round-up                [2]
+        // xxx1 00 - round-down              [1]
+        // xxx1 0b - round-down              [1]
+        // xxx1 a0 - half-even, round-up     [3]
+        // xxx1 ab - round-up                [2]
+        // [1] If the 54-th bit is 0 always round-down.
+        // [2] Otherwise round-up if the check bit is set or
+        // [3] the final bit is odd (half-even rounding up).
+        // [4] half-even rounding down.
+
+        if (hi == 0) {
+            // If lo is a 63-bit result then we are done
+            if (lo >= 0) {
+                return lo;
+            }
+            // Create a 63-bit number with a sticky bit for rounding, rescale the result
+            return 2 * (double) ((lo >>> 1) | (lo & 0x1));
+        }
+
+        // Initially we create the most significant 64-bits.
+        final int shift = Long.numberOfLeadingZeros(hi);
+        // Shift the high bits and add trailing low bits.
+        // The mask is for the bits from low that are *not* used.
+        // Flipping the mask obtains the bits we concatenate
+        // after shifting (64 - shift).
+        final long maskLow = -1L >>> shift;
+        long bits64 = (hi << shift) | ((lo & ~maskLow) >>> -shift);
+        // exponent for 2^exp is the index of the highest bit in the 128 bit integer
+        final int exp = 127 - shift;
+        // Some of the low bits are lost. If non-zero set
+        // a sticky bit for rounding.
+        bits64 |= (lo & maskLow) == 0 ? 0 : 1;
+
+        // We have a 64-bit unsigned fraction magnitude and an exponent.
+        // This must be converted to a IEEE double by mapping the fraction to a base of 2^53.
+
+        // Create the 53-bit mantissa without the implicit 1-bit
+        long bits = (bits64 >>> 11) & MASK52;
+        // Extract 54-th bit and a sticky bit
+        final long a = (bits64 >>> 10) & 0x1;
+        final long b = (bits64 << 54) == 0 ? 0 : 1;
+        // Perform half-even rounding.
+        bits += a & (b | (bits & 0x1));
+
+        // Add the exponent.
+        // No worry about overflow to the sign bit as the max exponent is 127.
+        bits += (long) (exp + EXP_BIAS) << EXP_SHIFT;
+
+        return Double.longBitsToDouble(bits);
+    }
+
+    /**
+     * Return the whole number that is nearest to the {@code double} argument {@code x}
+     * as an {@code int}, with ties rounding towards positive infinity.
+     *
+     * <p>This will raise an {@link ArithmeticException} if the closest
+     * integer result is not within the range {@code [-2^31, 2^31)},
+     * i.e. it overflows an {@code int}; or the argument {@code x}
+     * is not finite.
+     *
+     * <p>Note: This method is equivalent to:
+     * <pre>
+     * Math.toIntExact(Math.round(x))
+     * </pre>
+     *
+     * <p>The behaviour has been re-implemented for consistent error handling
+     * for {@code int}, {@code long} and {@code BigInteger} types.
+     *
+     * @param x Value.
+     * @return rounded value
+     * @throws ArithmeticException if the {@code result} overflows an {@code int},
+     * or {@code x} is not finite
+     * @see Math#round(double)
+     * @see Math#toIntExact(long)
+     */
+    static int toIntExact(double x) {
+        final double r = roundToInteger(x);
+        if (r >= -0x1.0p31 && r < 0x1.0p31) {
+            return (int) r;
+        }
+        throw new ArithmeticException("integer overflow: " + x);
+    }
+
+    /**
+     * Return the whole number that is nearest to the {@code double} argument {@code x}
+     * as an {@code long}, with ties rounding towards positive infinity.
+     *
+     * <p>This will raise an {@link ArithmeticException} if the closest
+     * integer result is not within the range {@code [-2^63, 2^63)},
+     * i.e. it overflows a {@code long}; or the argument {@code x}
+     * is not finite.
+     *
+     * @param x Value.
+     * @return rounded value
+     * @throws ArithmeticException if the {@code result} overflows a {@code long},
+     * or {@code x} is not finite
+     */
+    static long toLongExact(double x) {
+        final double r = roundToInteger(x);
+        if (r >= -0x1.0p63 && r < 0x1.0p63) {
+            return (long) r;
+        }
+        throw new ArithmeticException("long integer overflow: " + x);
+    }
+
+    /**
+     * Return the whole number that is nearest to the {@code double} argument {@code x}
+     * as an {@code int}, with ties rounding towards positive infinity.
+     *
+     * <p>This will raise an {@link ArithmeticException} if the argument {@code x}
+     * is not finite.
+     *
+     * @param x Value.
+     * @return rounded value
+     * @throws ArithmeticException if {@code x} is not finite
+     */
+    static BigInteger toBigIntegerExact(double x) {
+        if (!Double.isFinite(x)) {
+            throw new ArithmeticException("BigInteger overflow: " + x);
+        }
+        final double r = roundToInteger(x);
+        if (r >= -0x1.0p63 && r < 0x1.0p63) {
+            // Representable as a long
+            return BigInteger.valueOf((long) r);
+        }
+        // Large result
+        return new BigDecimal(r).toBigInteger();
+    }
+
+    /**
+     * Get the whole number that is the nearest to x, with ties rounding towards positive infinity.
+     *
+     * <p>This method is intended to perform the equivalent of
+     * {@link Math#round(double)} without converting to a {@code long} primitive type.
+     * This allows the domain of the result to be checked against the range {@code [-2^63, 2^63)}.
+     *
+     * <p>Note: Adapted from {@code o.a.c.math4.AccurateMath.rint} and
+     * modified to perform rounding towards positive infinity.
+     *
+     * @param x Number from which nearest whole number is requested.
+     * @return a double number r such that r is an integer {@code r - 0.5 <= x < r + 0.5}
+     */
+    private static double roundToInteger(double x) {
+        final double y = Math.floor(x);
+        final double d = x - y;
+        if (d >= HALF) {
+            // Here we do not preserve the sign of the operand in the case
+            // of -0.5 < x <= -0.0 since the rounded result is required as an integer.
+            // if y == -1.0:
+            //    return -0.0
+            return y + 1.0;
+        }
+        return y;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMean.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMean.java
new file mode 100644
index 0000000..0713047
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntMean.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+/**
+ * Computes the arithmetic mean of the available values. Uses the following definition
+ * of the <em>sample mean</em>:
+ *
+ * <p>\[ \frac{1}{n} \sum_{i=1}^n x_i \]
+ *
+ * <p>where \( n \) is the number of samples.
+ *
+ * <ul>
+ *   <li>The result is {@code NaN} if no values are added.
+ * </ul>
+ *
+ * <p>This class uses an exact integer sum to compute the mean. It supports up to 2<sup>63</sup>
+ * values as the count \( n \) is maintained as a {@code long}.
+ *
+ * <p>This class is designed to work with (though does not require)
+ * {@linkplain java.util.stream streams}.
+ *
+ * <p><strong>This implementation is not thread safe.</strong>
+ * If multiple threads access an instance of this class concurrently,
+ * and at least one of the threads invokes the {@link java.util.function.IntConsumer#accept(int) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link java.util.function.IntConsumer#accept(int) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
+ * as {@code accumulator} and {@code combiner} functions of
+ * {@link java.util.stream.Collector Collector} on a parallel stream,
+ * because the parallel implementation of {@link java.util.stream.Stream#collect Stream.collect()}
+ * provides the necessary partitioning, isolation, and merging of results for
+ * safe and efficient parallel execution.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/Mean">Mean (Wikipedia)</a>
+ * @since 1.1 */
+public final class IntMean implements IntStatistic, StatisticAccumulator<IntMean> {
+    /** Limit for small sample size where the sum can exactly map to a double.
+     * This is conservatively set using 2^21 values of 2^31 (2^21 ~ 2 million). */
+    private static final long SMALL_N = 1L << 21;
+
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /**
+     * Create an instance.
+     */
+    private IntMean() {
+        this(Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private IntMean(Int128 sum, int n) {
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntMean} instance.
+     */
+    public static IntMean create() {
+        return new IntMean();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntMean} instance.
+     */
+    public static IntMean of(int... values) {
+        // Sum of an array cannot exceed a 64-bit long
+        long s = 0;
+        for (final int x : values) {
+            s += x;
+        }
+        // Convert
+        return new IntMean(Int128.of(s), values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(int value) {
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the mean of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return mean of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        // Fast option when the sum fits within
+        // the mantissa of a double.
+        // Handles n=0 as NaN
+        if (n < SMALL_N) {
+            return (double) sum.lo64() / n;
+        }
+        // Extended precision.
+        // Could divide by DD.of(n) when |n| > 2^53.
+        return sum.toDD().divide(n).doubleValue();
+    }
+
+    @Override
+    public IntMean combine(IntMean other) {
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntVariance.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntVariance.java
new file mode 100644
index 0000000..70d108b
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/IntVariance.java
@@ -0,0 +1,248 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+
+/**
+ * Computes the variance of the available values. The default implementation uses the
+ * following definition of the <em>sample variance</em>:
+ *
+ * <p>\[ \tfrac{1}{n-1} \sum_{i=1}^n (x_i-\overline{x})^2 \]
+ *
+ * <p>where \( \overline{x} \) is the sample mean, and \( n \) is the number of samples.
+ *
+ * <ul>
+ *   <li>The result is {@code NaN} if no values are added.
+ *   <li>The result is zero if there is one value in the data set.
+ * </ul>
+ *
+ * <p>The use of the term \( n − 1 \) is called Bessel's correction. This is an unbiased
+ * estimator of the variance of a hypothetical infinite population. If the
+ * {@link #setBiased(boolean) biased} option is enabled the normalisation factor is
+ * changed to \( \frac{1}{n} \) for a biased estimator of the <em>sample variance</em>.
+ *
+ * <p>The implementation uses an exact integer sum to compute the scaled (by \( n \))
+ * sum of squared deviations from the mean; this is normalised by the scaled correction factor.
+ *
+ * <p>\[ \frac {n \times \sum_{i=1}^n x_i^2 - (\sum_{i=1}^n x_i)^2}{n \times (n - 1)} \]
+ *
+ * <p>It supports up to 2<sup>63</sup> values as the count \( n \) is maintained
+ * as a {@code long}.
+ *
+ * <p>This class is designed to work with (though does not require)
+ * {@linkplain java.util.stream streams}.
+ *
+ * <p><strong>This implementation is not thread safe.</strong>
+ * If multiple threads access an instance of this class concurrently,
+ * and at least one of the threads invokes the {@link java.util.function.IntConsumer#accept(int) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link java.util.function.IntConsumer#accept(int) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
+ * as {@code accumulator} and {@code combiner} functions of
+ * {@link java.util.stream.Collector Collector} on a parallel stream,
+ * because the parallel implementation of {@link java.util.stream.Stream#collect Stream.collect()}
+ * provides the necessary partitioning, isolation, and merging of results for
+ * safe and efficient parallel execution.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/variance">variance (Wikipedia)</a>
+ * @see <a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance">
+ *   Algorithms for computing the variance (Wikipedia)</a>
+ * @see <a href="https://en.wikipedia.org/wiki/Bessel%27s_correction">Bessel&#39;s correction</a>
+ * @since 1.1
+ */
+public final class IntVariance implements IntStatistic, StatisticAccumulator<IntVariance> {
+    /** Small array sample size.
+     * Used to avoid computing with UInt96 then converting to UInt128. */
+    private static final int SMALL_SAMPLE = 10;
+
+    /** Sum of the squared values. */
+    private final UInt128 sumSq;
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /** Flag to control if the statistic is biased, or should use a bias correction. */
+    private boolean biased;
+
+    /**
+     * Create an instance.
+     */
+    private IntVariance() {
+        this(UInt128.create(), Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sumSq Sum of the squared values.
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private IntVariance(UInt128 sumSq, Int128 sum, int n) {
+        this.sumSq = sumSq;
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntVariance} instance.
+     */
+    public static IntVariance create() {
+        return new IntVariance();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntVariance} instance.
+     */
+    public static IntVariance of(int... values) {
+        // Small arrays can be processed using the object
+        if (values.length < SMALL_SAMPLE) {
+            final IntVariance stat = new IntVariance();
+            for (final int x : values) {
+                stat.accept(x);
+            }
+            return stat;
+        }
+
+        // Arrays can be processed using specialised counts knowing the maximum limit
+        // for an array is 2^31 values.
+        long s = 0;
+        final UInt96 ss = UInt96.create();
+        // Process pairs as we know two maximum value int^2 will not overflow
+        // an unsigned long.
+        final int end = values.length & ~0x1;
+        for (int i = 0; i < end; i += 2) {
+            final long x = values[i];
+            final long y = values[i + 1];
+            s += x + y;
+            ss.addPositive(x * x + y * y);
+        }
+        if (end < values.length) {
+            final long x = values[end];
+            s += x;
+            ss.addPositive(x * x);
+        }
+
+        // Convert
+        return new IntVariance(UInt128.of(ss), Int128.of(s), values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(int value) {
+        sumSq.addPositive((long) value * value);
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the variance of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return variance of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        if (n == 0) {
+            return Double.NaN;
+        }
+        // Avoid a divide by zero
+        if (n == 1) {
+            return 0;
+        }
+        final long n0 = biased ? n : n - 1;
+
+        // Sum-of-squared deviations: sum(x^2) - sum(x)^2 / n
+        // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+        // The precursor is computed in integer precision.
+        // The divide uses double precision.
+        // This ensures we avoid cancellation in the difference and use a fast divide.
+        // The result is limited to by the rounding in the double computation.
+
+        // Compute the term if possible using fast integer arithmetic.
+        // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+        // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+        // Both are safe when n < 2^32.
+        double diff;
+        if ((n >>> Integer.SIZE) == 0) {
+            diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDouble();
+        } else {
+            diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(square(sum.toBigInteger())).doubleValue();
+        }
+        // Compute the divide in double precision
+        return diff / IntMath.unsignedMultiplyToDouble(n, n0);
+    }
+
+    /**
+     * Convenience method to square a BigInteger.
+     *
+     * @param x Value
+     * @return x^2
+     */
+    private static BigInteger square(BigInteger x) {
+        return x.multiply(x);
+    }
+
+    @Override
+    public IntVariance combine(IntVariance other) {
+        sumSq.add(other.sumSq);
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+
+    /**
+     * Sets the value of the biased flag. The default value is {@code false}.
+     *
+     * <p>If {@code false} the sum of squared deviations from the sample mean is normalised by
+     * {@code n - 1} where {@code n} is the number of samples. This is Bessel's correction
+     * for an unbiased estimator of the variance of a hypothetical infinite population.
+     *
+     * <p>If {@code true} the sum of squared deviations is normalised by the number of samples
+     * {@code n}.
+     *
+     * <p>Note: This option only applies when {@code n > 1}. The variance of {@code n = 1} is
+     * always 0.
+     *
+     * <p>This flag only controls the final computation of the statistic. The value of this flag
+     * will not affect compatibility between instances during a {@link #combine(IntVariance) combine}
+     * operation.
+     *
+     * @param v Value.
+     * @return {@code this} instance
+     */
+    public IntVariance setBiased(boolean v) {
+        biased = v;
+        return this;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMean.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMean.java
new file mode 100644
index 0000000..3f84a9e
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongMean.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+/**
+ * Computes the arithmetic mean of the available values. Uses the following definition
+ * of the <em>sample mean</em>:
+ *
+ * <p>\[ \frac{1}{n} \sum_{i=1}^n x_i \]
+ *
+ * <p>where \( n \) is the number of samples.
+ *
+ * <ul>
+ *   <li>The result is {@code NaN} if no values are added.
+ * </ul>
+ *
+ * <p>This class uses an exact integer sum to compute the mean. It supports up to 2<sup>63</sup>
+ * values as the count \( n \) is maintained as a {@code long}.
+ *
+ * <p>This class is designed to work with (though does not require)
+ * {@linkplain java.util.stream streams}.
+ *
+ * <p><strong>This implementation is not thread safe.</strong>
+ * If multiple threads access an instance of this class concurrently,
+ * and at least one of the threads invokes the {@link java.util.function.LongConsumer#accept(long) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link java.util.function.LongConsumer#accept(long) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
+ * as {@code accumulator} and {@code combiner} functions of
+ * {@link java.util.stream.Collector Collector} on a parallel stream,
+ * because the parallel implementation of {@link java.util.stream.Stream#collect Stream.collect()}
+ * provides the necessary partitioning, isolation, and merging of results for
+ * safe and efficient parallel execution.
+ *
+ * @since 1.1
+ */
+public final class LongMean implements LongStatistic, StatisticAccumulator<LongMean> {
+    /** Limit where the absolute sum can exactly map to a double. Set to 2^53. */
+    private static final long SMALL_SUM = 1L << 53;
+
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /**
+     * Create an instance.
+     */
+    private LongMean() {
+        this(Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private LongMean(Int128 sum, int n) {
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntMean} instance.
+     */
+    public static LongMean create() {
+        return new LongMean();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntMean} instance.
+     */
+    public static LongMean of(long... values) {
+        final Int128 s = Int128.create();
+        for (final long x : values) {
+            s.add(x);
+        }
+        return new LongMean(s, values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(long value) {
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the mean of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return mean of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        // Fast option when the sum fits within
+        // the mantissa of a double.
+        // Handles n=0 as NaN
+        if (sum.hi64() == 0 && Math.abs(sum.lo64()) < SMALL_SUM) {
+            return (double) sum.lo64() / n;
+        }
+        // Extended precision.
+        // Could divide by DD.of(n) when |n| > 2^53.
+        return sum.toDD().divide(n).doubleValue();
+    }
+
+    @Override
+    public LongMean combine(LongMean other) {
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongVariance.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongVariance.java
new file mode 100644
index 0000000..a6cbe72
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/LongVariance.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+
+/**
+ * Computes the variance of the available values. The default implementation uses the
+ * following definition of the <em>sample variance</em>:
+ *
+ * <p>\[ \tfrac{1}{n-1} \sum_{i=1}^n (x_i-\overline{x})^2 \]
+ *
+ * <p>where \( \overline{x} \) is the sample mean, and \( n \) is the number of samples.
+ *
+ * <ul>
+ *   <li>The result is {@code NaN} if no values are added.
+ *   <li>The result is zero if there is one value in the data set.
+ * </ul>
+ *
+ * <p>The use of the term \( n − 1 \) is called Bessel's correction. This is an unbiased
+ * estimator of the variance of a hypothetical infinite population. If the
+ * {@link #setBiased(boolean) biased} option is enabled the normalisation factor is
+ * changed to \( \frac{1}{n} \) for a biased estimator of the <em>sample variance</em>.
+ *
+ * <p>The implementation uses an exact integer sum to compute the scaled (by \( n \))
+ * sum of squared deviations from the mean; this is normalised by the scaled correction factor.
+ *
+ * <p>\[ \frac {n \times \sum_{i=1}^n x_i^2 - (\sum_{i=1}^n x_i)^2}{n \times (n - 1)} \]
+ *
+ * <p>It supports up to 2<sup>63</sup> values as the count \( n \) is maintained
+ * as a {@code long}.
+ *
+ * <p>This class is designed to work with (though does not require)
+ * {@linkplain java.util.stream streams}.
+ *
+ * <p><strong>This implementation is not thread safe.</strong>
+ * If multiple threads access an instance of this class concurrently,
+ * and at least one of the threads invokes the {@link java.util.function.LongConsumer#accept(long) accept} or
+ * {@link StatisticAccumulator#combine(StatisticResult) combine} method, it must be synchronized externally.
+ *
+ * <p>However, it is safe to use {@link java.util.function.LongConsumer#accept(long) accept}
+ * and {@link StatisticAccumulator#combine(StatisticResult) combine}
+ * as {@code accumulator} and {@code combiner} functions of
+ * {@link java.util.stream.Collector Collector} on a parallel stream,
+ * because the parallel implementation of {@link java.util.stream.Stream#collect Stream.collect()}
+ * provides the necessary partitioning, isolation, and merging of results for
+ * safe and efficient parallel execution.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/variance">variance (Wikipedia)</a>
+ * @see <a href="https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance">
+ *   Algorithms for computing the variance (Wikipedia)</a>
+ * @see <a href="https://en.wikipedia.org/wiki/Bessel%27s_correction">Bessel&#39;s correction</a>
+ * @since 1.1
+ */
+public final class LongVariance implements LongStatistic, StatisticAccumulator<LongVariance> {
+
+    /** Sum of the squared values. */
+    private final UInt192 sumSq;
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /** Flag to control if the statistic is biased, or should use a bias correction. */
+    private boolean biased;
+
+    /**
+     * Create an instance.
+     */
+    private LongVariance() {
+        this(UInt192.create(), Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sumSq Sum of the squared values.
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private LongVariance(UInt192 sumSq, Int128 sum, int n) {
+        this.sumSq = sumSq;
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntVariance} instance.
+     */
+    public static LongVariance create() {
+        return new LongVariance();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntVariance} instance.
+     */
+    public static LongVariance of(long... values) {
+        // Note: Arrays could be processed using specialised counts knowing the maximum limit
+        // for an array is 2^31 values. Requires a UInt160.
+
+        final Int128 s = Int128.create();
+        final UInt192 ss = UInt192.create();
+        for (final long x : values) {
+            s.add(x);
+            ss.addSquare(x);
+        }
+        return new LongVariance(ss, s, values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(long value) {
+        sumSq.addSquare(value);
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the variance of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return variance of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        if (n == 0) {
+            return Double.NaN;
+        }
+        // Avoid a divide by zero
+        if (n == 1) {
+            return 0;
+        }
+        final long n0 = biased ? n : n - 1;
+
+        // Sum-of-squared deviations: sum(x^2) - sum(x)^2 / n
+        // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+        // The precursor is computed in integer precision.
+        // The divide uses double precision.
+        // This ensures we avoid cancellation in the difference and use a fast divide.
+        // The result is limited to max 4 ulp by the rounding in the double computation
+        // When n0*n is < 2^53 the max error is reduced to two roundings.
+
+        // Compute the term if possible using fast integer arithmetic.
+        // 192-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+        // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+        // The first is safe when n < 2^32 but we must check the sum high bits.
+        double diff;
+        if (((n >>> Integer.SIZE) | sum.hi64()) == 0) {
+            diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDouble();
+        } else {
+            diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(square(sum.toBigInteger())).doubleValue();
+        }
+        // Compute the divide in double precision
+        return diff / IntMath.unsignedMultiplyToDouble(n, n0);
+    }
+
+    /**
+     * Convenience method to square a BigInteger.
+     *
+     * @param x Value
+     * @return x^2
+     */
+    private static BigInteger square(BigInteger x) {
+        return x.multiply(x);
+    }
+
+    @Override
+    public LongVariance combine(LongVariance other) {
+        sumSq.add(other.sumSq);
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+
+    /**
+     * Sets the value of the biased flag. The default value is {@code false}.
+     *
+     * <p>If {@code false} the sum of squared deviations from the sample mean is normalised by
+     * {@code n - 1} where {@code n} is the number of samples. This is Bessel's correction
+     * for an unbiased estimator of the variance of a hypothetical infinite population.
+     *
+     * <p>If {@code true} the sum of squared deviations is normalised by the number of samples
+     * {@code n}.
+     *
+     * <p>Note: This option only applies when {@code n > 1}. The variance of {@code n = 1} is
+     * always 0.
+     *
+     * <p>This flag only controls the final computation of the statistic. The value of this flag
+     * will not affect compatibility between instances during a {@link #combine(LongVariance) combine}
+     * operation.
+     *
+     * @param v Value.
+     * @return {@code this} instance
+     */
+    public LongVariance setBiased(boolean v) {
+        biased = v;
+        return this;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt128.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt128.java
new file mode 100644
index 0000000..c4b421c
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt128.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/**
+ * A mutable 128-bit unsigned integer.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values
+ * generated by squaring {@code int} values.
+ *
+ * @since 1.1
+ */
+final class UInt128 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Data is stored using integers to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int d;
+    /** bits 64-33. */
+    private int c;
+    /** bits 128-65. */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt128() {
+        // No-op
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     *
+     * @param hi High 64-bits.
+     * @param mid Middle 32-bits.
+     * @param lo Low 32-bits.
+     */
+    private UInt128(long hi, int mid, int lo) {
+        this.d = lo;
+        this.c = mid;
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     */
+    UInt128(long hi, long lo) {
+        this.d = (int) lo;
+        this.c = (int) (lo >>> Integer.SIZE);
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt128 create() {
+        return new UInt128();
+    }
+
+    /**
+     * Create an instance of the {@code UInt96} value.
+     *
+     * @param x Value.
+     * @return the instance
+     */
+    static UInt128 of(UInt96 x) {
+        final int lo = x.lo32();
+        final long hi = x.hi64();
+        final UInt128 y = new UInt128();
+        y.d = lo;
+        y.c = (int) hi;
+        y.ab = hi >>> Integer.SIZE;
+        return y;
+    }
+
+    /**
+     * Adds the value in place. It is assumed to be positive, for example the square of an
+     * {@code int} value. However no check is performed for a negative value.
+     *
+     * <p>Note: This addition handles {@value Long#MIN_VALUE} as an unsigned
+     * value of 2^63.
+     *
+     * @param x Value.
+     */
+    void addPositive(long x) {
+        // Sum with carry.
+        // Assuming x is positive then x + lo will not overflow 64-bits
+        // so we do not have to split x into upper and lower 32-bit values.
+        long s = x + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value in-place.
+     *
+     * @param x Value.
+     */
+    void add(UInt128 x) {
+        // Avoid issues adding to itself
+        final int dd = x.d;
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        long s = (dd & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+    /**
+     * Multiply by the unsigned value.
+     * Any overflow bits are lost.
+     *
+     * @param x Value.
+     * @return the product
+     */
+    UInt128 unsignedMultiply(int x) {
+        final long xx = x & MASK32;
+        // Multiply with carry.
+        long product = xx * (d & MASK32);
+        final int dd = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (c & MASK32);
+        final int cc = (int) product;
+        // Possible overflow here and bits are lost
+        final long aabb = (product >>> Integer.SIZE) + xx * ab;
+        return new UInt128(aabb, cc, dd);
+    }
+
+    /**
+     * Subtracts the value.
+     * Any overflow bits (negative result) are lost.
+     *
+     * @param x Value.
+     * @return the difference
+     */
+    UInt128 subtract(UInt128 x) {
+        // Difference with carry.
+        long diff = (d & MASK32) - (x.d & MASK32);
+        final int dd = (int) diff;
+        diff = (diff >> Integer.SIZE) + (c & MASK32) - (x.c & MASK32);
+        final int cc = (int) diff;
+        // Possible overflow here and bits are lost containing info on the
+        // magnitude of the true negative value
+        final long aabb = (diff >> Integer.SIZE) + ab - x.ab;
+        return new UInt128(aabb, cc, dd);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        // Test if we have more than 63-bits
+        if (ab != 0 || c < 0) {
+            return new BigInteger(1, ByteBuffer.allocate(Integer.BYTES * 4)
+                .putLong(ab)
+                .putInt(c)
+                .putInt(d).array());
+        }
+        // Create from a long
+        return BigInteger.valueOf(lo64());
+    }
+
+    /**
+     * Convert to a {@code double}.
+     *
+     * @return the value
+     */
+    double toDouble() {
+        return IntMath.uin128ToDouble(hi64(), lo64());
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * @return bits 64-1
+     */
+    long lo64() {
+        return (d & MASK32) | ((c & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the low 32-bits as an {@code int} value.
+     *
+     * @return bits 32-1
+     */
+    int lo32() {
+        return d;
+    }
+
+    /**
+     * Return the middle 32-bits as an {@code int} value.
+     *
+     * @return bits 64-33
+     */
+    int mid32() {
+        return c;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 128-65
+     */
+    long hi64() {
+        return ab;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt192.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt192.java
new file mode 100644
index 0000000..f390944
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt192.java
@@ -0,0 +1,247 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/**
+ * A mutable 192-bit unsigned integer.
+ *
+ * <p>This is a specialised class to implement an accumulator of squared {@code long} values.
+ *
+ * @since 1.1
+ */
+final class UInt192 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Data is stored using integers to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int f;
+    /** bits 64-33. */
+    private int e;
+    /** bits 96-65. */
+    private int d;
+    /** bits 128-97. */
+    private int c;
+    /** bits 192-129 (high 64-bits). */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt192() {
+        // No-op
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param mid Middle 64-bits.
+     * @param lo Low 64-bits.
+     */
+    UInt192(long hi, long mid, long lo) {
+        this.f = (int) lo;
+        this.e = (int) (lo >>> Integer.SIZE);
+        this.d = (int) mid;
+        this.c = (int) (mid >>> Integer.SIZE);
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     *
+     * @param ab bits 192-129 (high 64-bits).
+     * @param c bits 128-97.
+     * @param d bits 96-65.
+     * @param e bits 64-33.
+     * @param f bits 32-1.
+     */
+    private UInt192(long ab, int c, int d, int e, int f) {
+        this.ab = ab;
+        this.c = c;
+        this.d = d;
+        this.e = e;
+        this.f = f;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt192 create() {
+        return new UInt192();
+    }
+
+    /**
+     * Adds the squared value {@code x * x}.
+     *
+     * @param x Value.
+     */
+    void addSquare(long x) {
+        final long lo = x * x;
+        final long hi = IntMath.squareHigh(x);
+
+        // Sum with carry.
+        long s = (lo & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (lo >>> Integer.SIZE) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (hi & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (hi >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(UInt192 x) {
+        // Avoid issues adding to itself
+        final int ff = x.f;
+        final int ee = x.e;
+        final int dd = x.d;
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        long s = (ff & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (ee & MASK32) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (dd & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+
+    /**
+     * Multiply by the unsigned value.
+     * Any overflow bits are lost.
+     *
+     * @param x Value.
+     * @return the product
+     */
+    UInt192 unsignedMultiply(int x) {
+        final long xx = x & MASK32;
+        // Multiply with carry.
+        long product = xx * (f & MASK32);
+        final int ff = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (e & MASK32);
+        final int ee = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (d & MASK32);
+        final int dd = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (c & MASK32);
+        final int cc = (int) product;
+        // Possible overflow here and bits are lost
+        final long aabb = (product >>> Integer.SIZE) + xx * ab;
+        return new UInt192(aabb, cc, dd, ee, ff);
+    }
+
+    /**
+     * Subtracts the value.
+     * Any overflow bits (negative result) are lost.
+     *
+     * @param x Value.
+     * @return the difference
+     */
+    UInt192 subtract(UInt128 x) {
+        // Difference with carry.
+        // Subtract common part.
+        long diff = (f & MASK32) - (x.lo32()  & MASK32);
+        final int ff = (int) diff;
+        diff = (diff >> Integer.SIZE) + (e & MASK32) - (x.mid32() & MASK32);
+        final int ee = (int) diff;
+        diff = (diff >> Integer.SIZE) + (d & MASK32) - (x.hi64() & MASK32);
+        final int dd = (int) diff;
+        diff = (diff >> Integer.SIZE) + (c & MASK32) - (x.hi64() >>> Integer.SIZE);
+        final int cc = (int) diff;
+        // Possible overflow here and bits are lost containing info on the
+        // magnitude of the true negative value
+        final long aabb = (diff >> Integer.SIZE) + ab;
+        return new UInt192(aabb, cc, dd, ee, ff);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        final ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 6)
+            .putLong(ab)
+            .putInt(c)
+            .putInt(d)
+            .putInt(e)
+            .putInt(f);
+        // Sign is always positive. This works for zero.
+        return new BigInteger(1, bb.array());
+    }
+
+    /**
+     * Convert to a double.
+     *
+     * @return the value
+     */
+    double toDouble() {
+        final long h = hi64();
+        final long m = mid64();
+        final long l = lo64();
+        if (h == 0) {
+            return IntMath.uin128ToDouble(m, l);
+        }
+        // For correct rounding we use a sticky bit to represent magnitude
+        // lost from the low 64-bits. The result is scaled by 2^64.
+        return IntMath.uin128ToDouble(h, m | ((l == 0) ? 0 : 1)) * 0x1.0p64;
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return (f & MASK32) | ((e & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the middle 64-bits as a {@code long} value.
+     *
+     * @return bits 128-65
+     */
+    long mid64() {
+        return (d & MASK32) | ((c & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 192-129
+     */
+    long hi64() {
+        return ab;
+    }
+}
diff --git a/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt96.java b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt96.java
new file mode 100644
index 0000000..856a313
--- /dev/null
+++ b/commons-statistics-descriptive/src/main/java/org/apache/commons/statistics/descriptive/UInt96.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/**
+ * A mutable 96-bit unsigned integer.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values
+ * generated by squaring {@code int} values from an array (max observations=2^31).
+ *
+ * @since 1.1
+ */
+final class UInt96 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Low data is stored using an integer to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int c;
+    /** bits 96-33. */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt96() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private UInt96(long x) {
+        c = (int) x;
+        ab = (int) (x >>> Integer.SIZE);
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 32-bits.
+     */
+    UInt96(long hi, int lo) {
+        this.c = lo;
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt96 create() {
+        return new UInt96();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     * The value is assumed to be an unsigned 64-bit integer.
+     *
+     * @param x Value (must be positive).
+     * @return the instance
+     */
+    static UInt96 of(long x) {
+        return new UInt96(x);
+    }
+
+    /**
+     * Adds the value. It is assumed to be positive, for example the square of an
+     * {@code int} value. However no check is performed for a negative value.
+     *
+     * <p>Note: This addition handles {@value Long#MIN_VALUE} as an unsigned
+     * value of 2^63.
+     *
+     * @param x Value.
+     */
+    void addPositive(long x) {
+        // Sum with carry.
+        // Assuming x is positive then x + lo will not overflow 64-bits
+        // so we do not have to split x into upper and lower 32-bit values.
+        final long s = x + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(UInt96 x) {
+        // Avoid issues adding to itself
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        final long s = (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        if (ab != 0) {
+            final ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 3)
+                .putLong(ab)
+                .putInt(c);
+            return new BigInteger(1, bb.array());
+        }
+        return BigInteger.valueOf(c & MASK32);
+    }
+
+    /**
+     * Return the lower 32-bits as an {@code int} value.
+     *
+     * @return bits 32-1
+     */
+    int lo32() {
+        return c;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 96-33
+     */
+    long hi64() {
+        return ab;
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java
index 7b05937..4460121 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseIntStatisticTest.java
@@ -29,6 +29,7 @@ import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
 import org.apache.commons.statistics.distribution.TestUtils;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
@@ -395,6 +396,8 @@ abstract class BaseIntStatisticTest<S extends IntStatistic & StatisticAccumulato
      * Creates the equivalent {@link DoubleStatistic} from the {@code values}.
      * This is used to cross-validate the {@link IntStatistic} result.
      *
+     * <p>The test will be skipped if this method returns {@code null}.
+     *
      * @param values Values.
      * @return the statistic
      */
@@ -876,7 +879,9 @@ abstract class BaseIntStatisticTest<S extends IntStatistic & StatisticAccumulato
     @ParameterizedTest
     @MethodSource(value = {"testAccept"})
     final void testVsDoubleStatistic(int[] values) {
-        final double expected = createAsDoubleStatistic(values).getAsDouble();
+        final DoubleStatistic stat = createAsDoubleStatistic(values);
+        Assumptions.assumeTrue(stat != null);
+        final double expected = stat.getAsDouble();
         final DoubleTolerance tol = getToleranceAsDouble();
         TestUtils.assertEquals(expected, Statistics.add(create(), values).getAsDouble(), tol,
             () -> statisticName + " accept: " + format(values));
@@ -1212,17 +1217,4 @@ abstract class BaseIntStatisticTest<S extends IntStatistic & StatisticAccumulato
             .map(BaseIntStatisticTest::format)
             .collect(Collectors.joining(", "));
     }
-
-    /**
-     * Re-throw the error wrapped in an AssertionError with a message that appends the seed
-     * and repeat for the random order test.
-     *
-     * @param e Error.
-     * @param seed Seed.
-     * @param repeat Repeat of the total random permutations.
-     */
-    private static void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
-        throw new AssertionError(String.format("%s; Seed=%s; Repeat=%d/%d",
-            e.getMessage(), Arrays.toString(seed), repeat, RANDOM_PERMUTATIONS), e);
-    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java
index c3a567d..f8015de 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseLongStatisticTest.java
@@ -29,6 +29,7 @@ import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
 import org.apache.commons.statistics.distribution.TestUtils;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
@@ -67,7 +68,7 @@ import org.junit.jupiter.params.provider.MethodSource;
  *      This is used to verify that test cases are added using the correct type and
  *      test tolerances are configured for {@code double} result types.
  *  <li>{@link #create()}: Create an empty statistic.
- *  <li>{@link #create(int...)}: Create a statistic from a set of values.
+ *  <li>{@link #create(long...)}: Create a statistic from a set of values.
  *  <li>{@link #getEmptyValue()}: The expected value of a statistic when not enough values have
  *      been observed. The minimum number of values can be provided in {@link #getEmptySize()}.
  *  <li>{@link #getExpectedValue(long[])}: A method to compute an expected value for the
@@ -97,7 +98,7 @@ import org.junit.jupiter.params.provider.MethodSource;
  *      The default uses the class name without the "Test" suffix.
  *  <li>{@link #isCombineSymmetric()}: Specify whether to test {@code a.combine(b)} is exactly
  *      equal to {@code b.combine(a)}. The default is {@code true}.
- *  <li>{@link #mapValue(int)}: A method to update the sample data to the valid domain
+ *  <li>{@link #mapValue(long)}: A method to update the sample data to the valid domain
  *      for the statistic. This can be used to alter the default test data, for example
  *      by mapping any negative values to positive.
  * </ul>
@@ -395,6 +396,8 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
      * Creates the equivalent {@link DoubleStatistic} from the {@code values}.
      * This is used to cross-validate the {@link LongStatistic} result.
      *
+     * <p>The test will be skipped if this method returns {@code null}.
+     *
      * @param values Values.
      * @return the statistic
      */
@@ -606,8 +609,8 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
      *  <li>{@link java.util.function.LongConsumer#accept accept}
      *  <li>{@link java.util.function.LongConsumer#accept accept} and
      *      {@link StatisticAccumulator#combine(StatisticResult) combine}
-     *  <li>{@link #create(int...)}
-     *  <li>{@link #create(int...)} and
+     *  <li>{@link #create(long...)}
+     *  <li>{@link #create(long...)} and
      *      {@link StatisticAccumulator#combine(StatisticResult) combine}
      * </ol>
      *
@@ -649,7 +652,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link #create(int...)} method. The expected value and tolerance are supplied
+     * {@link #create(long...)} method. The expected value and tolerance are supplied
      * by the implementing class.
      *
      * @return the stream
@@ -666,7 +669,9 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
     @ParameterizedTest
     @MethodSource(value = {"testAccept"})
     final void testVsDoubleStatistic(long[] values) {
-        final double expected = createAsDoubleStatistic(values).getAsDouble();
+        final DoubleStatistic stat = createAsDoubleStatistic(values);
+        Assumptions.assumeTrue(stat != null);
+        final double expected = stat.getAsDouble();
         final DoubleTolerance tol = getToleranceAsDouble();
         TestUtils.assertEquals(expected, Statistics.add(create(), values).getAsDouble(), tol,
             () -> statisticName + " accept: " + format(values));
@@ -693,7 +698,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * {@link java.util.function.LongConsumer#accept(long) accept} method for each
      * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method. The expected value and tolerance are supplied by the implementing class.
      *
@@ -705,7 +710,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link #create(int...)} method for each array, then the
+     * {@link #create(long...)} method for each array, then the
      * {@link StatisticAccumulator#combine(StatisticResult) combine} method. The
      * expected value and tolerance are supplied by the implementing class.
      *
@@ -717,7 +722,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Stream the arguments to test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * {@link java.util.function.LongConsumer#accept(long) accept} method for each
      * element of a parallel stream, then the
      * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      * The expected value and tolerance are supplied by the implementing class.
@@ -863,9 +868,9 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method. The
+     * {@link java.util.function.LongConsumer#accept(long) accept} method. The
      * statistic is created using both the {@link #create()} and the
-     * {@link #create(int...)} methods; the two instances must compute the same result.
+     * {@link #create(long...)} methods; the two instances must compute the same result.
      */
     @ParameterizedTest
     @MethodSource
@@ -876,7 +881,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
     }
 
     /**
-     * Test the computation of the statistic using the {@link #create(int...)} method.
+     * Test the computation of the statistic using the {@link #create(long...)} method.
      */
     @ParameterizedTest
     @MethodSource
@@ -886,7 +891,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * {@link java.util.function.LongConsumer#accept(long) accept} method for each
      * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
@@ -898,7 +903,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Test the computation of the statistic using the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method for each
+     * {@link java.util.function.LongConsumer#accept(long) accept} method for each
      * array, then the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      */
@@ -941,7 +946,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
     /**
      * Test the computation of the statistic using a parallel stream of {@code double}
      * values. The accumulator is the
-     * {@link java.util.function.LongConsumer#accept(int) accept} method; the
+     * {@link java.util.function.LongConsumer#accept(long) accept} method; the
      * combiner is the {@link StatisticAccumulator#combine(StatisticResult) combine}
      * method.
      *
@@ -964,7 +969,7 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
 
     /**
      * Test the computation of the statistic using a parallel stream of {@code long[]}
-     * arrays. The arrays are mapped to a statistic using the {@link #create(int...)}
+     * arrays. The arrays are mapped to a statistic using the {@link #create(long...)}
      * method, and the stream reduced using the
      * {@link StatisticAccumulator#combine(StatisticResult) combine} method.
      *
@@ -1212,17 +1217,4 @@ abstract class BaseLongStatisticTest<S extends LongStatistic & StatisticAccumula
             .map(BaseLongStatisticTest::format)
             .collect(Collectors.joining(", "));
     }
-
-    /**
-     * Re-throw the error wrapped in an AssertionError with a message that appends the seed
-     * and repeat for the random order test.
-     *
-     * @param e Error.
-     * @param seed Seed.
-     * @param repeat Repeat of the total random permutations.
-     */
-    private static void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
-        throw new AssertionError(String.format("%s; Seed=%s; Repeat=%d/%d",
-            e.getMessage(), Arrays.toString(seed), repeat, RANDOM_PERMUTATIONS), e);
-    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java
index f058c84..ef28f72 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/BaseStatisticTest.java
@@ -17,6 +17,7 @@
 package org.apache.commons.statistics.descriptive;
 
 import java.math.BigInteger;
+import java.util.Arrays;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
 import org.apache.commons.statistics.distribution.DoubleTolerances;
 import org.junit.jupiter.api.Assertions;
@@ -81,7 +82,7 @@ abstract class BaseStatisticTest {
     protected abstract StatisticResult getEmptyValue();
 
     /**
-     * Creates the statistic result using an {@code double} value.
+     * Creates the statistic result using a {@code double} value.
      *
      * @param value Value.
      * @return the statistic result
@@ -148,7 +149,7 @@ abstract class BaseStatisticTest {
 
     /**
      * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testAccept(int[], StatisticResult, DoubleTolerance)} test.
+     * for a test using the primitive consumer {@code accept} method.
      *
      * <p>The default implementation uses {@link #getTolerance()}.
      *
@@ -160,7 +161,7 @@ abstract class BaseStatisticTest {
 
     /**
      * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testArray(int[], StatisticResult, DoubleTolerance)} test.
+     * for a test using creation from a primitive array.
      *
      * <p>The default implementation uses {@link #getTolerance()}.
      *
@@ -172,7 +173,8 @@ abstract class BaseStatisticTest {
 
     /**
      * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testAcceptAndCombine(int[][], StatisticResult, DoubleTolerance)} test.
+     * for a test using the primitive consumer {@code accept} method to create instances
+     * that are combined using {@link StatisticAccumulator#combine(StatisticResult)}.
      *
      * <p>The default implementation uses {@link #getTolerance()}.
      *
@@ -184,7 +186,8 @@ abstract class BaseStatisticTest {
 
     /**
      * Gets the tolerance for equality of the statistic and the expected value
-     * for the {@link #testArrayAndCombine(int[][], StatisticResult, DoubleTolerance)} test.
+     * for a test using creation from a primitive array to create instances
+     * that are combined using {@link StatisticAccumulator#combine(StatisticResult)}.
      *
      * <p>The default implementation uses {@link #getTolerance()}.
      *
@@ -199,8 +202,8 @@ abstract class BaseStatisticTest {
      * This method is used to cross-validate the statistic computation against the reference
      * {@code double} implementation.
      *
-     * <p>The default implementation return {@link #getTolerance()}.
-     * 
+     * <p>The default implementation uses {@link #getTolerance()}.
+     *
      * <p>Note: Computation using {@code double} values may not be as accurate as integer
      * specialisations. This tolerance can be set appropriately to detect errors, for example
      * using a relative tolerance of 1e-12.
@@ -307,7 +310,7 @@ abstract class BaseStatisticTest {
      *
      * @param s Statistic.
      * @return the native result
-     * @see #getNativeResult(IntStatistic)
+     * @see #getResultType()
      */
     private Object getNativeResult(StatisticResult s) {
         try {
@@ -328,4 +331,17 @@ abstract class BaseStatisticTest {
         }
         throw new IllegalStateException("Unrecognised result type: " + getResultType());
     }
+
+    /**
+     * Re-throw the error wrapped in an AssertionError with a message that appends the seed
+     * and repeat for the random order test.
+     *
+     * @param e Error.
+     * @param seed Seed.
+     * @param repeat Repeat of the total random permutations.
+     */
+    static void rethrowWithSeedAndRepeat(AssertionError e, long[] seed, int repeat) {
+        throw new AssertionError(String.format("%s; Seed=%s; Repeat=%d/%d",
+            e.getMessage(), Arrays.toString(seed), repeat, RANDOM_PERMUTATIONS), e);
+    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/Int128Test.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/Int128Test.java
new file mode 100644
index 0000000..042ef10
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/Int128Test.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link Int128}.
+ */
+class Int128Test {
+    private static final BigInteger TWO_POW_128 = BigInteger.ONE.shiftLeft(128);
+    private static final BigInteger TWO_POW_127 = BigInteger.ONE.shiftLeft(127);
+    private static final BigInteger MINUS_TWO_POW_127 = BigInteger.ONE.shiftLeft(127).negate();
+
+    @Test
+    void testCreate() {
+        final Int128 v = Int128.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"testAddLong"})
+    void testToBigInteger(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).shiftLeft(64).add(BigInteger.valueOf(b));
+        final Int128 v = new Int128(a, b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final Int128 v = Int128.of(a);
+        v.add(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, 2, Long.MIN_VALUE, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+                builder.accept(Arguments.of(i, -j));
+                builder.accept(Arguments.of(-i, j));
+                builder.accept(Arguments.of(-i, -j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final Int128 v = Int128.create();
+        for (final long x : a) {
+            v.add(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        TestHelper.assertEquals(new BigDecimal(expected), v.toDD(), 0x1.0p-106, "DD");
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> -(x >>> 2)).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, long b, long c, long d) {
+        final Int128 x = new Int128(a, b);
+        final Int128 y = new Int128(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo64());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The Int128 result is a signed 128-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [2^-127, 2^127).
+        // Since the overflow will be at most 1-bit we can wrap the value
+        // using +/- 2^128.
+        if (expected.compareTo(TWO_POW_127) >= 0) {
+            // too high
+            expected = expected.subtract(TWO_POW_128);
+        } else if (expected.compareTo(MINUS_TWO_POW_127) < 0) {
+            // too low
+            expected = expected.add(TWO_POW_128);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        TestHelper.assertEquals(new BigDecimal(expected), x.toDD(), 0x1.0p-106, "DD");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.compareTo(TWO_POW_127) >= 0) {
+            // too high
+            expected = expected.subtract(TWO_POW_128);
+        } else if (expected.compareTo(MINUS_TWO_POW_127) < 0) {
+            // too low
+            expected = expected.add(TWO_POW_128);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 1, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSquareLow(long a) {
+        final BigInteger expected = BigInteger.valueOf(a).pow(2);
+        final UInt128 v = Int128.of(a).squareLow();
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static LongStream testSquareLow() {
+        final LongStream.Builder builder = LongStream.builder();
+        final long[] x = {0, 1, Long.MIN_VALUE, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            builder.accept(i);
+            builder.accept(-i);
+        }
+        RandomSource.XO_RO_SHI_RO_128_PP.create().longs(20).forEach(builder);
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java
new file mode 100644
index 0000000..c390041
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMathTest.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+import java.util.function.Supplier;
+import java.util.stream.DoubleStream;
+import java.util.stream.LongStream;
+import java.util.stream.LongStream.Builder;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Test for {@link IntMath}.
+ */
+class IntMathTest {
+    /** 2^63. */
+    private static final BigInteger TWO_POW_63 = BigInteger.ONE.shiftLeft(63);
+
+    @ParameterizedTest
+    @MethodSource
+    void testSquareHigh(long a) {
+        final long actual = IntMath.squareHigh(a);
+        final long expected = BigInteger.valueOf(a).pow(2).shiftRight(64).longValue();
+        Assertions.assertEquals(expected, actual);
+    }
+
+    static LongStream testSquareHigh() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Builder builder = LongStream.builder();
+        builder.accept(0);
+        builder.accept(Long.MAX_VALUE);
+        builder.accept(Long.MIN_VALUE);
+        rng.ints(5).forEach(builder::accept);
+        rng.longs(50).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 1).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 2).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 5).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 13).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 35).forEach(builder::accept);
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testUnsignedMultiplyHigh(long a, long b) {
+        final long actual = IntMath.unsignedMultiplyHigh(a, b);
+        final BigInteger bi1 = toUnsignedBigInteger(a);
+        final BigInteger bi2 = toUnsignedBigInteger(b);
+        final BigInteger expected = bi1.multiply(bi2);
+        Assertions.assertEquals(expected.shiftRight(Long.SIZE).longValue(), actual,
+            () -> String.format("%s * %s", bi1, bi2));
+        final double x = expected.doubleValue();
+        Assertions.assertEquals(x, IntMath.unsignedMultiplyToDouble(a, b),
+            () -> String.format("double2 %s * %s", bi1, bi2));
+    }
+
+    static Stream<Arguments> testUnsignedMultiplyHigh() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] values = {
+            -1, 0, 1, Long.MAX_VALUE, Long.MIN_VALUE,
+            0xffL, 0xff00L, 0xff0000L, 0xff000000L,
+            0xff00000000L, 0xff0000000000L, 0xff000000000000L, 0xff000000000000L,
+            0xffffL, 0xffff0000L, 0xffff00000000L, 0xffff000000000000L,
+            0xffffffffL, 0xffffffff00000000L
+        };
+        for (final long v1 : values) {
+            for (final long v2 : values) {
+                builder.accept(Arguments.of(v1, v2));
+                builder.accept(Arguments.of(v1 >>> 15, v2 >>> 18));
+            }
+        }
+        for (int i = 0; i < 200; i++) {
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Create a big integer treating the value as unsigned.
+     *
+     * @param v Value
+     * @return the big integer
+     */
+    private static BigInteger toUnsignedBigInteger(long v) {
+        return v < 0 ?
+            TWO_POW_63.add(BigInteger.valueOf(v & Long.MAX_VALUE)) :
+            BigInteger.valueOf(v);
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testUin128ToDouble(long a, long b) {
+        final BigInteger bi1 = toUnsignedBigInteger(a).shiftLeft(Long.SIZE);
+        final BigInteger bi2 = toUnsignedBigInteger(b);
+        final double x = bi1.add(bi2).doubleValue();
+        Assertions.assertEquals(x, IntMath.uin128ToDouble(a, b),
+            () -> String.format("%s + %s", a, b));
+    }
+
+    static Stream<Arguments> testUin128ToDouble() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        for (int i = 0; i < 100; i++) {
+            long a = rng.nextLong();
+            long b = rng.nextLong();
+            builder.accept(Arguments.of(a, b));
+            builder.accept(Arguments.of(0, b));
+            // Edge cases where trailing bits are required for rounding.
+            // Create a 55-bit number. Ensure the highest bit is set.
+            a = (a << 9) | Long.MIN_VALUE;
+            // Shift right and carry bits down.
+            int shift = rng.nextInt(1, 64);
+            long c = a >>> shift;
+            long d = a << -shift;
+            // Check
+            Assertions.assertEquals(Long.bitCount(a), Long.bitCount(c) + Long.bitCount(d));
+            builder.accept(Arguments.of(c, d));
+            // Add a trailing bit that may change rounding
+            builder.accept(Arguments.of(c, d | 1));
+            // Repeat for special case of a 64-bit unsigned integer
+            builder.accept(Arguments.of(0, a | 1));
+        }
+        // At least one case where the trailing bit does effect rounding
+        // 54-bits all set is an odd number + 0.5
+        builder.accept(Arguments.of(1, (1L << 11)));
+        builder.accept(Arguments.of(1, (1L << 11) | 1));
+        // Unset the second to last bit and repeat above is an even number + 0.5
+        builder.accept(Arguments.of(1, ((1L & ~0x2) << 11)));
+        builder.accept(Arguments.of(1, ((1L & ~0x2) << 11) | 1));
+        return builder.build();
+    }
+
+    /**
+     * Test round-to-int exact matches the result generated using JDK Math functions.
+     * The only exception is NaN which will round to zero (see {@link #testToIntExactNaN()}.
+     *
+     * <p>Note: Rounding for all types is tested in StatisticResultTest.
+     */
+    @ParameterizedTest
+    @MethodSource
+    @ValueSource(doubles = {-1.265384, 67.346578, 72893.5, -42.678,
+        Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY})
+    void testToIntExact(double x) {
+        for (final int sign : new int[] {-1, 1}) {
+            final double y = x * sign;
+            final Supplier<String> intMsg = () -> String.valueOf(y);
+            Integer i = null;
+            try {
+                i = Math.toIntExact(Math.round(y));
+            } catch (Throwable t) {
+                Assertions.assertThrowsExactly(t.getClass(), () -> IntMath.toIntExact(y), intMsg);
+            }
+            if (i != null) {
+                Assertions.assertEquals(i.intValue(), IntMath.toIntExact(y), intMsg);
+            }
+        }
+    }
+
+    static DoubleStream testToIntExact() {
+        DoubleStream.Builder builder = DoubleStream.builder();
+        for (double x = 0; x < 3.5; x += 0.25) {
+            builder.accept(x);
+        }
+        for (double x = -1.5; x <= 1.5; x += 0.25) {
+            builder.accept(x + Integer.MAX_VALUE);
+            builder.accept(x + Integer.MIN_VALUE);
+        }
+        return builder.build();
+    }
+
+    /**
+     * Test round-to-int exact does not match the result generated using JDK Math functions
+     * for NaN.
+     *
+     * <p>Note: Rounding for all types is tested in StatisticResultTest.
+     */
+    @Test
+    void testToIntExactNaN() {
+        final double x = Double.NaN;
+        Assertions.assertEquals(0, Math.toIntExact(Math.round(x)));
+        Assertions.assertThrowsExactly(ArithmeticException.class, () -> IntMath.toIntExact(x));
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMeanTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMeanTest.java
new file mode 100644
index 0000000..60d5449
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntMeanTest.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+/**
+ * Test for {@link IntMean}.
+ */
+final class IntMeanTest extends BaseIntStatisticTest<IntMean> {
+
+    @Override
+    protected IntMean create() {
+        return IntMean.create();
+    }
+
+    @Override
+    protected IntMean create(int... values) {
+        return IntMean.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(int... values) {
+        return Mean.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        // Large shifts in the rolling mean are not computed very accurately
+        return DoubleTolerances.relative(5e-8);
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Double.NaN);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(int[] values) {
+        // Use the JDK as a reference implementation
+        final double x = Arrays.stream(values).average().orElse(Double.NaN);
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected DoubleTolerance getTolerance() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        final Stream.Builder<StatisticTestData> builder = Stream.builder();
+        builder.accept(addCase(Integer.MAX_VALUE - 1, Integer.MAX_VALUE));
+        builder.accept(addCase(Integer.MIN_VALUE + 1, Integer.MIN_VALUE));
+        final int[] a = new int[2 * 512 * 512];
+        Arrays.fill(a, 0, a.length / 2, 10);
+        Arrays.fill(a, a.length / 2, a.length, 1);
+        builder.accept(addReference(5.5, a));
+
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Python Numpy v1.25.1: numpy.mean
+        builder.accept(addReference(2.5, tol, 1, 2, 3, 4));
+        builder.accept(addReference(12.0, tol, 5, 9, 13, 14, 10, 12, 11, 15, 19));
+        // R v4.3.1: mean(x)
+        builder.accept(addReference(5.5, tol, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+        builder.accept(addReference(8.75, tol, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 50));
+        return builder.build();
+    }
+
+    /**
+     * Test a large integer sums that overflow a {@code long}.
+     * Overflow is created by repeat addition.
+     *
+     * <p>Note: Currently no check is made for overflow in the
+     * count of observations. If this overflows then the statistic
+     * will be incorrect so the test is limited to {@code n < 2^63}.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "-1628367811, -516725738, 60",
+        "627834682, 456456670, 61",
+        "2147483647, 2147483646, 61",
+        "-2147483648, -2147483647, 61",
+    })
+    void testLongOverflow(int x, int y, int exp) {
+        final IntMean s = IntMean.of(x, y);
+        final double mean = ((long) x + y) * 0.5;
+        for (int i = 0; i < exp; i++) {
+            // Assumes the sum as a long will overflow
+            s.combine(s);
+            Assertions.assertEquals(mean, s.getAsDouble());
+        }
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntVarianceTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntVarianceTest.java
new file mode 100644
index 0000000..5eea9e8
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/IntVarianceTest.java
@@ -0,0 +1,191 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+import org.apache.commons.statistics.distribution.TestUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link IntVariance}.
+ */
+final class IntVarianceTest extends BaseIntStatisticTest<IntVariance> {
+
+    @Override
+    protected IntVariance create() {
+        return IntVariance.create();
+    }
+
+    @Override
+    protected IntVariance create(int... values) {
+        return IntVariance.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(int... values) {
+        return Variance.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        return DoubleTolerances.ulps(20);
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Double.NaN);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(int[] values) {
+        if (values.length == 1) {
+            return createStatisticResult(0.0);
+        }
+        final long s = Arrays.stream(values).asLongStream().sum();
+        final BigInteger ss = Arrays.stream(values)
+            .mapToObj(i -> BigInteger.valueOf((long) i * i))
+            .reduce(BigInteger.ZERO, BigInteger::add);
+        final MathContext mc = MathContext.DECIMAL128;
+        final int n = values.length;
+        // var = (n * sum(x^2) - sum(x)^2) / (n * (n-1))
+        // Exact numerator
+        final BigInteger num = ss.multiply(BigInteger.valueOf(n)).subtract(
+            BigInteger.valueOf(s).pow(2));
+        // Exact divide
+        final double x = new BigDecimal(num)
+            .divide(BigDecimal.valueOf(n * (n - 1L)), mc)
+            .doubleValue();
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected DoubleTolerance getTolerance() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        final Stream.Builder<StatisticTestData> builder = Stream.builder();
+        builder.accept(addCase(Integer.MAX_VALUE - 1, Integer.MAX_VALUE));
+        builder.accept(addCase(Integer.MIN_VALUE + 1, Integer.MIN_VALUE));
+
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Python Numpy v1.25.1: numpy.var(x, ddof=1)
+        builder.accept(addReference(1.6666666666666667, tol, 1, 2, 3, 4));
+        builder.accept(addReference(7.454545454545454, tol,
+            14, 8, 11, 10, 7, 9, 10, 11, 10, 15, 5, 10));
+        // R v4.3.1: var(x)
+        builder.accept(addReference(9.166666666666666, tol, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+        builder.accept(addReference(178.75, tol, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 50));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testBiased(int[] values, double biased, double unbiased, DoubleTolerance tol) {
+        final IntVariance stat = IntVariance.of(values);
+        // Default is unbiased
+        final double actualUnbiased = stat.getAsDouble();
+        TestUtils.assertEquals(unbiased, actualUnbiased, tol, () -> "Unbiased: " + format(values));
+        Assertions.assertSame(stat, stat.setBiased(true));
+        final double acutalBiased = stat.getAsDouble();
+        TestUtils.assertEquals(biased, acutalBiased, tol, () -> "Biased: " + format(values));
+        // The mutable state can be switched back and forth
+        Assertions.assertSame(stat, stat.setBiased(false));
+        Assertions.assertEquals(actualUnbiased, stat.getAsDouble(), () -> "Unbiased: " + format(values));
+        Assertions.assertSame(stat, stat.setBiased(true));
+        Assertions.assertEquals(acutalBiased, stat.getAsDouble(), () -> "Biased: " + format(values));
+    }
+
+    static Stream<Arguments> testBiased() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Note: Biased variance is ((10-5.5)**2 + (1-5.5)**2)/2 = 20.25
+        // Scale by (2 * 512 * 512) / (2 * 512 * 512 - 1)
+        // The variance is invariant to shift
+        final int shift = 253674678;
+        final int[] a = new int[2 * 512 * 512];
+        Arrays.fill(a, 0, a.length / 2, 10 + shift);
+        Arrays.fill(a, a.length / 2, a.length, 1 + shift);
+        builder.accept(Arguments.of(a, 20.25, 20.250038623883484, tol));
+
+        // Python Numpy v1.25.1: numpy.var(x, ddof=0/1)
+        // Note: Numpy allows other degrees of freedom adjustment than 0 or 1.
+        builder.accept(Arguments.of(new int[] {1, 2, 3}, 0.6666666666666666, 1, tol));
+        builder.accept(Arguments.of(new int[] {1, 2}, 0.25, 0.5, tol));
+        // Matlab R2023s: var(x, 1/0)
+        // Matlab only allows turning the biased option on (1) or off (0).
+        // Note: Numpy will return NaN for ddof=1 when the array length is 1 (since 0 / 0 = NaN).
+        // This implementation matches the behaviour of Matlab which returns zero.
+        builder.accept(Arguments.of(new int[] {1}, 0, 0, tol));
+        builder.accept(Arguments.of(new int[] {1, 2, 4, 8}, 7.1875, 9.583333333333334, tol));
+        return builder.build();
+    }
+
+    /**
+     * Test a large integer sums that overflow a {@code long}.
+     * Overflow is created by repeat addition.
+     *
+     * <p>Note: Currently no check is made for overflow in the
+     * count of observations. If this overflows then the statistic
+     * will be incorrect so the test is limited to {@code n < 2^63}.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "-1628367811, -516725738, 60",
+        "627834682, 456456670, 61",
+        "2147483647, 2147483646, 61",
+        "-2147483648, -2147483647, 61",
+    })
+    void testLongOverflow(int x, int y, int exp) {
+        final IntVariance s = IntVariance.of(x, y);
+        // var = sum((x - mean)^2) / (n-1)
+        //     = (n * sum(x^2) - sum(x)^2) / (n * (n-1))
+        long n = 2;
+        BigInteger term1 = BigInteger.valueOf((long) x * x).add(BigInteger.valueOf((long) y * y));
+        BigInteger term2 = BigInteger.valueOf((long) x + y);
+        final DoubleTolerance tol = DoubleTolerances.ulps(2);
+        for (int i = 0; i < exp; i++) {
+            // Assumes the sum as a long will overflow
+            s.combine(s);
+            n <<= 1;
+            term1 = term1.add(term1);
+            term2 = term2.add(term2);
+            final double expected = new BigDecimal(
+                    term1.multiply(BigInteger.valueOf(n)).subtract(term2.pow(2)))
+                .divide(
+                    new BigDecimal(BigInteger.valueOf(n).multiply(BigInteger.valueOf(n - 1))),
+                    MathContext.DECIMAL128)
+                .doubleValue();
+            TestUtils.assertEquals(expected, s.getAsDouble(), tol);
+        }
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMeanTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMeanTest.java
new file mode 100644
index 0000000..0d0f86b
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongMeanTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+/**
+ * Test for {@link LongMean}.
+ */
+final class LongMeanTest extends BaseLongStatisticTest<LongMean> {
+
+    @Override
+    protected LongMean create() {
+        return LongMean.create();
+    }
+
+    @Override
+    protected LongMean create(long... values) {
+        return LongMean.of(values);
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Double.NaN);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(long... values) {
+        return Mean.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        // Data with large shifts in the rolling mean is not computed very accurately
+        return DoubleTolerances.relative(5e-8);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(long[] values) {
+        final BigInteger sum = Arrays.stream(values)
+            .mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger.ZERO, BigInteger::add);
+        final double x = new BigDecimal(sum)
+            .divide(BigDecimal.valueOf(values.length), MathContext.DECIMAL128)
+            .doubleValue();
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected DoubleTolerance getTolerance() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        final Stream.Builder<StatisticTestData> builder = Stream.builder();
+        builder.accept(addCase(Long.MAX_VALUE - 1, Long.MAX_VALUE));
+        builder.accept(addCase(Long.MIN_VALUE + 1, Long.MIN_VALUE));
+        final long[] a = new long[2 * 512 * 512];
+        Arrays.fill(a, 0, a.length / 2, 10);
+        Arrays.fill(a, a.length / 2, a.length, 1);
+        builder.accept(addReference(5.5, a));
+
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Python Numpy v1.25.1: numpy.mean
+        builder.accept(addReference(2.5, tol, 1, 2, 3, 4));
+        builder.accept(addReference(12.0, tol, 5, 9, 13, 14, 10, 12, 11, 15, 19));
+        // R v4.3.1: mean(x)
+        builder.accept(addReference(5.5, tol, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+        builder.accept(addReference(8.75, tol, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 50));
+        return builder.build();
+    }
+
+    /**
+     * Test a large integer sums that overflow a {@code long}.
+     * Overflow is created by repeat addition.
+     *
+     * <p>Note: Currently no check is made for overflow in the
+     * count of observations. If this overflows then the statistic
+     * will be incorrect so the test is limited to {@code n < 2^63}.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "-1628367672438123811, -97927322516725738, 60",
+        "3279208082627834682, 4234564566706285432, 61",
+        "9223372036854775807, 9223372036854775806, 61",
+        "-9223372036854775808, -9223372036854775807, 61",
+    })
+    void testLongOverflow(long x, long y, int exp) {
+        final LongMean s = LongMean.of(x, y);
+        final double mean = BigInteger.valueOf(x)
+            .add(BigInteger.valueOf(y)).doubleValue() * 0.5;
+        for (int i = 0; i < exp; i++) {
+            // Assumes the sum as a long will overflow
+            s.combine(s);
+            Assertions.assertEquals(mean, s.getAsDouble());
+        }
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongVarianceTest.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongVarianceTest.java
new file mode 100644
index 0000000..b17c2f4
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/LongVarianceTest.java
@@ -0,0 +1,203 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.statistics.distribution.DoubleTolerance;
+import org.apache.commons.statistics.distribution.DoubleTolerances;
+import org.apache.commons.statistics.distribution.TestUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link LongVariance}.
+ */
+final class LongVarianceTest extends BaseLongStatisticTest<LongVariance> {
+
+    @Override
+    protected LongVariance create() {
+        return LongVariance.create();
+    }
+
+    @Override
+    protected LongVariance create(long... values) {
+        return LongVariance.of(values);
+    }
+
+    @Override
+    protected DoubleStatistic createAsDoubleStatistic(long... values) {
+        if (values.length == 0) {
+            return Variance.create();
+        }
+        // Detect cases where all the values are the same double (no variance)
+        // and map values with a shift.
+        if (values.length > 1) {
+            final double first = values[0];
+            if (!Arrays.stream(values).asDoubleStream().filter(x -> x != first).findAny().isPresent()) {
+                final long shift = Arrays.stream(values).min().orElse(0);
+                values = Arrays.stream(values).map(x -> x - shift).toArray();
+            }
+        }
+        return Variance.of(Arrays.stream(values).asDoubleStream().toArray());
+    }
+
+    @Override
+    protected DoubleTolerance getToleranceAsDouble() {
+        return DoubleTolerances.ulps(20);
+    }
+
+    @Override
+    protected StatisticResult getEmptyValue() {
+        return createStatisticResult(Double.NaN);
+    }
+
+    @Override
+    protected StatisticResult getExpectedValue(long[] values) {
+        if (values.length == 1) {
+            return createStatisticResult(0.0);
+        }
+        final BigInteger s = Arrays.stream(values).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger.ZERO, BigInteger::add);
+        final BigInteger ss = Arrays.stream(values)
+            .mapToObj(i -> BigInteger.valueOf(i).pow(2))
+            .reduce(BigInteger.ZERO, BigInteger::add);
+        final MathContext mc = MathContext.DECIMAL128;
+        final int n = values.length;
+        // var = (n * sum(x^2) - sum(x)^2) / (n * (n-1))
+        // Exact numerator
+        final BigInteger num = ss.multiply(BigInteger.valueOf(n)).subtract(s.pow(2));
+        // Exact divide
+        final double x = new BigDecimal(num)
+            .divide(BigDecimal.valueOf(n * (n - 1L)), mc)
+            .doubleValue();
+        return createStatisticResult(x);
+    }
+
+    @Override
+    protected DoubleTolerance getTolerance() {
+        return DoubleTolerances.equals();
+    }
+
+    @Override
+    protected Stream<StatisticTestData> streamTestData() {
+        final Stream.Builder<StatisticTestData> builder = Stream.builder();
+        builder.accept(addCase(Long.MAX_VALUE - 1, Long.MAX_VALUE));
+        builder.accept(addCase(Long.MIN_VALUE + 1, Long.MIN_VALUE));
+
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Python Numpy v1.25.1: numpy.var(x, ddof=1)
+        builder.accept(addReference(1.6666666666666667, tol, 1, 2, 3, 4));
+        builder.accept(addReference(7.454545454545454, tol,
+            14, 8, 11, 10, 7, 9, 10, 11, 10, 15, 5, 10));
+        // R v4.3.1: var(x)
+        builder.accept(addReference(9.166666666666666, tol, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
+        builder.accept(addReference(178.75, tol, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 50));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testBiased(long[] values, double biased, double unbiased, DoubleTolerance tol) {
+        final LongVariance stat = LongVariance.of(values);
+        // Default is unbiased
+        final double actualUnbiased = stat.getAsDouble();
+        TestUtils.assertEquals(unbiased, actualUnbiased, tol, () -> "Unbiased: " + format(values));
+        Assertions.assertSame(stat, stat.setBiased(true));
+        final double acutalBiased = stat.getAsDouble();
+        TestUtils.assertEquals(biased, acutalBiased, tol, () -> "Biased: " + format(values));
+        // The mutable state can be switched back and forth
+        Assertions.assertSame(stat, stat.setBiased(false));
+        Assertions.assertEquals(actualUnbiased, stat.getAsDouble(), () -> "Unbiased: " + format(values));
+        Assertions.assertSame(stat, stat.setBiased(true));
+        Assertions.assertEquals(acutalBiased, stat.getAsDouble(), () -> "Biased: " + format(values));
+    }
+
+    static Stream<Arguments> testBiased() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        // Same cases as for the DoubleStatistic Variance but the tolerance is exact
+        final DoubleTolerance tol = DoubleTolerances.equals();
+
+        // Note: Biased variance is ((10-5.5)**2 + (1-5.5)**2)/2 = 20.25
+        // Scale by (2 * 512 * 512) / (2 * 512 * 512 - 1)
+        // The variance is invariant to shift
+        final long shift = -1379182644762676428L;
+        final long[] a = new long[2 * 512 * 512];
+        Arrays.fill(a, 0, a.length / 2, 10 + shift);
+        Arrays.fill(a, a.length / 2, a.length, 1 + shift);
+        builder.accept(Arguments.of(a, 20.25, 20.250038623883484, tol));
+
+        // Python Numpy v1.25.1: numpy.var(x, ddof=0/1)
+        // Note: Numpy allows other degrees of freedom adjustment than 0 or 1.
+        builder.accept(Arguments.of(new long[] {1, 2, 3}, 0.6666666666666666, 1, tol));
+        builder.accept(Arguments.of(new long[] {1, 2}, 0.25, 0.5, tol));
+        // Matlab R2023s: var(x, 1/0)
+        // Matlab only allows turning the biased option on (1) or off (0).
+        // Note: Numpy will return NaN for ddof=1 when the array length is 1 (since 0 / 0 = NaN).
+        // This implementation matches the behaviour of Matlab which returns zero.
+        builder.accept(Arguments.of(new long[] {1}, 0, 0, tol));
+        builder.accept(Arguments.of(new long[] {1, 2, 4, 8}, 7.1875, 9.583333333333334, tol));
+        return builder.build();
+    }
+
+    /**
+     * Test a large integer sums that overflow a {@code long}.
+     * Overflow is created by repeat addition.
+     *
+     * <p>Note: Currently no check is made for overflow in the
+     * count of observations. If this overflows then the statistic
+     * will be incorrect so the test is limited to {@code n < 2^63}.
+     */
+    @ParameterizedTest
+    @CsvSource({
+        "-1628367672438123811, -97927322516725738, 60",
+        "3279208082627834682, 4234564566706285432, 61",
+        "9223372036854775807, 9223372036854775806, 61",
+        "-9223372036854775808, -9223372036854775807, 61",
+    })
+    void testLongOverflow(long x, long y, int exp) {
+        final LongVariance s = LongVariance.of(x, y);
+        // var = sum((x - mean)^2) / (n-1)
+        //     = (n * sum(x^2) - sum(x)^2) / (n * (n-1))
+        long n = 2;
+        BigInteger term1 = BigInteger.valueOf(x).pow(2).add(BigInteger.valueOf(y).pow(2));
+        BigInteger term2 = BigInteger.valueOf(x).add(BigInteger.valueOf(y));
+        final DoubleTolerance tol = DoubleTolerances.ulps(2);
+        for (int i = 0; i < exp; i++) {
+            // Assumes the sum as a long will overflow
+            s.combine(s);
+            n <<= 1;
+            term1 = term1.add(term1);
+            term2 = term2.add(term2);
+            final double expected = new BigDecimal(
+                    term1.multiply(BigInteger.valueOf(n)).subtract(term2.pow(2)))
+                .divide(
+                    new BigDecimal(BigInteger.valueOf(n).multiply(BigInteger.valueOf(n - 1))),
+                    MathContext.DECIMAL128)
+                .doubleValue();
+            TestUtils.assertEquals(expected, s.getAsDouble(), tol);
+        }
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java
index 2eb35e9..4df0044 100644
--- a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/TestHelper.java
@@ -21,6 +21,7 @@ import java.math.BigInteger;
 import java.math.MathContext;
 import java.util.Arrays;
 import java.util.function.Supplier;
+import org.apache.commons.numbers.core.DD;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.simple.RandomSource;
 import org.apache.commons.statistics.distribution.DoubleTolerance;
@@ -447,4 +448,131 @@ final class TestHelper {
             }
         }
     }
+
+    // DD equality checks adapted from o.a.c.numbers.core.TestUtils
+
+    /**
+     * Assert the two numbers are equal within the provided relative error.
+     *
+     * <p>The provided error is relative to the exact result in expected: (e - a) / e.
+     * If expected is zero this division is undefined. In this case the actual must be zero
+     * (no absolute tolerance is supported). The reporting of the error uses the absolute
+     * error and the return value of the relative error is 0. Cases of complete cancellation
+     * should be avoided for benchmarking relative accuracy.
+     *
+     * <p>Note that the actual double-double result is not validated using the high and low
+     * parts individually. These are summed and compared to the expected.
+     *
+     * <p>Set {@code eps} to negative to report the relative error to the stdout and
+     * ignore failures.
+     *
+     * <p>The relative error is signed. The sign of the error
+     * is the same as that returned from Double.compare(actual, expected); it is
+     * computed using {@code actual - expected}.
+     *
+     * @param expected expected value
+     * @param actual actual value
+     * @param eps maximum relative error between the two values
+     * @param msg failure message
+     * @return relative error difference between the values (signed)
+     * @throws NumberFormatException if {@code actual} contains non-finite values
+     */
+    static double assertEquals(BigDecimal expected, DD actual, double eps, String msg) {
+        return assertEquals(expected, actual, eps, () -> msg);
+    }
+
+    /**
+     * Assert the two numbers are equal within the provided relative error.
+     *
+     * <p>The provided error is relative to the exact result in expected: (e - a) / e.
+     * If expected is zero this division is undefined. In this case the actual must be zero
+     * (no absolute tolerance is supported). The reporting of the error uses the absolute
+     * error and the return value of the relative error is 0. Cases of complete cancellation
+     * should be avoided for benchmarking relative accuracy.
+     *
+     * <p>Note that the actual double-double result is not validated using the high and low
+     * parts individually. These are summed and compared to the expected.
+     *
+     * <p>Set {@code eps} to negative to report the relative error to the stdout and
+     * ignore failures.
+     *
+     * <p>The relative error is signed. The sign of the error
+     * is the same as that returned from Double.compare(actual, expected); it is
+     * computed using {@code actual - expected}.
+     *
+     * @param expected expected value
+     * @param actual actual value
+     * @param eps maximum relative error between the two values
+     * @param msg failure message
+     * @return relative error difference between the values (signed)
+     * @throws NumberFormatException if {@code actual} contains non-finite values
+     */
+    static double assertEquals(BigDecimal expected, DD actual, double eps, Supplier<String> msg) {
+        // actual - expected
+        final BigDecimal delta = new BigDecimal(actual.hi())
+            .add(new BigDecimal(actual.lo()))
+            .subtract(expected);
+        boolean equal;
+        if (expected.compareTo(BigDecimal.ZERO) == 0) {
+            // Edge case. Currently an absolute tolerance is not supported as summation
+            // to zero cases generated in testing all pass.
+            equal = actual.doubleValue() == 0;
+
+            // DEBUG:
+            if (eps < 0) {
+                if (!equal) {
+                    printf("%sexpected 0 != actual <%s + %s> (abs.error=%s)%n",
+                        prefix(msg), actual.hi(), actual.lo(), delta.doubleValue());
+                }
+            } else if (!equal) {
+                Assertions.fail(String.format("%sexpected 0 != actual <%s + %s> (abs.error=%s)",
+                    prefix(msg), actual.hi(), actual.lo(), delta.doubleValue()));
+            }
+
+            return 0;
+        }
+
+        final double rel = delta.divide(expected, MathContext.DECIMAL128).doubleValue();
+        // Allow input of a negative maximum ULPs
+        equal = Math.abs(rel) <= Math.abs(eps);
+
+        // DEBUG:
+        if (eps < 0) {
+            if (!equal) {
+                printf("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))%n",
+                    prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(),
+                    rel, Math.abs(rel) / eps);
+            }
+        } else if (!equal) {
+            Assertions.fail(String.format("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))",
+                prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(),
+                rel, Math.abs(rel) / eps));
+        }
+
+        return rel;
+    }
+
+    /**
+     * Print a formatted message to stdout.
+     * Provides a single point to disable checkstyle warnings on print statements and
+     * enable/disable all print debugging.
+     *
+     * @param format Format string.
+     * @param args Arguments.
+     */
+    static void printf(String format, Object... args) {
+        // CHECKSTYLE: stop regex
+        System.out.printf(format, args);
+        // CHECKSTYLE: resume regex
+    }
+
+    /**
+     * Get the prefix for the message.
+     *
+     * @param msg Message supplier
+     * @return the prefix
+     */
+    static String prefix(Supplier<String> msg) {
+        return msg == null ? "" : msg.get() + ": ";
+    }
 }
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt128Test.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt128Test.java
new file mode 100644
index 0000000..22f3d5f
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt128Test.java
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link UInt128}.
+ */
+class UInt128Test {
+    private static final BigInteger TWO_POW_128 = BigInteger.ONE.shiftLeft(128);
+
+    @Test
+    void testCreate() {
+        final UInt128 v = UInt128.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @Test
+    void testAddLongMinValue() {
+        final UInt128 v = new UInt128(0, 1268361283468345237L);
+        final BigInteger x = BigInteger.ONE.shiftLeft(63);
+        BigInteger expected = v.toBigInteger();
+        for (int i = 1; i <= 5; i++) {
+            // Accepts a negative value without exception. This is
+            // computed correctly if the current low 32 bits
+            // added to the argument do not overflow. This is always
+            // true for min value as all lower 32-bits are zero.
+            v.addPositive(Long.MIN_VALUE);
+            expected = expected.add(x);
+            Assertions.assertEquals(expected, v.toBigInteger());
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final UInt128 v = new UInt128(0, a);
+        v.addPositive(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt128 v = UInt128.create();
+        for (final long x : a) {
+            Assertions.assertFalse(x < 0, "Value must be positive");
+            v.addPositive(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        Assertions.assertEquals(expected.doubleValue(), v.toDouble(), "double");
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, long b, long c, long d) {
+        final UInt128 x = new UInt128(a, b);
+        final UInt128 y = new UInt128(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo64());
+        Assertions.assertEquals((int) (b >>> Integer.SIZE), x.mid32());
+        Assertions.assertEquals((int) b, x.lo32());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 128-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^128).
+        if (expected.testBit(128)) {
+            expected = expected.flipBit(128);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        Assertions.assertEquals(expected.doubleValue(), x.toDouble(), "double");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(128)) {
+            expected = expected.flipBit(128);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 1, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testOfInt96(long a, int b) {
+        final UInt96 x = new UInt96(a, b);
+        final UInt128 y = UInt128.of(x);
+        Assertions.assertEquals(x.toBigInteger(), y.toBigInteger());
+    }
+
+    static Stream<Arguments> testOfInt96() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final int b = rng.nextInt();
+            builder.accept(Arguments.of(a, b));
+            builder.accept(Arguments.of(0, b));
+            builder.accept(Arguments.of(a, 0));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testMultiplyInt(long a, long b, int n) {
+        assertMultiplyInt(a, b, n);
+        assertMultiplyInt(a >>> 32, b, n);
+        assertMultiplyInt(0, b, n);
+    }
+
+    private static void assertMultiplyInt(long a, long b, int n) {
+        final UInt128 v = new UInt128(a, b);
+        BigInteger expected = v.toBigInteger().multiply(BigInteger.valueOf(n & 0xffff_ffffL));
+        // Clip to 128-bits. Only required if the upper 32-bits are non-zero.
+        final int len = expected.bitLength();
+        if (len > 128 && (v.hi64() >>> Integer.SIZE) != 0) {
+            expected = expected.subtract(expected.shiftRight(128).shiftLeft(128));
+        }
+        Assertions.assertEquals(expected, v.unsignedMultiply(n).toBigInteger());
+    }
+
+    static Stream<Arguments> testMultiplyInt() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final int[] x = {0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE};
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            for (final int n : x) {
+                builder.accept(Arguments.of(a, b, n));
+            }
+            for (int j = 0; j < 5; j++) {
+                builder.accept(Arguments.of(a, b, rng.nextInt()));
+            }
+        }
+        builder.accept(Arguments.of(-1L >>> 32, -1L, -1));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSubtract(long a, long b, long c, long d) {
+        assertSubtract(a, b, c, d);
+        assertSubtract(c, d, a, b);
+    }
+
+    private static void assertSubtract(long a, long b, long c, long d) {
+        final UInt128 x = new UInt128(a, b);
+        final UInt128 y = new UInt128(c, d);
+        BigInteger expected = x.toBigInteger().subtract(y.toBigInteger());
+        if (expected.signum() < 0) {
+            expected = expected.add(TWO_POW_128);
+        }
+        Assertions.assertEquals(expected, x.subtract(y).toBigInteger());
+    }
+
+    static Stream<Arguments> testSubtract() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            final long d = rng.nextLong();
+            builder.accept(Arguments.of(a, b, c, d));
+            builder.accept(Arguments.of(0, 0, c, d));
+            builder.accept(Arguments.of(-1L, -1L, c, d));
+        }
+        builder.accept(Arguments.of(-1L, -1L, -1L, -1L));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt192Test.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt192Test.java
new file mode 100644
index 0000000..8456855
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt192Test.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link UInt192}.
+ */
+class UInt192Test {
+    private static final BigInteger TWO_POW_192 = BigInteger.ONE.shiftLeft(192);
+
+    @Test
+    void testCreate() {
+        final UInt192 v = UInt192.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddSquareLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).pow(2)
+            .add(BigInteger.valueOf(b).pow(2));
+        final UInt192 v = UInt192.create();
+        v.addSquare(a);
+        v.addSquare(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddSquareLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 61278342166787978L, 42, 8652939272947492397L};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddSquareLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .map(x -> x.pow(2))
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt192 v = UInt192.create();
+        for (final long x : a) {
+            v.addSquare(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        Assertions.assertEquals(expected.doubleValue(), v.toDouble(), "double");
+    }
+
+    static Stream<Arguments> testAddSquareLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt192(long a, long b, long c, long d, long e, long f) {
+        final UInt192 x = new UInt192(a, b, c);
+        final UInt192 y = new UInt192(d, e, f);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.mid64());
+        Assertions.assertEquals(c, x.lo64());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 192-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^192).
+        if (expected.testBit(192)) {
+            expected = expected.flipBit(192);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d, %d) + (%d, %d, %d)", a, b, c, d, e, f));
+        // Check floating-point representation
+        Assertions.assertEquals(expected.doubleValue(), x.toDouble(), "double");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(192)) {
+            expected = expected.flipBit(192);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d, %d) self-addition", d, e, f));
+    }
+
+    static Stream<Arguments> testAddInt192() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testMultiplyInt(long a, long b, long c, int n) {
+        assertMultiplyInt(a, b, c, n);
+        assertMultiplyInt(a >>> 32, b, c, n);
+        assertMultiplyInt(0, b, c, n);
+    }
+
+    private static void assertMultiplyInt(long a, long b, long c, int n) {
+        final UInt192 v = new UInt192(a, b, c);
+        BigInteger expected = v.toBigInteger().multiply(BigInteger.valueOf(n & 0xffff_ffffL));
+        // Clip to 192-bits. Only required if the upper 32-bits are non-zero.
+        final int len = expected.bitLength();
+        if (len > 192 && (a >>> Integer.SIZE) != 0) {
+            expected = expected.subtract(expected.shiftRight(192).shiftLeft(192));
+        }
+        Assertions.assertEquals(expected, v.unsignedMultiply(n).toBigInteger());
+    }
+
+    static Stream<Arguments> testMultiplyInt() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final int[] x = {0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE};
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            for (final int n : x) {
+                builder.accept(Arguments.of(a, b, c, n));
+            }
+            for (int j = 0; j < 5; j++) {
+                builder.accept(Arguments.of(a, b, c, rng.nextInt()));
+            }
+        }
+        builder.accept(Arguments.of(-1L >>> 32, -1L, -1L, -1));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSubtract(long a, long b, long c, long d, long e) {
+        assertSubtract(a, b, c, d, e);
+    }
+
+    private static void assertSubtract(long a, long b, long c, long d, long e) {
+        final UInt192 x = new UInt192(a, b, c);
+        final UInt128 y = new UInt128(d, e);
+        BigInteger expected = x.toBigInteger().subtract(y.toBigInteger());
+        if (expected.signum() < 0) {
+            expected = expected.add(TWO_POW_192);
+        }
+        Assertions.assertEquals(expected, x.subtract(y).toBigInteger());
+    }
+
+    static Stream<Arguments> testSubtract() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            final long d = rng.nextLong();
+            final long e = rng.nextLong();
+            builder.accept(Arguments.of(a, b, c, d, e));
+            builder.accept(Arguments.of(0, 0, 0, d, e));
+            builder.accept(Arguments.of(-1L, -1L, -1L, d, e));
+        }
+        builder.accept(Arguments.of(-1L, -1L, -1L, -1L, -1L));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt96Test.java b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt96Test.java
new file mode 100644
index 0000000..f198ab7
--- /dev/null
+++ b/commons-statistics-descriptive/src/test/java/org/apache/commons/statistics/descriptive/UInt96Test.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.descriptive;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link UInt96}.
+ */
+class UInt96Test {
+    @Test
+    void testCreate() {
+        final UInt96 v = UInt96.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @Test
+    void testAddLongMinValue() {
+        final UInt96 v = UInt96.of(5675757768682342956L);
+        final BigInteger x = BigInteger.ONE.shiftLeft(63);
+        BigInteger expected = v.toBigInteger();
+        for (int i = 1; i <= 5; i++) {
+            // Accepts a negative value without exception. This is
+            // computed correctly if the current low 32 bits
+            // added to the argument do not overflow. This is always
+            // true for min value as all lower 32-bits are zero.
+            v.addPositive(Long.MIN_VALUE);
+            expected = expected.add(x);
+            Assertions.assertEquals(expected, v.toBigInteger());
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final UInt96 v = UInt96.of(a);
+        v.addPositive(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt96 v = UInt96.create();
+        for (final long x : a) {
+            Assertions.assertFalse(x < 0, "Value must be positive");
+            v.addPositive(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, int b, long c, int d) {
+        final UInt96 x = new UInt96(a, b);
+        final UInt96 y = new UInt96(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo32());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 96-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^96).
+        if (expected.testBit(96)) {
+            expected = expected.flipBit(96);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(96)) {
+            expected = expected.flipBit(96);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextInt(), rng.nextLong() >>> 2, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextInt(), rng.nextLong() >>> 1, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextInt(), rng.nextLong() >>> 2, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextInt(), rng.nextLong(), rng.nextInt()));
+        }
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/pom.xml b/commons-statistics-examples/examples-jmh/pom.xml
index f2e9386..ce3319e 100644
--- a/commons-statistics-examples/examples-jmh/pom.xml
+++ b/commons-statistics-examples/examples-jmh/pom.xml
@@ -51,10 +51,14 @@
       <artifactId>commons-rng-simple</artifactId>
     </dependency>
 
+    <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-numbers-fraction</artifactId>
+    </dependency>
+
     <dependency>
       <groupId>org.apache.commons</groupId>
       <artifactId>commons-numbers-rootfinder</artifactId>
-      <version>${statistics.commons.numbers.version}</version>
     </dependency>
 
     <dependency>
@@ -79,8 +83,14 @@
     <!-- Workaround to avoid duplicating config files. -->
     <statistics.parent.dir>${basedir}/../..</statistics.parent.dir>
 
+    <!-- JDK 11+ required for Math.multiplyHigh -->
+    <maven.compiler.source>11</maven.compiler.source>
+    <maven.compiler.target>11</maven.compiler.target>
+    <maven.compiler.release>11</maven.compiler.release>
+    <commons.compiler.release>11</commons.compiler.release>
+
     <!-- JMH Benchmark related properties: version, name of the benchmarking uber jar. -->
-    <jmh.version>1.33</jmh.version>
+    <jmh.version>1.36</jmh.version>
     <uberjar.name>examples-jmh</uberjar.name>
     <project.mainClass>org.openjdk.jmh.Main</project.mainClass>
     <!-- Disable analysis for benchmarking code. -->
@@ -88,6 +98,17 @@
     <spotbugs.skip>true</spotbugs.skip>
     <!-- Disable JDK compatibility check for benchmarking code. -->
     <animal.sniffer.skip>true</animal.sniffer.skip>
+
+    <!--
+      NOTE:
+      This module uses Java 11 but does not have an explicit module-info for imported packages.
+      A module-info is generated by the moditect plugin. This does not appear to be discovered
+      as it is in the build output directory.
+      This can cause the javadoc plugin to fail when run with the package phase so skip to make
+      this module compatible with the default goal. Run javadoc plugin using e.g.:
+      mvn javadoc:javadoc -Dmaven.javadoc.skip=false
+    -->
+    <maven.javadoc.skip>true</maven.javadoc.skip>
   </properties>
 
   <build>
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128.java
new file mode 100644
index 0000000..66c9e92
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128.java
@@ -0,0 +1,276 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A mutable 128-bit signed integer.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.Int128} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values.
+ *
+ * <p>Note: This number uses a signed long integer representation of:
+ *
+ * <pre>value = 2<sup>64</sup> * hi64 + lo64</pre>
+ *
+ * <p>If the high value is zero then the low value is the long representation of the
+ * number including the sign bit. Otherwise the low value corresponds to a correction
+ * term for the scaled high value which contains the sign-bit of the number.
+ *
+ * @since 1.1
+ */
+final class Int128 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    /** low 64-bits. */
+    private long lo;
+    /** high 64-bits. */
+    private long hi;
+
+    /**
+     * Create an instance.
+     */
+    private Int128() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private Int128(long x) {
+        lo = x;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     */
+    Int128(long hi, long lo) {
+        this.lo = lo;
+        this.hi = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static Int128 create() {
+        return new Int128();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     *
+     * @param x Value.
+     * @return the instance
+     */
+    static Int128 of(long x) {
+        return new Int128(x);
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(long x) {
+        final long y = lo;
+        final long r = y + x;
+        // Overflow if the result has the opposite sign of both arguments
+        // (+,+) -> -
+        // (-,-) -> +
+        // Detect opposite sign:
+        if (((y ^ r) & (x ^ r)) < 0) {
+            // Carry overflow bit
+            hi += x < 0 ? -1 : 1;
+        }
+        lo = r;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add2(long x) {
+        final long y = lo;
+        final long r = y + x;
+        // Overflow if the result has the opposite sign of both arguments
+        // (+,+) -> -
+        // (-,-) -> +
+        // Branchless.
+        // Extract sign bit.
+        long signMask = ((y ^ r) & (x ^ r)) >> 63;
+        // Carry using [0/1] * [+1/-1]
+        hi += signMask & (1 - ((x >>> 62) & 0x2));
+        lo = r;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(Int128 x) {
+        // Avoid issues adding to itself
+        final long l = x.lo;
+        final long h = x.hi;
+        add(l);
+        hi += h;
+    }
+
+    /**
+     * Compute the square of the low 64-bits of this number.
+     *
+     * <p>Warning: This ignores the upper 64-bits. Use with caution.
+     *
+     * @return the square
+     */
+    UInt128 squareLow() {
+        final long x = lo;
+        final long upper = IntMath.squareHigh(x);
+        return new UInt128(upper, x * x);
+    }
+
+    /**
+     * Compute the square of this number.
+     *
+     * @return the square
+     */
+    BigInteger square() {
+        if (hi == 0) {
+            final long x = lo;
+            final long upper = IntMath.squareHigh(x);
+            final long lower = x * x;
+            if (upper == 0) {
+                return BigInteger.valueOf(lower);
+            }
+            return new BigInteger(1, ByteBuffer.allocate(Long.BYTES * 2)
+                .putLong(upper).putLong(lower).array());
+        }
+        final BigInteger result = toBigInteger();
+        return result.multiply(result);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        long h = hi;
+        long l = lo;
+        // Special cases
+        if (h == 0) {
+            return BigInteger.valueOf(l);
+        }
+        if (l == 0) {
+            return BigInteger.valueOf(h).shiftLeft(64);
+        }
+
+        // The representation is 2^64 * hi64 + lo64.
+        // Here we avoid evaluating the addition:
+        // BigInteger.valueOf(l).add(BigInteger.valueOf(h).shiftLeft(64))
+        // It is faster to create from bytes.
+        // BigInteger bytes are an unsigned integer in BigEndian format, plus a sign.
+        // If both values are positive we can use the values unchanged.
+        // Otherwise selective negation is used to create a positive magnitude
+        // and we track the sign.
+        // Note: Negation of -2^63 is valid to create an unsigned 2^63.
+
+        int sign = 1;
+        if ((h ^ l) < 0) {
+            // Opposite signs and lo64 is not zero.
+            // The lo64 bits are an adjustment to the magnitude of hi64
+            // to make it smaller.
+            // Here we rearrange to [2^64 * (hi64-1)] + [2^64 - lo64].
+            // The second term [2^64 - lo64] can use lo64 as an unsigned 64-bit integer.
+            // The first term [2^64 * (hi64-1)] does not work if low is zero.
+            // It would work if zero was detected and we carried the overflow
+            // bit up to h to make it equal to: (h - 1) + 1 == h.
+            // Instead lo64 == 0 is handled as a special case above.
+
+            if (h >= 0) {
+                // Treat (unchanged) low as an unsigned add
+                h = h - 1;
+            } else {
+                // As above with negation
+                h = ~h; // -h - 1
+                l = -l;
+                sign = -1;
+            }
+        } else if (h < 0) {
+            // Invert negative values to create the equivalent positive magnitude.
+            h = -h;
+            l = -l;
+            sign = -1;
+        }
+
+        return new BigInteger(sign,
+            ByteBuffer.allocate(Long.BYTES * 2)
+                .putLong(h).putLong(l).array());
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Don't combine two 64-bit DD numbers:
+        // DD.of(hi).scalb(64).add(DD.of(lo))
+        // It is more accurate to create a 96-bit number and add the final 32-bits.
+        // Sum low to high.
+        return DD.of(lo).add((hi & MASK32) * 0x1.0p64).add((hi >> Integer.SIZE) * 0x1.0p96);
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * <p>If the high value is zero then the low value is the long representation of the
+     * number including the sign bit. Otherwise this value corresponds to a correction
+     * term for the scaled high value which contains the sign-bit of the number
+     * (see {@link Int128}).
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return lo;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return the high 64-bits
+     * @see #lo64()
+     */
+    long hi64() {
+        return hi;
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMath.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMath.java
new file mode 100644
index 0000000..4c9292e
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMath.java
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+
+/**
+ * Support class for integer math.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.IntMath} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * @since 1.1
+ */
+final class IntMath {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+    /** Mask for the lower 52-bits of a long. */
+    private static final long MASK52 = 0xf_ffff_ffff_ffffL;
+
+    /** No instances. */
+    private IntMath() {}
+
+    /**
+     * Square the values as if an unsigned 64-bit long to produce the high 64-bits
+     * of the 128-bit unsigned result.
+     *
+     * <p>This method computes the equivalent of:
+     * <pre>{@code
+     * Math.multiplyHigh(x, x)
+     * Math.unsignedMultiplyHigh(x, x) - (((x >> 63) & x) << 1)
+     * }</pre>
+     *
+     * <p>Note: The method {@code Math.multiplyHigh} was added in JDK 9
+     * and should be used as above when the source code targets Java 11
+     * to exploit the intrinsic method.
+     *
+     * <p>Note: The method uses the unsigned multiplication. When the input is negative
+     * it can be adjusted to the signed result by subtracting the argument twice from the
+     * result.
+     *
+     * @param x Value
+     * @return the high 64-bits of the 128-bit result
+     */
+    static long squareHigh(long x) {
+        // Computation is based on the following observation about the upper (a and x)
+        // and lower (b and y) bits of unsigned big-endian integers:
+        //   ab * xy
+        // =  b *  y
+        // +  b * x0
+        // + a0 *  y
+        // + a0 * x0
+        // = b * y
+        // + b * x * 2^32
+        // + a * y * 2^32
+        // + a * x * 2^64
+        //
+        // Summation using a character for each byte:
+        //
+        //             byby byby
+        // +      bxbx bxbx 0000
+        // +      ayay ayay 0000
+        // + axax axax 0000 0000
+        //
+        // The summation can be rearranged to ensure no overflow given
+        // that the result of two unsigned 32-bit integers multiplied together
+        // plus two full 32-bit integers cannot overflow 64 bits:
+        // > long x = (1L << 32) - 1
+        // > x * x + x + x == -1 (all bits set, no overflow)
+        //
+        // The carry is a composed intermediate which will never overflow:
+        //
+        //             byby byby
+        // +           bxbx 0000
+        // +      ayay ayay 0000
+        //
+        // +      bxbx 0000 0000
+        // + axax axax 0000 0000
+
+        final long a = x >>> 32;
+        final long b = x & MASK32;
+
+        final long aa = a * a;
+        final long ab = a * b;
+        final long bb = b * b;
+
+        // Cannot overflow
+        final long carry = (bb >>> 32) +
+                           (ab & MASK32) +
+                            ab;
+        // Note:
+        // low = (carry << 32) | (bb & MASK32)
+        // Benchmarking shows outputting low to a long[] output argument
+        // has no benefit over computing 'low = value * value' separately.
+
+        final long hi = (ab >>> 32) + (carry >>> 32) + aa;
+        // Adjust to the signed result:
+        // if x < 0:
+        //    hi - 2 * x
+        return hi - (((x >> 63) & x) << 1);
+    }
+
+    /**
+     * Multiply the two values as if unsigned 64-bit longs to produce the high 64-bits
+     * of the 128-bit unsigned result.
+     *
+     * <p>This method computes the equivalent of:
+     * <pre>{@code
+     * Math.multiplyHigh(a, b) + ((a >> 63) & b) + ((b >> 63) & a)
+     * }</pre>
+     *
+     * <p>Note: The method {@code Math.multiplyHigh} was added in JDK 9
+     * and should be used as above when the source code targets Java 11
+     * to exploit the intrinsic method.
+     *
+     * <p>Note: The method {@code Math.unsignedMultiplyHigh} was added in JDK 18
+     * and should be used when the source code target allows.
+     *
+     * <p>Taken from {@code o.a.c.rng.core.source64.LXMSupport}.
+     *
+     * @param value1 the first value
+     * @param value2 the second value
+     * @return the high 64-bits of the 128-bit result
+     */
+    static long unsignedMultiplyHigh(long value1, long value2) {
+        // Computation is based on the following observation about the upper (a and x)
+        // and lower (b and y) bits of unsigned big-endian integers:
+        //   ab * xy
+        // =  b *  y
+        // +  b * x0
+        // + a0 *  y
+        // + a0 * x0
+        // = b * y
+        // + b * x * 2^32
+        // + a * y * 2^32
+        // + a * x * 2^64
+        //
+        // Summation using a character for each byte:
+        //
+        //             byby byby
+        // +      bxbx bxbx 0000
+        // +      ayay ayay 0000
+        // + axax axax 0000 0000
+        //
+        // The summation can be rearranged to ensure no overflow given
+        // that the result of two unsigned 32-bit integers multiplied together
+        // plus two full 32-bit integers cannot overflow 64 bits:
+        // > long x = (1L << 32) - 1
+        // > x * x + x + x == -1 (all bits set, no overflow)
+        //
+        // The carry is a composed intermediate which will never overflow:
+        //
+        //             byby byby
+        // +           bxbx 0000
+        // +      ayay ayay 0000
+        //
+        // +      bxbx 0000 0000
+        // + axax axax 0000 0000
+
+        final long a = value1 >>> 32;
+        final long b = value1 & MASK32;
+        final long x = value2 >>> 32;
+        final long y = value2 & MASK32;
+
+        final long by = b * y;
+        final long bx = b * x;
+        final long ay = a * y;
+        final long ax = a * x;
+
+        // Cannot overflow
+        final long carry = (by >>> 32) +
+                           (bx & MASK32) +
+                            ay;
+        // Note:
+        // low = (carry << 32) | (by & INT_TO_UNSIGNED_BYTE_MASK)
+        // Benchmarking shows outputting low to a long[] output argument
+        // has no benefit over computing 'low = value1 * value2' separately.
+
+        return (bx >>> 32) + (carry >>> 32) + ax;
+    }
+
+    /**
+     * Multiply the arguments as if unsigned integers to a {@code double} result.
+     *
+     * @param a Value.
+     * @param b Value.
+     * @return the double
+     */
+    static double unsignedMultiplyToDoubleBigInteger(long a, long b) {
+        final long lo = a * b;
+
+        // Fast case: check the arguments cannot overflow a long.
+        // This is true if neither has the upper 33-bits set.
+        if (((a | b) >>> 31) == 0) {
+            // Implicit conversion to a double
+            return lo;
+        }
+
+        final long hi = unsignedMultiplyHigh(a, b);
+
+        // Convert to a double using BigInteger
+        return new BigInteger(1, ByteBuffer.allocate(Long.BYTES * 2)
+            .putLong(hi)
+            .putLong(lo)
+            .array()).doubleValue();
+    }
+
+    /**
+     * Multiply the arguments as if unsigned integers to a {@code double} result.
+     *
+     * @param x Value.
+     * @param y Value.
+     * @return the double
+     */
+    static double unsignedMultiplyToDouble(long x, long y) {
+        final long lo = x * y;
+        // Fast case: check the arguments cannot overflow a long.
+        // This is true if neither has the upper 33-bits set.
+        if (((x | y) >>> 31) == 0) {
+            // Implicit conversion to a double.
+            return lo;
+        }
+        return uin128ToDouble(unsignedMultiplyHigh(x, y), lo);
+    }
+
+    /**
+     * Convert an unsigned 128-bit integer to a {@code double}.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     * @return the double
+     */
+    static double uin128ToDouble(long hi, long lo) {
+        // Require the representation:
+        // 2^exp * mantissa / 2^53
+        // The mantissa has an implied leading 1-bit.
+
+        // We have the mantissa final bit as xxx0 or xxx1.
+        // To perform correct rounding we maintain the 54-th bit (a) and
+        // a check bit (b) of remaining bits.
+        // Cases:
+        // xxx0 00 - round-down              [1]
+        // xxx0 0b - round-down              [1]
+        // xxx0 a0 - half-even, round-down   [4]
+        // xxx0 ab - round-up                [2]
+        // xxx1 00 - round-down              [1]
+        // xxx1 0b - round-down              [1]
+        // xxx1 a0 - half-even, round-up     [3]
+        // xxx1 ab - round-up                [2]
+        // [1] If the 54-th bit is 0 always round-down.
+        // [2] Otherwise round-up if the check bit is set or
+        // [3] the final bit is odd (half-even rounding up).
+        // [4] half-even rounding down.
+
+        if (hi == 0) {
+            // If lo is a 63-bit result then we are done
+            if (lo >= 0) {
+                return lo;
+            }
+            // Create a 63-bit number with a sticky bit for rounding, rescale the result
+            return 2 * (double) ((lo >>> 1) | (lo & 0x1));
+        }
+
+        // Initially we create the most significant 64-bits.
+        final int shift = Long.numberOfLeadingZeros(hi);
+        // Shift the high bits and add trailing low bits.
+        // The mask is for the bits from low that are *not* used.
+        // Flipping the mask obtains the bits we concatenate
+        // after shifting (64 - shift).
+        final long maskLow = -1L >>> shift;
+        long bits64 = (hi << shift) | ((lo & ~maskLow) >>> -shift);
+        // exponent for 2^exp is the index of the highest bit in the 128 bit integer
+        final int exp = 127 - shift;
+        // Some of the low bits are lost. If non-zero set
+        // a sticky bit for rounding.
+        bits64 |= (lo & maskLow) == 0 ? 0 : 1;
+
+        // We have a 64-bit unsigned fraction magnitude and an exponent.
+        // This must be converted to a IEEE double by mapping the fraction to a base of 2^53.
+
+        // Create the 53-bit mantissa without the implicit 1-bit
+        long bits = (bits64 >>> 11) & MASK52;
+        // Extract 54-th bit and a sticky bit
+        final long a = (bits64 >>> 10) & 0x1;
+        final long b = (bits64 << 54) == 0 ? 0 : 1;
+        // Perform half-even rounding.
+        bits += a & (b | (bits & 0x1));
+        // Add the exponent.
+        // No worry about overflow to the sign bit as the max exponent is 127.
+        bits += (long) (exp + 1023) << 52;
+
+        return Double.longBitsToDouble(bits);
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMomentPerformance.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMomentPerformance.java
new file mode 100644
index 0000000..cbc4af6
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMomentPerformance.java
@@ -0,0 +1,1719 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.math.MathContext;
+import java.util.Arrays;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+import java.util.function.DoubleConsumer;
+import java.util.function.DoubleSupplier;
+import java.util.function.IntConsumer;
+import java.util.function.LongConsumer;
+import java.util.function.Supplier;
+import java.util.function.ToDoubleFunction;
+import java.util.function.ToLongFunction;
+import java.util.stream.LongStream;
+import org.apache.commons.numbers.core.DD;
+import org.apache.commons.numbers.fraction.BigFraction;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.apache.commons.statistics.descriptive.DoubleStatistic;
+import org.apache.commons.statistics.descriptive.IntMean;
+import org.apache.commons.statistics.descriptive.IntStatistic;
+import org.apache.commons.statistics.descriptive.IntVariance;
+import org.apache.commons.statistics.descriptive.LongMean;
+import org.apache.commons.statistics.descriptive.LongStatistic;
+import org.apache.commons.statistics.descriptive.LongVariance;
+import org.apache.commons.statistics.descriptive.Mean;
+import org.apache.commons.statistics.descriptive.Variance;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Executes a benchmark of the moment-based statistics for integer values
+ * ({@code int} or {@code long}) compared to using {@code double} values.
+ */
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
+@State(Scope.Benchmark)
+@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"})
+public class IntMomentPerformance {
+    /** Commons Statistics Mean implementation. */
+    private static final String DOUBLE_MEAN = "DoubleMean";
+    /** Integer mean implementation. */
+    private static final String INT_MEAN = "IntMean";
+    /** Long mean implementation. */
+    private static final String LONG_MEAN = "LongMean";
+    /** Sum using a long mean implementation. */
+    private static final String LONG_SUM_MEAN = "LongSumMean";
+    /** Sum using a BigInteger mean implementation. */
+    private static final String BIG_INTEGER_SUM_MEAN = "BigIntegerSumMean";
+    /** JDK Stream mean implementation. */
+    private static final String STREAM_MEAN = "StreamMean";
+    /** Commons Statistics Variance implementation. */
+    private static final String DOUBLE_VAR = "DoubleVariance";
+    /** Integer variance implementation. */
+    private static final String INT_VAR = "IntVariance";
+    /** Long variance implementation. */
+    private static final String LONG_VAR = "LongVariance";
+    /** Long variance implementation using Math.multiplyHigh. */
+    private static final String LONG_VAR2 = "LongVariance2";
+
+    /**
+     * Source of array data.
+     */
+    @State(Scope.Benchmark)
+    public static class DataSource {
+        /** Data length. */
+        @Param({"2", "1000"})
+        private int length;
+
+        /** Data. */
+        private int[] data;
+
+        /** Data as a double. */
+        private double[] doubleData;
+
+        /** Data as a long. */
+        private long[] longData;
+
+        /**
+         * @return the data
+         */
+        public int[] getData() {
+            return data;
+        }
+
+        /**
+         * @return the data
+         */
+        public double[] getDoubleData() {
+            return doubleData;
+        }
+
+        /**
+         * @return the data
+         */
+        public long[] getLongData() {
+            return longData;
+        }
+
+        /**
+         * Create the data.
+         * Data will be randomized per iteration.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            longData = RandomSource.XO_RO_SHI_RO_128_PP.create().longs(length).toArray();
+            doubleData = Arrays.stream(longData).asDoubleStream().toArray();
+            data = Arrays.stream(longData).mapToInt(x -> (int) x).toArray();
+        }
+    }
+
+    /**
+     * Source of a {@link IntConsumer} action.
+     */
+    @State(Scope.Benchmark)
+    public static class IntActionSource {
+        /** Name of the source. */
+        @Param({DOUBLE_MEAN, INT_MEAN,
+                // Disabled: Run-time ~ IntMean
+                // LONG_SUM_MEAN
+                DOUBLE_VAR, INT_VAR})
+        private String name;
+
+        /** The action. */
+        private Supplier<IntStatistic> action;
+
+        /**
+         * @return the action
+         */
+        public IntStatistic getAction() {
+            return action.get();
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (DOUBLE_MEAN.equals(name)) {
+                action = () -> {
+                    final Mean m = Mean.create();
+                    return createIntStatistic(m, m);
+                };
+            } else if (INT_MEAN.equals(name)) {
+                action = () -> {
+                    final IntMean m = IntMean.create();
+                    return createIntStatistic(m, m);
+                };
+            } else if (LONG_SUM_MEAN.equals(name)) {
+                action = () -> {
+                    final LongSumMean m = new LongSumMean();
+                    return createIntStatistic(m, m);
+                };
+            } else if (DOUBLE_VAR.equals(name)) {
+                action = () -> {
+                    final Variance m = Variance.create();
+                    return createIntStatistic(m, m);
+                };
+            } else if (INT_VAR.equals(name)) {
+                action = () -> {
+                    final IntVariance m = IntVariance.create();
+                    return createIntStatistic(m, m);
+                };
+            } else {
+                throw new IllegalStateException("Unknown int action: " + name);
+            }
+        }
+
+        /**
+         * Creates the {@link IntStatistic}.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static IntStatistic createIntStatistic(IntConsumer c, DoubleSupplier s) {
+            return new IntStatistic() {
+                @Override
+                public void accept(int value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+
+        /**
+         * Creates the {@link IntStatistic}.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static IntStatistic createIntStatistic(DoubleConsumer c, DoubleSupplier s) {
+            return new IntStatistic() {
+                @Override
+                public void accept(int value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+    }
+
+    /**
+     * Source of a {@link DoubleConsumer} action.
+     */
+    @State(Scope.Benchmark)
+    public static class DoubleActionSource {
+        /** Name of the source. */
+        @Param({DOUBLE_MEAN, DOUBLE_VAR})
+        private String name;
+
+        /** The action. */
+        private Supplier<DoubleStatistic> action;
+
+        /**
+         * @return the action
+         */
+        public DoubleStatistic getAction() {
+            return action.get();
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (DOUBLE_MEAN.equals(name)) {
+                action = () -> {
+                    final Mean m = Mean.create();
+                    return createDoubleStatistic(m, m);
+                };
+            } else if (DOUBLE_VAR.equals(name)) {
+                action = () -> {
+                    final Variance m = Variance.create();
+                    return createDoubleStatistic(m, m);
+                };
+            } else {
+                throw new IllegalStateException("Unknown double action: " + name);
+            }
+        }
+
+        /**
+         * Creates the {@link DoubleStatistic}.
+         *
+         * <p>This method is here to provide parity when comparing actual instances
+         * of {@link DoubleStatistic} with composed objects for the equivalent
+         * int/long statistics.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static DoubleStatistic createDoubleStatistic(DoubleConsumer c, DoubleSupplier s) {
+            return new DoubleStatistic() {
+                @Override
+                public void accept(double value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+    }
+
+    /**
+     * Source of a {@link LongConsumer} action.
+     */
+    @State(Scope.Benchmark)
+    public static class LongActionSource {
+        /** Name of the source. */
+        @Param({DOUBLE_MEAN, LONG_MEAN, BIG_INTEGER_SUM_MEAN,
+                DOUBLE_VAR, LONG_VAR, LONG_VAR2})
+        private String name;
+
+        /** The action. */
+        private Supplier<LongStatistic> action;
+
+        /**
+         * @return the action
+         */
+        public LongStatistic getAction() {
+            return action.get();
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (DOUBLE_MEAN.equals(name)) {
+                action = () -> {
+                    final Mean m = Mean.create();
+                    return createLongStatistic(m, m);
+                };
+            } else if (LONG_MEAN.equals(name)) {
+                action = () -> {
+                    final LongMean m = LongMean.create();
+                    return createLongStatistic(m, m);
+                };
+            } else if (BIG_INTEGER_SUM_MEAN.equals(name)) {
+                action = () -> {
+                    final BigIntegerSumMean m = new BigIntegerSumMean();
+                    return createLongStatistic(m, m);
+                };
+            } else if (DOUBLE_VAR.equals(name)) {
+                action = () -> {
+                    final Variance m = Variance.create();
+                    return createLongStatistic(m, m);
+                };
+            } else if (LONG_VAR.equals(name)) {
+                action = () -> {
+                    final LongVariance m = LongVariance.create();
+                    return createLongStatistic(m, m);
+                };
+            } else if (LONG_VAR2.equals(name)) {
+                action = () -> {
+                    final LongVariance2 m = LongVariance2.create();
+                    return createLongStatistic(m, m);
+                };
+            } else {
+                throw new IllegalStateException("Unknown long action: " + name);
+            }
+        }
+
+        /**
+         * Creates the {@link LongStatistic}.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static LongStatistic createLongStatistic(LongConsumer c, DoubleSupplier s) {
+            return new LongStatistic() {
+                @Override
+                public void accept(long value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+
+        /**
+         * Creates the {@link LongStatistic}.
+         *
+         * @param c Consumer.
+         * @param s Supplier.
+         * @return the statistic
+         */
+        private static LongStatistic createLongStatistic(DoubleConsumer c, DoubleSupplier s) {
+            return new LongStatistic() {
+                @Override
+                public void accept(long value) {
+                    c.accept(value);
+                }
+                @Override
+                public double getAsDouble() {
+                    return s.getAsDouble();
+                }
+            };
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code int[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class IntFunctionSource {
+        /** Name of the source. */
+        @Param({INT_MEAN,
+            // Disabled: Run-time ~ IntMean
+            //LONG_SUM_MEAN,
+            STREAM_MEAN, INT_VAR})
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<int[]> function;
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<int[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (INT_MEAN.equals(name)) {
+                function = x -> IntMean.of(x).getAsDouble();
+            } else if (LONG_SUM_MEAN.equals(name)) {
+                function = LongSumMean::mean;
+            } else if (STREAM_MEAN.equals(name)) {
+                function = x -> Arrays.stream(x).average().orElse(Double.NaN);
+            } else if (INT_VAR.equals(name)) {
+                function = x -> IntVariance.of(x).getAsDouble();
+            } else {
+                throw new IllegalStateException("Unknown int function: " + name);
+            }
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code double[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class DoubleFunctionSource {
+        /** Name of the source. */
+        @Param({DOUBLE_MEAN, DOUBLE_VAR})
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<double[]> function;
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<double[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (DOUBLE_MEAN.equals(name)) {
+                function = x -> Mean.of(x).getAsDouble();
+            } else if (DOUBLE_VAR.equals(name)) {
+                function = x -> Variance.of(x).getAsDouble();
+            } else {
+                throw new IllegalStateException("Unknown double function: " + name);
+            }
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code long[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class LongFunctionSource {
+        /** Name of the source. */
+        @Param({LONG_MEAN, BIG_INTEGER_SUM_MEAN, LONG_VAR, LONG_VAR2})
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<long[]> function;
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<long[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if (LONG_MEAN.equals(name)) {
+                function = x -> LongMean.of(x).getAsDouble();
+            } else if (BIG_INTEGER_SUM_MEAN.equals(name)) {
+                function = BigIntegerSumMean::mean;
+            } else if (LONG_VAR.equals(name)) {
+                function = x -> LongVariance.of(x).getAsDouble();
+            } else if (LONG_VAR2.equals(name)) {
+                function = x -> LongVariance2.of(x).getAsDouble();
+            } else {
+                throw new IllegalStateException("Unknown long function: " + name);
+            }
+        }
+    }
+
+    /**
+     * Class containing the variance data.
+     */
+    static class IntVarianceData {
+        /** Sum of the squared values. */
+        private final UInt128 sumSq;
+        /** Sum of the values. */
+        private final Int128 sum;
+        /** Count of values that have been added. */
+        private long n;
+
+        /**
+         * @param sumSq Sum of the squared values.
+         * @param sum Sum of the values.
+         * @param n Count of values that have been added.
+         */
+        IntVarianceData(UInt128 sumSq, Int128 sum, long n) {
+            this.sumSq = sumSq;
+            this.sum = sum;
+            this.n = n;
+        }
+
+        /**
+         * @return the sum of the squared values
+         */
+        UInt128 getSumSq() {
+            return new UInt128(sumSq.hi64(), sumSq.lo64());
+        }
+
+        /**
+         * @return the sum
+         */
+        Int128 getSum() {
+            return new Int128(sum.hi64(), sum.lo64());
+        }
+
+        /**
+         * @return the count of values that have been added
+         */
+        long getN() {
+            return n;
+        }
+
+        /**
+         * @return the copy
+         */
+        IntVarianceData copy() {
+            return new IntVarianceData(getSumSq(), getSum(), n);
+        }
+
+        /**
+         * Adds the other instance.
+         *
+         * @param other the other
+         * @return this instance
+         */
+        IntVarianceData add(IntVarianceData other) {
+            // Prevent the data from becoming too large
+            n = Math.addExact(n, other.n);
+            sumSq.add(other.sumSq);
+            sum.add(other.sum);
+            return this;
+        }
+    }
+
+    /**
+     * Source of {@code int} variance data.
+     *
+     * <p>This class generates a pool of variance data from a random sample of integers
+     * in a range. The pool objects are then combined with each other for a given number of
+     * rounds, effectively doubling the size of pool objects each round.
+     * Using the defaults will create objects in the pool of:
+     * <pre>
+     * E[ sum(x) ] = (511 / 2) mean value * (95 / 2) mean samples ~ 12136.25 ~ 2^8 * 2^5.5 {@code < 2^14}
+     * E[ sum(x^2) = ((511 / 2)^2 mean value^2) * (95 / 2) mean samples ~ 3100811.875 ~ 2^16 * 2^5.5 {@code < 2^22}
+     * Max[ sum(x) = 511 * 63 = 32193 {@code < 2^15}
+     * Max[ sum(x^2) = 15^2 * 63 = 14175 {@code < 2^16}
+     * man[ n ] = 63 {@code < 2^6}
+     * </pre>
+     * <p>The objects from this pool can be added together a maximum of 56 times before n overflows.
+     * The sum of the values will overflow a long at approximately 18 combines.
+     */
+    @State(Scope.Benchmark)
+    public static class IntVarianceDataSource {
+        /** Consistent seed. */
+        private static final Long SEED = ThreadLocalRandom.current().nextLong();
+        /** Lower limit. */
+        @Param({"0"})
+        private int origin;
+        /** Upper limit. */
+        @Param({"512"})
+        private int bound;
+        /** Minimum samples. */
+        @Param({"32"})
+        private int minSamples;
+        /** Maximum samples. */
+        @Param({"64"})
+        private int maxSamples;
+        /** Pool size. */
+        @Param({"64"})
+        private int poolSize;
+        /** Number of combine operations. */
+        @Param({"8", "16", "24", "32", "48"})
+        private int combine;
+
+        /** Data. */
+        private IntVarianceData[] data;
+
+        /**
+         * The number of data values.
+         *
+         * @return the size
+         */
+        public int size() {
+            return data.length;
+        }
+        /**
+         * Get a copy of the data for the specified index.
+         *
+         * @param i Index.
+         * @return the data
+         */
+        public IntVarianceData getData(int i) {
+            return data[i].copy();
+        }
+
+        /**
+         * Create the data.
+         */
+        @Setup
+        public void setup() {
+            // Consistent seed so the same data is provided to all methods
+            final UniformRandomProvider rng = RandomSource.XO_SHI_RO_512_SS.create(SEED);
+            // Initial pool
+            final IntVarianceData[] pool = new IntVarianceData[poolSize];
+            for (int i = 0; i < pool.length; i++) {
+                final int n = rng.nextInt(minSamples, maxSamples);
+                final UInt128 sumSq = UInt128.create();
+                final Int128 sum = Int128.create();
+                rng.ints(n, origin, bound).forEach(x -> {
+                    sumSq.addPositive((long) x * x);
+                    sum.add(x);
+                });
+                pool[i] = new IntVarianceData(sumSq, sum, n);
+            }
+            // Combine to grow the average size of the pool objects
+            for (int round = 0; round < combine; round++) {
+                final IntVarianceData[] last = pool.clone();
+                for (int i = 0; i < pool.length; i++) {
+                    // Copy the instance that will be the LHS of the add operation
+                    pool[i] = last[i].copy().add(last[rng.nextInt(poolSize)]);
+                }
+            }
+            data = pool;
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code IntVarianceData}.
+     */
+    @State(Scope.Benchmark)
+    public static class IntVarianceFunctionSource {
+        /** {@link MathContext} with 20 digits of precision. */
+        private static final MathContext MC_20_DIGITS = new MathContext(20);
+
+        /** Name of the source. */
+        @Param({"DD", "DD2", "BigIntegerPow", "BigIntegerMultiply",
+            "SumSquareBigInteger", "SumSquareMultiplyBigInteger",
+            "UIntBigInteger", "UIntDD", "UIntDD2", "UIntBigInteger2", "UIntBigInteger3",
+            "UIntDouble",
+            // Very slow
+            //"UIntBigFraction", "UIntBigDecimal"
+        })
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<IntVarianceData> function;
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<IntVarianceData> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if ("DD".equals(name)) {
+                function = IntVarianceFunctionSource::varianceDD;
+            } else if ("DD2".equals(name)) {
+                function = IntVarianceFunctionSource::varianceDD2;
+            } else if ("BigIntegerPow".equals(name)) {
+                function = IntVarianceFunctionSource::varianceBigIntegerPow;
+            } else if ("BigIntegerMultiply".equals(name)) {
+                function = IntVarianceFunctionSource::varianceBigIntegerMultiply;
+            } else if ("SumSquareBigInteger".equals(name)) {
+                function = IntVarianceFunctionSource::varianceSumSquareBigInteger;
+            } else if ("SumSquareMultiplyBigInteger".equals(name)) {
+                function = IntVarianceFunctionSource::varianceSumSquareMultiplyIntBigInteger;
+            } else if ("UIntBigInteger".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigInteger;
+            } else if ("UIntDD".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntDD;
+            } else if ("UIntDD2".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntDD2;
+            } else if ("UIntBigInteger2".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigInteger2;
+            } else if ("UIntBigInteger3".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigInteger3;
+            } else if ("UIntDouble".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntDouble;
+            } else if ("UIntBigFraction".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigFraction;
+            } else if ("UIntBigDecimal".equals(name)) {
+                function = IntVarianceFunctionSource::varianceUIntBigDecimal;
+            } else {
+                throw new IllegalStateException("Unknown int variance function: " + name);
+            }
+        }
+
+        /**
+         * Convenience method to square a BigInteger.
+         *
+         * @param x Value
+         * @return x^2
+         */
+        private static BigInteger square(BigInteger x) {
+            return x.multiply(x);
+        }
+
+        /**
+         * Compute the variance using double-double arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceDD(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            final DD diff = sumSq.toDD().multiply(n).subtract(sum.toDD().square());
+            if (diff.hi() < 0) {
+                return 0;
+            }
+            // Divisor is an exact double
+            if (n < (1L << 26)) {
+                // n0*n is safe as a long
+                return diff.divide(n0 * n).doubleValue();
+            }
+            return diff.divide(DD.of(n).multiply(DD.of(n0))).doubleValue();
+        }
+
+        /**
+         * Compute the variance using double-double arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceDD2(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations: sum(x^2) - sum(x)^2 / n
+            final DD ss = sumSq.toDD().subtract(sum.toDD().square().divide(n));
+            if (ss.hi() < 0) {
+                return 0;
+            }
+            return ss.divide(n0).doubleValue();
+        }
+
+        /**
+         * Compute the variance using BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceBigIntegerPow(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            final BigInteger diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(sum.toBigInteger().pow(2));
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceBigIntegerMultiply(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            final BigInteger diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(square(sum.toBigInteger()));
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using Int128 and BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceSumSquareBigInteger(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the second term if possible using fast integer arithmetic.
+            final BigInteger term1 = sumSq.toBigInteger().multiply(BigInteger.valueOf(n));
+            final BigInteger term2 = sum.hi64() == 0 ? sum.squareLow().toBigInteger() : square(sum.toBigInteger());
+            final BigInteger diff = term1.subtract(term2);
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using UInt128/Int128 and BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceSumSquareMultiplyIntBigInteger(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // sum(x^2) * n will be OK when n < 2^32.
+            final BigInteger term1 = n < 1L << 32 ? sumSq.unsignedMultiply((int) n).toBigInteger() :
+                sumSq.toBigInteger().multiply(BigInteger.valueOf(n));
+            final BigInteger term2 = sum.hi64() == 0 ? sum.squareLow().toBigInteger() : square(sum.toBigInteger());
+            final BigInteger diff = term1.subtract(term2);
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using UInt128/Int128 and BigInteger arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigInteger(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                // It may still be possible to compute the square
+                BigInteger sum2;
+                if (sum.hi64() == 0) {
+                    sum2 = sum.squareLow().toBigInteger();
+                } else {
+                    sum2 = sum.toBigInteger();
+                    sum2 = sum2.multiply(sum2);
+                }
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(sum2);
+            }
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using UInt128/Int128 and DD arithmetic.
+         * The final divide uses double precision.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntDD(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // Both are safe when n < 2^32.
+            if ((n >>> Integer.SIZE) == 0) {
+                DD diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDD();
+                // Divisor is an exact double
+                if (n < (1L << 26)) {
+                    // n0*n is safe as a long
+                    return diff.divide(n0 * n).doubleValue();
+                }
+                return diff.divide(DD.of(n).multiply(DD.of(n0))).doubleValue();
+            }
+            BigInteger diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using UInt128/Int128 and DD arithmetic.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntDD2(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // Both are safe when n < 2^32.
+            if ((n >>> Integer.SIZE) == 0) {
+                DD diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDD();
+                // Divisor is an exact double
+                if (n < (1L << 26)) {
+                    // n0*n is safe as a long
+                    return diff.divide(n0 * n).doubleValue();
+                }
+                return diff.divide(DD.of(n).multiply(DD.of(n0))).doubleValue();
+            }
+            BigInteger diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            // Assume n is big to overflow the sum(x)
+            // Compute the divide in double-double precision
+            return DD.of(diff.doubleValue()).divide(DD.of(n).multiply(DD.of(n0))).doubleValue();
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * not fast compute the squared sum. This benchmarks as faster: the BigInteger multiply
+         * on small values for sum(x)^2 is efficient.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigInteger2(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            }
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * computes the squared sum in Int128. This benchmarks slower than converting to BigInteger
+         * and computing the square.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigInteger3(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(sum.square());
+            }
+            // Compute the divide in double precision
+            return diff.doubleValue() / ((double) n0 * n);
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * not fast compute the squared sum. This benchmarks as faster: the BigInteger multiply
+         * on small values for sum(x)^2 is efficient.
+         *
+         * <p>This method uses the {@link UInt128#toDouble()} to avoid going via BigInteger.
+         * The divisor is computed in extended precision.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntDouble(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            double diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDouble();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                    .subtract(square(sum.toBigInteger())).doubleValue();
+            }
+            // Compute the divide in double precision
+            return diff / IntMath.unsignedMultiplyToDouble(n, n0);
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * not fast compute the squared sum. This benchmarks as faster: the BigInteger multiply
+         * on small values for sum(x)^2 is efficient.
+         *
+         * <p>The final divide uses BigFraction for large size, or double.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigFraction(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            }
+            if (n < (1L << 26)) {
+                // Compute the divide in double precision
+                return diff.doubleValue() / ((double) n0 * n);
+            }
+            return BigFraction.of(diff, BigInteger.valueOf(n0).multiply(BigInteger.valueOf(n)))
+                .doubleValue();
+        }
+
+        /**
+         * Compute the variance using unsigned integer (UInt128/Int128 or BigInteger) arithmetic.
+         * The final divide uses double precision.
+         *
+         * <p>Note: This is similar to {@link #varianceUIntBigInteger(IntVarianceData)} but does
+         * not fast compute the squared sum. This benchmarks as faster: the BigInteger multiply
+         * on small values for sum(x)^2 is efficient.
+         *
+         * <p>The final divide uses BigDecimal for large size, or double.
+         *
+         * @param data Variance data.
+         * @return the variance
+         */
+        static double varianceUIntBigDecimal(IntVarianceData data) {
+            final long n = data.getN();
+            if (n == 0) {
+                return Double.NaN;
+            }
+            // Avoid a divide by zero
+            if (n == 1) {
+                return 0;
+            }
+            final UInt128 sumSq = data.getSumSq();
+            final Int128 sum = data.getSum();
+            // Assume unbiased
+            final long n0 = n - 1;
+            // Extended precision.
+            // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+            // Compute the term if possible using fast integer arithmetic.
+            // 128-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+            // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+            // Both are safe when n < 2^32.
+            BigInteger diff;
+            if ((n >>> Integer.SIZE) == 0) {
+                diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toBigInteger();
+            } else {
+                diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n)).subtract(square(sum.toBigInteger()));
+            }
+            if (n < (1L << 26)) {
+                // Compute the divide in double precision
+                return diff.doubleValue() / ((double) n0 * n);
+            }
+            return new BigDecimal(diff).divide(new BigDecimal(
+                BigInteger.valueOf(n0).multiply(BigInteger.valueOf(n))), MC_20_DIGITS)
+                .doubleValue();
+        }
+    }
+
+    /**
+     * Source of {@code long} array data.
+     * The data is designed to overflow a sum as a long with a specified frequency.
+     * There are 3 cases: positive values; negative values; any sign. The amount
+     * of overflow is controlled using a shift to remove magnitude. No shift expects
+     * overflow 50% of the time when summing same sign values. If both signs are used then the
+     * random walk will be based around 0 with overflow occurring proportional to
+     * the magnitude. Chance of overflow will rapidly drop when the values are not full
+     * magnitude numbers.
+     */
+    @State(Scope.Benchmark)
+    public static class LongDataSource {
+        /** Data length: 2^10. If shift is above 10 then no overflow will occur. */
+        @Param({"1024"})
+        private int length;
+        /** Data sign. */
+        @Param({"positive", "negative", "both"})
+        private String sign;
+        /** Data bit shift. */
+        @Param({"0", "1", "2", "4", "8", "16"})
+        private int shift;
+
+        /** Data. */
+        private long[] data;
+
+        /**
+         * @return the data
+         */
+        public long[] getData() {
+            return data;
+        }
+
+        /**
+         * Create the data.
+         * Data will be randomized per iteration.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            LongStream s = RandomSource.XO_RO_SHI_RO_128_PP.create().longs(length);
+            if ("positive".equals(sign)) {
+                s = s.map(x -> x >>> 1);
+            } else if ("negative".equals(sign)) {
+                s = s.map(x -> x | Long.MIN_VALUE);
+            } else if (!"both".equals(sign)) {
+                throw new IllegalStateException("Unknown sign: " + sign);
+            }
+            if (shift > 0) {
+                final int bits = shift;
+                // Signed shift maintains negative values
+                s = s.map(x -> x >> bits);
+            }
+            data = s.toArray();
+        }
+    }
+
+    /**
+     * Source of a {@link ToLongFunction} for a {@code long[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class LongSumFunctionSource {
+        /** Name of the source.
+         * The branchless 128bitAdd2 runs at constant speed but is slower than 128bitAdd. */
+        @Param({"128bitAdd", "128bitAdd2", "64bitSum"})
+        private String name;
+
+        /** The action. */
+        private ToLongFunction<long[]> function;
+
+        /**
+         * @return the function
+         */
+        public ToLongFunction<long[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            if ("128bitAdd".equals(name)) {
+                function = x -> {
+                    final Int128 s = Int128.create();
+                    for (long y : x) {
+                        s.add(y);
+                    }
+                    return s.hi64();
+                };
+            } else if ("128bitAdd2".equals(name)) {
+                function = x -> {
+                    final Int128 s = Int128.create();
+                    for (long y : x) {
+                        s.add2(y);
+                    }
+                    return s.hi64();
+                };
+            } else if ("64bitSum".equals(name)) {
+                function = x -> {
+                    long s = 0;
+                    for (long y : x) {
+                        s += y;
+                    }
+                    return s;
+                };
+            } else {
+                throw new IllegalStateException("Unknown long sum function: " + name);
+            }
+        }
+    }
+
+    /**
+     * Source of {@code long} array data to multiply as unsigned pairs.
+     * Magnitude is approximately controlled using a bit shift on the values.
+     */
+    @State(Scope.Benchmark)
+    public static class MultiplyLongDataSource {
+        /** Data length. */
+        @Param({"1024"})
+        private int length;
+        /** Data bit shift. */
+        @Param({"0", "33"})
+        private int shift;
+
+        /** Data. */
+        private long[] data;
+
+        /**
+         * @return the data
+         */
+        public long[] getData() {
+            return data;
+        }
+
+        /**
+         * Create the data.
+         * Data will be randomized per iteration.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            LongStream s = RandomSource.XO_RO_SHI_RO_128_PP.create().longs(length * 2L);
+            if (shift > 0) {
+                final int bits = shift;
+                s = s.map(x -> x >>> bits);
+            }
+            data = s.toArray();
+        }
+    }
+
+    /**
+     * Source of a {@link ToDoubleFunction} for a {@code long[]}.
+     */
+    @State(Scope.Benchmark)
+    public static class MultiplyLongFunctionSource {
+        /** Name of the source. */
+        @Param({"double", "unsignedMultiplyToDoubleBigInteger", "unsignedMultiplyToDouble"})
+        private String name;
+
+        /** The action. */
+        private ToDoubleFunction<long[]> function;
+
+        /**
+         * Function for two long arguments.
+         */
+        interface LongLongToDoubleFunction {
+            /**
+             * Apply the function.
+             *
+             * @param a Value.
+             * @param b Value.
+             * @return the result
+             */
+            double apply(long a, long b);
+        }
+
+        /**
+         * @return the function
+         */
+        public ToDoubleFunction<long[]> getFunction() {
+            return function;
+        }
+
+        /**
+         * Create the function.
+         */
+        @Setup(Level.Iteration)
+        public void setup() {
+            final LongLongToDoubleFunction f = createFunction(name);
+            function = x -> applyAll(x, f);
+        }
+
+        /**
+         * Creates the function.
+         *
+         * @param functionName Function name.
+         * @return the function
+         */
+        private LongLongToDoubleFunction createFunction(String functionName) {
+            if ("double".equals(functionName)) {
+                return (x, y) -> (double) x * y;
+            } else if ("unsignedMultiplyToDoubleBigInteger".equals(name)) {
+                return IntMath::unsignedMultiplyToDoubleBigInteger;
+            } else if ("unsignedMultiplyToDouble".equals(name)) {
+                return IntMath::unsignedMultiplyToDouble;
+            } else {
+                throw new IllegalStateException("Unknown multiply long function: " + name);
+            }
+        }
+
+        /**
+         * Apply the function to all pairs in the data.
+         *
+         * @param array Data.
+         * @param f Function.
+         * @return the result
+         */
+        private static double applyAll(long[] array, LongLongToDoubleFunction f) {
+            double s = 0;
+            for (int i = 0; i < array.length; i += 2) {
+                s += f.apply(array[i], array[i + 1]);
+            }
+            return s;
+        }
+    }
+
+    /**
+     * A mean of {@code int} data using a {@code long} sum.
+     */
+    static class LongSumMean implements IntConsumer, DoubleSupplier {
+        /** Count of values that have been added. */
+        private long n;
+
+        /** Sum of values that have been added. */
+        private long s;
+
+        @Override
+        public void accept(int value) {
+            s += value;
+            n++;
+        }
+
+        @Override
+        public double getAsDouble() {
+            return (double) s / n;
+        }
+
+        /**
+         * Compute the mean using a sum.
+         *
+         * @param data Data.
+         * @return the mean
+         */
+        static double mean(int[] data) {
+            long s = 0;
+            for (final int x : data) {
+                s += x;
+            }
+            return (double) s / data.length;
+        }
+    }
+
+    /**
+     * A mean of {@code long} data using a {@code BigInteger} sum.
+     */
+    static class BigIntegerSumMean implements LongConsumer, DoubleSupplier {
+        /** Count of values that have been added. */
+        private long n;
+
+        /** Sum of values that have been added. */
+        private BigInteger s = BigInteger.ZERO;
+
+        @Override
+        public void accept(long value) {
+            s = s.add(BigInteger.valueOf(value));
+            n++;
+        }
+
+        @Override
+        public double getAsDouble() {
+            return s.doubleValue() / n;
+        }
+
+        /**
+         * Compute the mean using a sum.
+         *
+         * @param data Data.
+         * @return the mean
+         */
+        static double mean(long[] data) {
+            BigInteger s = BigInteger.ZERO;
+            for (final long x : data) {
+                s = s.add(BigInteger.valueOf(x));
+            }
+            return s.doubleValue() / data.length;
+        }
+    }
+
+    /**
+     * Apply the action to each {@code int} value.
+     *
+     * @param <T> the action type
+     * @param action Action.
+     * @param values Values.
+     * @return the value
+     */
+    static <T extends IntConsumer & DoubleSupplier> double forEach(T action, int[] values) {
+        for (final int x : values) {
+            action.accept(x);
+        }
+        return action.getAsDouble();
+    }
+
+    /**
+     * Apply the action to each {@code double} value.
+     *
+     * @param <T> the action type
+     * @param action Action.
+     * @param values Values.
+     * @return the value
+     */
+    static <T extends DoubleConsumer & DoubleSupplier> double forEach(T action, double[] values) {
+        for (final double x : values) {
+            action.accept(x);
+        }
+        return action.getAsDouble();
+    }
+
+    /**
+     * Apply the action to each {@code long} value.
+     *
+     * @param <T> the action type
+     * @param action Action.
+     * @param values Values.
+     * @return the value
+     */
+    static <T extends LongConsumer & DoubleSupplier> double forEach(T action, long[] values) {
+        for (final long x : values) {
+            action.accept(x);
+        }
+        return action.getAsDouble();
+    }
+
+    /**
+     * Create the statistic using a consumer of {@code int} values.
+     *
+     * @param action Source of the data action.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double forEachIntStatistic(IntActionSource action, DataSource source) {
+        return forEach(action.getAction(), source.getData());
+    }
+
+    /**
+     * Create the statistic using a consumer of {@code double} values.
+     *
+     * @param action Source of the data action.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double forEachDoubleStatistic(DoubleActionSource action, DataSource source) {
+        return forEach(action.getAction(), source.getDoubleData());
+    }
+
+    /**
+     * Create the statistic using a consumer of {@code long} values.
+     *
+     * @param action Source of the data action.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double forEachLongStatistic(LongActionSource action, DataSource source) {
+        return forEach(action.getAction(), source.getLongData());
+    }
+
+    /**
+     * Create the statistic using a {@code int[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double arrayIntStatistic(IntFunctionSource function, DataSource source) {
+        return function.getFunction().applyAsDouble(source.getData());
+    }
+
+    /**
+     * Create the statistic using a {@code double[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double arrayDoubleStatistic(DoubleFunctionSource function, DataSource source) {
+        return function.getFunction().applyAsDouble(source.getDoubleData());
+    }
+
+    /**
+     * Create the statistic using a {@code long[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the statistic
+     */
+    @Benchmark
+    public double arrayLongStatistic(LongFunctionSource function, DataSource source) {
+        return function.getFunction().applyAsDouble(source.getLongData());
+    }
+
+    /**
+     * Create the variance using a aggregated {@code int[]} data.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @param bh Data sink.
+     */
+    @Benchmark
+    public void intVariance(IntVarianceFunctionSource function, IntVarianceDataSource source, Blackhole bh) {
+        final int size = source.size();
+        final ToDoubleFunction<IntVarianceData> f = function.getFunction();
+        for (int i = 0; i < size; i++) {
+            bh.consume(f.applyAsDouble(source.getData(i)));
+        }
+    }
+
+    /**
+     * Create the sum using a {@code long[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the sum
+     */
+    @Benchmark
+    public long longSum(LongSumFunctionSource function, LongDataSource source) {
+        return function.getFunction().applyAsLong(source.getData());
+    }
+
+    /**
+     * Create the product using a {@code long[]} function.
+     *
+     * @param function Source of the function.
+     * @param source Source of the data.
+     * @return the sum
+     */
+    @Benchmark
+    public double multiplyToDouble(MultiplyLongFunctionSource function, MultiplyLongDataSource source) {
+        return function.getFunction().applyAsDouble(source.getData());
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVariance2.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVariance2.java
new file mode 100644
index 0000000..451d569
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVariance2.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.util.function.DoubleSupplier;
+import java.util.function.LongConsumer;
+
+/**
+ * Computes the variance of the available values.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.LongVariance} to allow benchmarking.
+ * This uses {@code java.lang.Math.multiplyHigh(long, long)} and requires Java 11+.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/variance">variance (Wikipedia)</a>
+ * @since 1.1
+ */
+final class LongVariance2 implements LongConsumer, DoubleSupplier {
+
+    /** Sum of the squared values. */
+    private final UInt192 sumSq;
+    /** Sum of the values. */
+    private final Int128 sum;
+    /** Count of values that have been added. */
+    private long n;
+
+    /** Flag to control if the statistic is biased, or should use a bias correction. */
+    private boolean biased;
+
+    /**
+     * Create an instance.
+     */
+    private LongVariance2() {
+        this(UInt192.create(), Int128.create(), 0);
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param sumSq Sum of the squared values.
+     * @param sum Sum of the values.
+     * @param n Count of values that have been added.
+     */
+    private LongVariance2(UInt192 sumSq, Int128 sum, int n) {
+        this.sumSq = sumSq;
+        this.sum = sum;
+        this.n = n;
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * <p>The initial result is {@code NaN}.
+     *
+     * @return {@code IntVariance} instance.
+     */
+    public static LongVariance2 create() {
+        return new LongVariance2();
+    }
+
+    /**
+     * Returns an instance populated using the input {@code values}.
+     *
+     * @param values Values.
+     * @return {@code IntVariance} instance.
+     */
+    public static LongVariance2 of(long... values) {
+        // Note: Arrays could be processed using specialised counts knowing the maximum
+        // limit
+        // for an array is 2^31 values. Requires a UInt160.
+
+        final Int128 s = Int128.create();
+        final UInt192 ss = UInt192.create();
+        for (final long x : values) {
+            s.add(x);
+            ss.addSquare2(x);
+        }
+        return new LongVariance2(ss, s, values.length);
+    }
+
+    /**
+     * Updates the state of the statistic to reflect the addition of {@code value}.
+     *
+     * @param value Value.
+     */
+    @Override
+    public void accept(long value) {
+        sumSq.addSquare2(value);
+        sum.add(value);
+        n++;
+    }
+
+    /**
+     * Gets the variance of all input values.
+     *
+     * <p>When no values have been added, the result is {@code NaN}.
+     *
+     * @return variance of all values.
+     */
+    @Override
+    public double getAsDouble() {
+        if (n == 0) {
+            return Double.NaN;
+        }
+        // Avoid a divide by zero
+        if (n == 1) {
+            return 0;
+        }
+        final long n0 = biased ? n : n - 1;
+
+        // Sum-of-squared deviations: sum(x^2) - sum(x)^2 / n
+        // Sum-of-squared deviations precursor: n * sum(x^2) - sum(x)^2
+        // The precursor is computed in integer precision.
+        // The divide uses double precision.
+        // This ensures we avoid cancellation in the difference and use a fast divide.
+        // The result is limited to max 4 ulp by the rounding in the double computation
+        // When n0*n is < 2^53 the max error is reduced to two roundings.
+
+        // Compute the term if possible using fast integer arithmetic.
+        // 192-bit sum(x^2) * n will be OK when the upper 32-bits are zero.
+        // 128-bit sum(x)^2 will be OK when the upper 64-bits are zero.
+        // The first is safe when n < 2^32 but we must check the sum high bits.
+        double diff;
+        if (((n >>> Integer.SIZE) | sum.hi64()) == 0) {
+            diff = sumSq.unsignedMultiply((int) n).subtract(sum.squareLow()).toDouble();
+        } else {
+            diff = sumSq.toBigInteger().multiply(BigInteger.valueOf(n))
+                .subtract(square(sum.toBigInteger())).doubleValue();
+        }
+        // Compute the divide in double precision
+        return diff / IntMath.unsignedMultiplyToDouble(n, n0);
+    }
+
+    /**
+     * Convenience method to square a BigInteger.
+     *
+     * @param x Value
+     * @return x^2
+     */
+    private static BigInteger square(BigInteger x) {
+        return x.multiply(x);
+    }
+
+    /**
+     * Combine with the {@code other} instance.
+     *
+     * @param other Other instance.
+     * @return this instance
+     */
+    public LongVariance2 combine(LongVariance2 other) {
+        sumSq.add(other.sumSq);
+        sum.add(other.sum);
+        n += other.n;
+        return this;
+    }
+
+    /**
+     * Sets the value of the biased flag. The default value is {@code false}.
+     *
+     * <p>If {@code false} the sum of squared deviations from the sample mean is
+     * normalised by {@code n - 1} where {@code n} is the number of samples. This is
+     * Bessel's correction for an unbiased estimator of the variance of a hypothetical
+     * infinite population.
+     *
+     * <p>If {@code true} the sum of squared deviations is normalised by the number of
+     * samples {@code n}.
+     *
+     * <p>Note: This option only applies when {@code n > 1}. The variance of {@code n = 1}
+     * is always 0.
+     *
+     * <p>This flag only controls the final computation of the statistic. The value of
+     * this flag will not affect compatibility between instances during a
+     * {@link #combine(LongVariance2) combine} operation.
+     *
+     * @param v Value.
+     * @return {@code this} instance
+     */
+    public LongVariance2 setBiased(boolean v) {
+        biased = v;
+        return this;
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128.java
new file mode 100644
index 0000000..5f6b5f3
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128.java
@@ -0,0 +1,285 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A 128-bit unsigned integer.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.UInt128} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values
+ * generated by squaring {@code int} values.
+ *
+ * @since 1.1
+ */
+final class UInt128 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Data is stored using integers to allow efficient sum-with-carry addition
+
+    /** low 32-bits. */
+    private int d;
+    /** low 32-bits. */
+    private int c;
+    /** high 64-bits. */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt128() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private UInt128(long x) {
+        d = (int) x;
+        c = (int) (x >>> Integer.SIZE);
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     *
+     * @param hi High 64-bits.
+     * @param mid Middle 32-bits.
+     * @param lo Low 32-bits.
+     */
+    private UInt128(long hi, int mid, int lo) {
+        this.d = lo;
+        this.c = mid;
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 64-bits.
+     */
+    UInt128(long hi, long lo) {
+        this.d = (int) lo;
+        this.c = (int) (lo >>> Integer.SIZE);
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt128 create() {
+        return new UInt128();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     * The value is assumed to be an unsigned 64-bit integer.
+     *
+     * @param x Value (must be positive).
+     * @return the instance
+     */
+    static UInt128 of(long x) {
+        return new UInt128(x);
+    }
+
+    /**
+     * Create an instance of the {@code UInt96} value.
+     *
+     * @param x Value.
+     * @return the instance
+     */
+    static UInt128 of(UInt96 x) {
+        final int lo = x.lo32();
+        final long hi = x.hi64();
+        final UInt128 y = new UInt128();
+        y.d = lo;
+        y.c = (int) hi;
+        y.ab = hi >>> Integer.SIZE;
+        return y;
+    }
+
+    /**
+     * Adds the value in place. It is assumed to be positive, for example the square of an
+     * {@code int} value. However no check is performed for a negative value.
+     *
+     * <p>Note: This addition handles {@value Long#MIN_VALUE} as an unsigned
+     * value of 2^63.
+     *
+     * @param x Value.
+     */
+    void addPositive(long x) {
+        // Sum with carry.
+        // Assuming x is positive then x + lo will not overflow 64-bits
+        // so we do not have to split x into upper and lower 32-bit values.
+        long s = x + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value in-place.
+     *
+     * @param x Value.
+     */
+    void add(UInt128 x) {
+        // Avoid issues adding to itself
+        final int dd = x.d;
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        long s = (dd & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+    /**
+     * Multiply by the unsigned value.
+     * Any overflow bits are lost.
+     *
+     * @param x Value.
+     * @return the product
+     */
+    UInt128 unsignedMultiply(int x) {
+        final long xx = x & MASK32;
+        // Multiply with carry.
+        long product = xx * (d & MASK32);
+        final int dd = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (c & MASK32);
+        final int cc = (int) product;
+        // Possible overflow here and bits are lost
+        final long aabb = (product >>> Integer.SIZE) + xx * ab;
+        return new UInt128(aabb, cc, dd);
+    }
+
+    /**
+     * Subtracts the value.
+     * Any overflow bits (negative result) are lost.
+     *
+     * @param x Value.
+     * @return the difference
+     */
+    UInt128 subtract(UInt128 x) {
+        // Difference with carry.
+        long diff = (d & MASK32) - (x.d & MASK32);
+        final int dd = (int) diff;
+        diff = (diff >> Integer.SIZE) + (c & MASK32) - (x.c & MASK32);
+        final int cc = (int) diff;
+        // Possible overflow here and bits are lost containing info on the
+        // magnitude of the true negative value
+        final long aabb = (diff >> Integer.SIZE) + ab - x.ab;
+        return new UInt128(aabb, cc, dd);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        // Test if we have more than 63-bits
+        if (ab != 0 || c < 0) {
+            ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 4)
+                .putLong(ab)
+                .putInt(c)
+                .putInt(d);
+            return new BigInteger(1, bb.array());
+        }
+        // Create from a long
+        return BigInteger.valueOf(((c & MASK32) << Integer.SIZE) | (d & MASK32));
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Sum low to high
+        return DD.ofSum(d & MASK32, (c & MASK32) * 0x1.0p32)
+            .add((ab & MASK32) * 0x1.0p64)
+            .add((ab >>> Integer.SIZE) * 0x1.0p96);
+    }
+
+    /**
+     * Convert to a {@code double}.
+     *
+     * @return the value
+     */
+    double toDouble() {
+        return IntMath.uin128ToDouble(hi64(), lo64());
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return (d & MASK32) | ((c & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the low 32-bits as an {@code int} value.
+     *
+     * @return bits 32-1
+     */
+    int lo32() {
+        return d;
+    }
+
+    /**
+     * Return the middle 32-bits as an {@code int} value.
+     *
+     * @return bits 64-33
+     */
+    int mid32() {
+        return c;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 128-65
+     */
+    long hi64() {
+        return ab;
+    }
+
+    /**
+     * Return the higher 32-bits as an {@code int} value.
+     *
+     * @return bits 128-97
+     */
+    int hi32() {
+        return (int) (ab >>> Integer.SIZE);
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192.java
new file mode 100644
index 0000000..7dd3de2
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192.java
@@ -0,0 +1,297 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A 192-bit unsigned integer.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.UInt192} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * <p>This is a specialised class to implement an accumulator of squared {@code long} values.
+ *
+ * @since 1.1
+ */
+final class UInt192 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Data is stored using integers to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int f;
+    /** bits 64-33. */
+    private int e;
+    /** bits 96-65. */
+    private int d;
+    /** bits 128-97. */
+    private int c;
+    /** bits 192-129 (high 64-bits). */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt192() {
+        // No-op
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param mid Middle 64-bits.
+     * @param lo Low 64-bits.
+     */
+    UInt192(long hi, long mid, long lo) {
+        this.f = (int) lo;
+        this.e = (int) (lo >>> Integer.SIZE);
+        this.d = (int) mid;
+        this.c = (int) (mid >>> Integer.SIZE);
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     *
+     * @param ab bits 192-129 (high 64-bits).
+     * @param c bits 128-97.
+     * @param d bits 96-65.
+     * @param e bits 64-33.
+     * @param f bits 32-1.
+     */
+    private UInt192(long ab, int c, int d, int e, int f) {
+        this.ab = ab;
+        this.c = c;
+        this.d = d;
+        this.e = e;
+        this.f = f;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt192 create() {
+        return new UInt192();
+    }
+
+    /**
+     * Adds the squared value {@code x * x}.
+     *
+     * @param x Value.
+     */
+    void addSquare(long x) {
+        final long lo = x * x;
+        final long hi = IntMath.squareHigh(x);
+
+        // Sum with carry.
+        long s = (lo & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (lo >>> Integer.SIZE) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (hi & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (hi >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the squared value {@code x * x}.
+     *
+     * <p>This uses {@code java.lang.Math.multiplyHigh(long, long)} and requires Java 11+.
+     *
+     * @param x Value.
+     */
+    void addSquare2(long x) {
+        final long lo = x * x;
+        final long hi = Math.multiplyHigh(x, x);
+
+        // Sum with carry.
+        long s = (lo & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (lo >>> Integer.SIZE) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (hi & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (hi >>> Integer.SIZE) + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(UInt192 x) {
+        // Avoid issues adding to itself
+        final int ff = x.f;
+        final int ee = x.e;
+        final int dd = x.d;
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        long s = (ff & MASK32) + (f & MASK32);
+        f = (int) s;
+        s = (s >>> Integer.SIZE) + (ee & MASK32) + (e & MASK32);
+        e = (int) s;
+        s = (s >>> Integer.SIZE) + (dd & MASK32) + (d & MASK32);
+        d = (int) s;
+        s = (s >>> Integer.SIZE) + (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+
+    /**
+     * Multiply by the unsigned value.
+     * Any overflow bits are lost.
+     *
+     * @param x Value.
+     * @return the product
+     */
+    UInt192 unsignedMultiply(int x) {
+        final long xx = x & MASK32;
+        // Multiply with carry.
+        long product = xx * (f & MASK32);
+        final int ff = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (e & MASK32);
+        final int ee = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (d & MASK32);
+        final int dd = (int) product;
+        product = (product >>> Integer.SIZE) + xx * (c & MASK32);
+        final int cc = (int) product;
+        // Possible overflow here and bits are lost
+        final long aabb = (product >>> Integer.SIZE) + xx * ab;
+        return new UInt192(aabb, cc, dd, ee, ff);
+    }
+
+    /**
+     * Subtracts the value.
+     * Any overflow bits (negative result) are lost.
+     *
+     * @param x Value.
+     * @return the difference
+     */
+    UInt192 subtract(UInt128 x) {
+        // Difference with carry.
+        // Subtract common part.
+        long diff = (f & MASK32) - (x.lo32()  & MASK32);
+        final int ff = (int) diff;
+        diff = (diff >> Integer.SIZE) + (e & MASK32) - (x.mid32() & MASK32);
+        final int ee = (int) diff;
+        diff = (diff >> Integer.SIZE) + (d & MASK32) - (x.hi64() & MASK32);
+        final int dd = (int) diff;
+        diff = (diff >> Integer.SIZE) + (c & MASK32) - (x.hi64() >>> Integer.SIZE);
+        final int cc = (int) diff;
+        // Possible overflow here and bits are lost containing info on the
+        // magnitude of the true negative value
+        final long aabb = (diff >> Integer.SIZE) + ab;
+        return new UInt192(aabb, cc, dd, ee, ff);
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        final ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 6)
+            .putLong(ab)
+            .putInt(c)
+            .putInt(d)
+            .putInt(e)
+            .putInt(f);
+        // Sign is always positive. This works for zero.
+        return new BigInteger(1, bb.array());
+    }
+
+    /**
+     * Convert to a double.
+     *
+     * @return the value
+     */
+    double toDouble() {
+        long h = hi64();
+        long m = mid64();
+        long l = lo64();
+        if (h == 0) {
+            return IntMath.uin128ToDouble(m, l);
+        }
+        // For correct rounding we use a sticky bit to represent magnitude
+        // lost from the low 64-bits. The result is scaled by 2^64.
+        return IntMath.uin128ToDouble(h, m | ((l != 0) ? 1 : 0)) * 0x1.0p64;
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Sum low to high
+        return DD.ofSum(f & MASK32, (e & MASK32) * 0x1.0p32)
+            .add((d & MASK32) * 0x1.0p64)
+            .add((c & MASK32) * 0x1.0p96)
+            .add((ab & MASK32) * 0x1.0p128)
+            .add((ab >>> Integer.SIZE) * 0x1.0p160);
+    }
+
+    /**
+     * Return the lower 64-bits as a {@code long} value.
+     *
+     * @return the low 64-bits
+     */
+    long lo64() {
+        return (f & MASK32) | ((e & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the middle 64-bits as a {@code long} value.
+     *
+     * @return bits 128-65
+     */
+    long mid64() {
+        return (d & MASK32) | ((c & MASK32) << Integer.SIZE);
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 192-129
+     */
+    long hi64() {
+        return ab;
+    }
+
+    /**
+     * Return the higher 32-bits as an {@code int} value.
+     *
+     * @return the high 32-bits
+     */
+    int hi32() {
+        return (int) (ab >>> Integer.SIZE);
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96.java b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96.java
new file mode 100644
index 0000000..d7e1659
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/main/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import org.apache.commons.numbers.core.DD;
+
+/**
+ * A 96-bit unsigned integer.
+ *
+ * <p>This is a copy of {@code o.a.c.statistics.descriptive.Int96} to allow benchmarking.
+ * Additional methods may have been added for comparative benchmarks.
+ *
+ * <p>This is a specialised class to implement an accumulator of {@code long} values
+ * generated by squaring {@code int} values from an array (max observations=2^31).
+ *
+ * @since 1.1
+ */
+final class UInt96 {
+    /** Mask for the lower 32-bits of a long. */
+    private static final long MASK32 = 0xffff_ffffL;
+
+    // Low data is stored using an integer to allow efficient sum-with-carry addition
+
+    /** bits 32-1 (low 32-bits). */
+    private int c;
+    /** bits 96-33. */
+    private long ab;
+
+    /**
+     * Create an instance.
+     */
+    private UInt96() {
+        // No-op
+    }
+
+    /**
+     * Create an instance.
+     *
+     * @param x Value.
+     */
+    private UInt96(long x) {
+        c = (int) x;
+        ab = (int) (x >>> Integer.SIZE);
+    }
+
+    /**
+     * Create an instance using a direct binary representation.
+     * This is package-private for testing.
+     *
+     * @param hi High 64-bits.
+     * @param lo Low 32-bits.
+     */
+    UInt96(long hi, int lo) {
+        this.c = lo;
+        this.ab = hi;
+    }
+
+    /**
+     * Create an instance. The initial value is zero.
+     *
+     * @return the instance
+     */
+    static UInt96 create() {
+        return new UInt96();
+    }
+
+    /**
+     * Create an instance of the {@code long} value.
+     * The value is assumed to be an unsigned 64-bit integer.
+     *
+     * @param x Value (must be positive).
+     * @return the instance
+     */
+    static UInt96 of(long x) {
+        return new UInt96(x);
+    }
+
+    /**
+     * Adds the value. It is assumed to be positive, for example the square of an
+     * {@code int} value. However no check is performed for a negative value.
+     *
+     * <p>Note: This addition handles {@value Long#MIN_VALUE} as an unsigned
+     * value of 2^63.
+     *
+     * @param x Value.
+     */
+    void addPositive(long x) {
+        // Sum with carry.
+        // Assuming x is positive then x + lo will not overflow 64-bits
+        // so we do not have to split x into upper and lower 32-bit values.
+        final long s = x + (c & MASK32);
+        c = (int) s;
+        ab += s >>> Integer.SIZE;
+    }
+
+    /**
+     * Adds the value.
+     *
+     * @param x Value.
+     */
+    void add(UInt96 x) {
+        // Avoid issues adding to itself
+        final int cc = x.c;
+        final long aabb = x.ab;
+        // Sum with carry.
+        final long s = (cc & MASK32) + (c & MASK32);
+        c = (int) s;
+        ab += (s >>> Integer.SIZE) + aabb;
+    }
+
+    /**
+     * Convert to a BigInteger.
+     *
+     * @return the value
+     */
+    BigInteger toBigInteger() {
+        if (ab != 0) {
+            final ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES * 3)
+                .putLong(ab)
+                .putInt(c);
+            return new BigInteger(1, bb.array());
+        }
+        return BigInteger.valueOf(c & MASK32);
+    }
+
+    /**
+     * Convert to a double-double.
+     *
+     * @return the value
+     */
+    DD toDD() {
+        // Sum low to high
+        return DD.ofSum(c & MASK32, (ab & MASK32) * 0x1.0p32)
+            .add((ab >>> Integer.SIZE) * 0x1.0p64);
+    }
+
+    /**
+     * Return the lower 32-bits as an {@code int} value.
+     *
+     * @return bits 32-1
+     */
+    int lo32() {
+        return c;
+    }
+
+    /**
+     * Return the higher 64-bits as a {@code long} value.
+     *
+     * @return bits 96-33
+     */
+    long hi64() {
+        return ab;
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128Test.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128Test.java
new file mode 100644
index 0000000..2e490f0
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/Int128Test.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.LongStream;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link Int128}.
+ */
+class Int128Test {
+    private static final BigInteger TWO_POW_128 = BigInteger.ONE.shiftLeft(128);
+    private static final BigInteger TWO_POW_127 = BigInteger.ONE.shiftLeft(127);
+    private static final BigInteger MINUS_TWO_POW_127 = BigInteger.ONE.shiftLeft(127).negate();
+
+    @Test
+    void testCreate() {
+        final Int128 v = Int128.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"testAddLong"})
+    void testToBigInteger(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).shiftLeft(64).add(BigInteger.valueOf(b));
+        final Int128 v = new Int128(a, b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final Int128 v = Int128.of(a);
+        v.add(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"testAddLong"})
+    void testAddLong2(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final Int128 v = Int128.of(a);
+        v.add2(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, 2, Long.MIN_VALUE, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+                builder.accept(Arguments.of(i, -j));
+                builder.accept(Arguments.of(-i, j));
+                builder.accept(Arguments.of(-i, -j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final Int128 v = Int128.create();
+        for (final long x : a) {
+            v.add(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), v.toDD(), 0x1.0p-106, "DD");
+    }
+
+    @ParameterizedTest
+    @MethodSource(value = {"testAddLongs"})
+    void testAddLongs2(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final Int128 v = Int128.create();
+        for (final long x : a) {
+            v.add2(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> -(x >>> 2)).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, long b, long c, long d) {
+        final Int128 x = new Int128(a, b);
+        final Int128 y = new Int128(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo64());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The Int128 result is a signed 128-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [2^-127, 2^127).
+        // Since the overflow will be at most 1-bit we can wrap the value
+        // using +/- 2^128.
+        if (expected.compareTo(TWO_POW_127) >= 0) {
+            // too high
+            expected = expected.subtract(TWO_POW_128);
+        } else if (expected.compareTo(MINUS_TWO_POW_127) < 0) {
+            // too low
+            expected = expected.add(TWO_POW_128);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), x.toDD(), 0x1.0p-106, "DD");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.compareTo(TWO_POW_127) >= 0) {
+            // too high
+            expected = expected.subtract(TWO_POW_128);
+        } else if (expected.compareTo(MINUS_TWO_POW_127) < 0) {
+            // too low
+            expected = expected.add(TWO_POW_128);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 1, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSquareLow(long a) {
+        final BigInteger expected = BigInteger.valueOf(a).pow(2);
+        final UInt128 v = Int128.of(a).squareLow();
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static LongStream testSquareLow() {
+        final LongStream.Builder builder = LongStream.builder();
+        final long[] x = {0, 1, Long.MIN_VALUE, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            builder.accept(i);
+            builder.accept(-i);
+        }
+        RandomSource.XO_RO_SHI_RO_128_PP.create().longs(20).forEach(builder);
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMathTest.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMathTest.java
new file mode 100644
index 0000000..d1bc9cf
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/IntMathTest.java
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigInteger;
+import java.util.stream.LongStream;
+import java.util.stream.LongStream.Builder;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link IntMath}.
+ */
+class IntMathTest {
+    /** 2^63. */
+    private static final BigInteger TWO_POW_63 = BigInteger.ONE.shiftLeft(63);
+
+    @ParameterizedTest
+    @MethodSource
+    void testSquareHigh(long a) {
+        final long actual = IntMath.squareHigh(a);
+        final long expected = BigInteger.valueOf(a).pow(2).shiftRight(64).longValue();
+        Assertions.assertEquals(expected, actual);
+    }
+
+    static LongStream testSquareHigh() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Builder builder = LongStream.builder();
+        builder.accept(0);
+        builder.accept(Long.MAX_VALUE);
+        builder.accept(Long.MIN_VALUE);
+        rng.ints(5).forEach(builder::accept);
+        rng.longs(50).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 1).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 2).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 5).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 13).forEach(builder::accept);
+        rng.longs(10).map(x -> x >>> 35).forEach(builder::accept);
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testUnsignedMultiplyHigh(long a, long b) {
+        final long actual = IntMath.unsignedMultiplyHigh(a, b);
+        final BigInteger bi1 = toUnsignedBigInteger(a);
+        final BigInteger bi2 = toUnsignedBigInteger(b);
+        final BigInteger expected = bi1.multiply(bi2);
+        Assertions.assertEquals(expected.shiftRight(Long.SIZE).longValue(), actual,
+            () -> String.format("%s * %s", bi1, bi2));
+        final double x = expected.doubleValue();
+        Assertions.assertEquals(x, IntMath.unsignedMultiplyToDoubleBigInteger(a, b),
+            () -> String.format("double %s * %s", bi1, bi2));
+        Assertions.assertEquals(x, IntMath.unsignedMultiplyToDouble(a, b),
+            () -> String.format("double2 %s * %s", bi1, bi2));
+    }
+
+    static Stream<Arguments> testUnsignedMultiplyHigh() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] values = {
+            -1, 0, 1, Long.MAX_VALUE, Long.MIN_VALUE,
+            0xffL, 0xff00L, 0xff0000L, 0xff000000L,
+            0xff00000000L, 0xff0000000000L, 0xff000000000000L, 0xff000000000000L,
+            0xffffL, 0xffff0000L, 0xffff00000000L, 0xffff000000000000L,
+            0xffffffffL, 0xffffffff00000000L
+        };
+        for (final long v1 : values) {
+            for (final long v2 : values) {
+                builder.accept(Arguments.of(v1, v2));
+                builder.accept(Arguments.of(v1 >>> 15, v2 >>> 18));
+            }
+        }
+        for (int i = 0; i < 200; i++) {
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    /**
+     * Create a big integer treating the value as unsigned.
+     *
+     * @param v Value
+     * @return the big integer
+     */
+    private static BigInteger toUnsignedBigInteger(long v) {
+        return v < 0 ?
+            TWO_POW_63.add(BigInteger.valueOf(v & Long.MAX_VALUE)) :
+            BigInteger.valueOf(v);
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testUin128ToDouble(long a, long b) {
+        final BigInteger bi1 = toUnsignedBigInteger(a).shiftLeft(Long.SIZE);
+        final BigInteger bi2 = toUnsignedBigInteger(b);
+        final double x = bi1.add(bi2).doubleValue();
+        Assertions.assertEquals(x, IntMath.uin128ToDouble(a, b),
+            () -> String.format("%s + %s", a, b));
+    }
+
+    static Stream<Arguments> testUin128ToDouble() {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        for (int i = 0; i < 100; i++) {
+            long a = rng.nextLong();
+            long b = rng.nextLong();
+            builder.accept(Arguments.of(a, b));
+            builder.accept(Arguments.of(0, b));
+            // Edge cases where trailing bits are required for rounding.
+            // Create a 55-bit number. Ensure the highest bit is set.
+            a = (a << 9) | Long.MIN_VALUE;
+            // Shift right and carry bits down.
+            int shift = rng.nextInt(1, 64);
+            long c = a >>> shift;
+            long d = a << -shift;
+            // Check
+            Assertions.assertEquals(Long.bitCount(a), Long.bitCount(c) + Long.bitCount(d));
+            builder.accept(Arguments.of(c, d));
+            // Add a trailing bit that may change rounding
+            builder.accept(Arguments.of(c, d | 1));
+        }
+        // At least one case where the trailing bit does effect rounding
+        // 54-bits all set is an odd number + 0.5
+        builder.accept(Arguments.of(1, (1L << 11)));
+        builder.accept(Arguments.of(1, (1L << 11) | 1));
+        // Unset the second to last bit and repeat above is an even number + 0.5
+        builder.accept(Arguments.of(1, ((1L & ~0x2) << 11)));
+        builder.accept(Arguments.of(1, ((1L & ~0x2) << 11) | 1));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVarianceTest.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVarianceTest.java
new file mode 100644
index 0000000..6d4fa21
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/LongVarianceTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.apache.commons.statistics.descriptive.LongVariance;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Test for {@link LongVariance2}. Tested against {@link LongVariance}.
+ */
+class LongVarianceTest {
+    @ParameterizedTest
+    @ValueSource(ints = {0, 1, 2, 5, 10, 13, 1000})
+    void testVariance(int n) {
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final long[] values = rng.longs(n).toArray();
+        final LongVariance v1 = LongVariance.of(values);
+        final LongVariance2 v2 = LongVariance2.of(values);
+        double variance = v1.getAsDouble();
+        final double actual = v1.getAsDouble();
+        Assertions.assertEquals(variance, actual, "Variance");
+
+        if (n > 1) {
+            variance = v1.setBiased(true).getAsDouble();
+            Assertions.assertEquals(variance, v2.setBiased(true).getAsDouble(), "Variance biased");
+        }
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/TestUtils.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/TestUtils.java
new file mode 100644
index 0000000..7144a6a
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/TestUtils.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.util.function.Supplier;
+import org.apache.commons.numbers.core.DD;
+import org.junit.jupiter.api.Assertions;
+
+/**
+ * Test utilities.
+ */
+final class TestUtils {
+    /** No instances. */
+    private TestUtils() {}
+
+    // DD equality checks adapted from o.a.c.numbers.core.TestUtils
+
+    /**
+     * Assert the two numbers are equal within the provided relative error.
+     *
+     * <p>The provided error is relative to the exact result in expected: (e - a) / e.
+     * If expected is zero this division is undefined. In this case the actual must be zero
+     * (no absolute tolerance is supported). The reporting of the error uses the absolute
+     * error and the return value of the relative error is 0. Cases of complete cancellation
+     * should be avoided for benchmarking relative accuracy.
+     *
+     * <p>Note that the actual double-double result is not validated using the high and low
+     * parts individually. These are summed and compared to the expected.
+     *
+     * <p>Set {@code eps} to negative to report the relative error to the stdout and
+     * ignore failures.
+     *
+     * <p>The relative error is signed. The sign of the error
+     * is the same as that returned from Double.compare(actual, expected); it is
+     * computed using {@code actual - expected}.
+     *
+     * @param expected expected value
+     * @param actual actual value
+     * @param eps maximum relative error between the two values
+     * @param msg failure message
+     * @return relative error difference between the values (signed)
+     * @throws NumberFormatException if {@code actual} contains non-finite values
+     */
+    static double assertEquals(BigDecimal expected, DD actual, double eps, String msg) {
+        return assertEquals(expected, actual, eps, () -> msg);
+    }
+
+    /**
+     * Assert the two numbers are equal within the provided relative error.
+     *
+     * <p>The provided error is relative to the exact result in expected: (e - a) / e.
+     * If expected is zero this division is undefined. In this case the actual must be zero
+     * (no absolute tolerance is supported). The reporting of the error uses the absolute
+     * error and the return value of the relative error is 0. Cases of complete cancellation
+     * should be avoided for benchmarking relative accuracy.
+     *
+     * <p>Note that the actual double-double result is not validated using the high and low
+     * parts individually. These are summed and compared to the expected.
+     *
+     * <p>Set {@code eps} to negative to report the relative error to the stdout and
+     * ignore failures.
+     *
+     * <p>The relative error is signed. The sign of the error
+     * is the same as that returned from Double.compare(actual, expected); it is
+     * computed using {@code actual - expected}.
+     *
+     * @param expected expected value
+     * @param actual actual value
+     * @param eps maximum relative error between the two values
+     * @param msg failure message
+     * @return relative error difference between the values (signed)
+     * @throws NumberFormatException if {@code actual} contains non-finite values
+     */
+    static double assertEquals(BigDecimal expected, DD actual, double eps, Supplier<String> msg) {
+        // actual - expected
+        final BigDecimal delta = new BigDecimal(actual.hi())
+            .add(new BigDecimal(actual.lo()))
+            .subtract(expected);
+        boolean equal;
+        if (expected.compareTo(BigDecimal.ZERO) == 0) {
+            // Edge case. Currently an absolute tolerance is not supported as summation
+            // to zero cases generated in testing all pass.
+            equal = actual.doubleValue() == 0;
+
+            // DEBUG:
+            if (eps < 0) {
+                if (!equal) {
+                    printf("%sexpected 0 != actual <%s + %s> (abs.error=%s)%n",
+                        prefix(msg), actual.hi(), actual.lo(), delta.doubleValue());
+                }
+            } else if (!equal) {
+                Assertions.fail(String.format("%sexpected 0 != actual <%s + %s> (abs.error=%s)",
+                    prefix(msg), actual.hi(), actual.lo(), delta.doubleValue()));
+            }
+
+            return 0;
+        }
+
+        final double rel = delta.divide(expected, MathContext.DECIMAL128).doubleValue();
+        // Allow input of a negative maximum ULPs
+        equal = Math.abs(rel) <= Math.abs(eps);
+
+        // DEBUG:
+        if (eps < 0) {
+            if (!equal) {
+                printf("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))%n",
+                    prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(),
+                    rel, Math.abs(rel) / eps);
+            }
+        } else if (!equal) {
+            Assertions.fail(String.format("%sexpected <%s> != actual <%s + %s> (rel.error=%s (%.3f x tol))",
+                prefix(msg), expected.round(MathContext.DECIMAL128), actual.hi(), actual.lo(),
+                rel, Math.abs(rel) / eps));
+        }
+
+        return rel;
+    }
+
+    /**
+     * Print a formatted message to stdout.
+     * Provides a single point to disable checkstyle warnings on print statements and
+     * enable/disable all print debugging.
+     *
+     * @param format Format string.
+     * @param args Arguments.
+     */
+    static void printf(String format, Object... args) {
+        // CHECKSTYLE: stop regex
+        System.out.printf(format, args);
+        // CHECKSTYLE: resume regex
+    }
+
+    /**
+     * Get the prefix for the message.
+     *
+     * @param msg Message supplier
+     * @return the prefix
+     */
+    static String prefix(Supplier<String> msg) {
+        return msg == null ? "" : msg.get() + ": ";
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128Test.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128Test.java
new file mode 100644
index 0000000..db002c5
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt128Test.java
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link UInt128}.
+ */
+class UInt128Test {
+    @Test
+    void testCreate() {
+        final UInt128 v = UInt128.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @Test
+    void testAddLongMinValue() {
+        final UInt128 v = UInt128.of(1268361283468345237L);
+        final BigInteger x = BigInteger.ONE.shiftLeft(63);
+        BigInteger expected = v.toBigInteger();
+        for (int i = 1; i <= 5; i++) {
+            // Accepts a negative value without exception. This is
+            // computed correctly if the current low 32 bits
+            // added to the argument do not overflow. This is always
+            // true for min value as all lower 32-bits are zero.
+            v.addPositive(Long.MIN_VALUE);
+            expected = expected.add(x);
+            Assertions.assertEquals(expected, v.toBigInteger());
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final UInt128 v = UInt128.of(a);
+        v.addPositive(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt128 v = UInt128.create();
+        for (final long x : a) {
+            Assertions.assertFalse(x < 0, "Value must be positive");
+            v.addPositive(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), v.toDD(), 0x1.0p-106, "DD");
+        Assertions.assertEquals(expected.doubleValue(), v.toDouble(), "double");
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, long b, long c, long d) {
+        final UInt128 x = new UInt128(a, b);
+        final UInt128 y = new UInt128(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo64());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 128-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^128).
+        if (expected.testBit(128)) {
+            expected = expected.flipBit(128);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), x.toDD(), 0x1.0p-106, "DD");
+        Assertions.assertEquals(expected.doubleValue(), x.toDouble(), "double");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(128)) {
+            expected = expected.flipBit(128);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong() >>> 1, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong() >>> 2, rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testOfInt96(long a, int b) {
+        final UInt96 x = new UInt96(a, b);
+        final UInt128 y = UInt128.of(x);
+        Assertions.assertEquals(x.toBigInteger(), y.toBigInteger());
+    }
+
+    static Stream<Arguments> testOfInt96() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final int b = rng.nextInt();
+            builder.accept(Arguments.of(a, b));
+            builder.accept(Arguments.of(0, b));
+            builder.accept(Arguments.of(a, 0));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testMultiplyInt(long a, long b, int n) {
+        assertMultiplyInt(a, b, n);
+        assertMultiplyInt(a >>> 32, b, n);
+        assertMultiplyInt(0, b, n);
+    }
+
+    private static void assertMultiplyInt(long a, long b, int n) {
+        final UInt128 v = new UInt128(a, b);
+        BigInteger expected = v.toBigInteger().multiply(BigInteger.valueOf(n & 0xffff_ffffL));
+        // Clip to 128-bits. Only required if the upper 32-bits are non-zero.
+        final int len = expected.bitLength();
+        if (len > 128 && v.hi32() != 0) {
+            expected = expected.subtract(expected.shiftRight(128).shiftLeft(128));
+        }
+        Assertions.assertEquals(expected, v.unsignedMultiply(n).toBigInteger());
+    }
+
+    static Stream<Arguments> testMultiplyInt() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final int[] x = {0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE};
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            for (final int n : x) {
+                builder.accept(Arguments.of(a, b, n));
+            }
+            for (int j = 0; j < 5; j++) {
+                builder.accept(Arguments.of(a, b, rng.nextInt()));
+            }
+        }
+        builder.accept(Arguments.of(-1L >>> 32, -1L, -1));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSubtract(long a, long b, long c, long d) {
+        assertSubtract(a, b, c, d);
+        assertSubtract(c, d, a, b);
+    }
+
+    private static void assertSubtract(long a, long b, long c, long d) {
+        final UInt128 x = new UInt128(a, b);
+        final UInt128 y = new UInt128(c, d);
+        BigInteger expected = x.toBigInteger().subtract(y.toBigInteger());
+        if (expected.signum() < 0) {
+            expected = expected.add(BigInteger.ONE.shiftLeft(128));
+        }
+        Assertions.assertEquals(expected, x.subtract(y).toBigInteger());
+    }
+
+    static Stream<Arguments> testSubtract() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            final long d = rng.nextLong();
+            builder.accept(Arguments.of(a, b, c, d));
+            builder.accept(Arguments.of(0, 0, c, d));
+            builder.accept(Arguments.of(-1L, -1L, c, d));
+        }
+        builder.accept(Arguments.of(-1L, -1L, -1L, -1L));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192Test.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192Test.java
new file mode 100644
index 0000000..7f8d124
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt192Test.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link UInt192}.
+ */
+class UInt192Test {
+    @Test
+    void testCreate() {
+        final UInt192 v = UInt192.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddSquareLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).pow(2)
+            .add(BigInteger.valueOf(b).pow(2));
+        final UInt192 v = UInt192.create();
+        v.addSquare(a);
+        v.addSquare(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddSquareLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 61278342166787978L, 42, 8652939272947492397L};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddSquareLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .map(x -> x.pow(2))
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt192 v = UInt192.create();
+        for (final long x : a) {
+            v.addSquare(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), v.toDD(), 0x1.0p-106, "DD");
+        Assertions.assertEquals(expected.doubleValue(), v.toDouble(), "double");
+    }
+
+    static Stream<Arguments> testAddSquareLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt192(long a, long b, long c, long d, long e, long f) {
+        final UInt192 x = new UInt192(a, b, c);
+        final UInt192 y = new UInt192(d, e, f);
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 192-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^192).
+        if (expected.testBit(192)) {
+            expected = expected.flipBit(192);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d, %d) + (%d, %d, %d)", a, b, c, d, e, f));
+        // Check floating-point representation
+        TestUtils.assertEquals(new BigDecimal(expected), x.toDD(), 0x1.0p-106, "DD");
+        Assertions.assertEquals(expected.doubleValue(), x.toDouble(), "double");
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(192)) {
+            expected = expected.flipBit(192);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d, %d) self-addition", d, e, f));
+    }
+
+    static Stream<Arguments> testAddInt192() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong() >>> 2, rng.nextLong(), rng.nextLong()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextLong(), rng.nextLong(),
+                                        rng.nextLong(), rng.nextLong(), rng.nextLong()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testMultiplyInt(long a, long b, long c, int n) {
+        assertMultiplyInt(a, b, c, n);
+        assertMultiplyInt(a >>> 32, b, c, n);
+        assertMultiplyInt(0, b, c, n);
+    }
+
+    private static void assertMultiplyInt(long a, long b, long c, int n) {
+        final UInt192 v = new UInt192(a, b, c);
+        BigInteger expected = v.toBigInteger().multiply(BigInteger.valueOf(n & 0xffff_ffffL));
+        // Clip to 192-bits. Only required if the upper 32-bits are non-zero.
+        final int len = expected.bitLength();
+        if (len > 192 && v.hi32() != 0) {
+            expected = expected.subtract(expected.shiftRight(192).shiftLeft(192));
+        }
+        Assertions.assertEquals(expected, v.unsignedMultiply(n).toBigInteger());
+    }
+
+    static Stream<Arguments> testMultiplyInt() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        final int[] x = {0, 1, -1, Integer.MAX_VALUE, Integer.MIN_VALUE};
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            for (final int n : x) {
+                builder.accept(Arguments.of(a, b, c, n));
+            }
+            for (int j = 0; j < 5; j++) {
+                builder.accept(Arguments.of(a, b, c, rng.nextInt()));
+            }
+        }
+        builder.accept(Arguments.of(-1L >>> 32, -1L, -1L, -1));
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testSubtract(long a, long b, long c, long d, long e) {
+        assertSubtract(a, b, c, d, e);
+    }
+
+    private static void assertSubtract(long a, long b, long c, long d, long e) {
+        final UInt192 x = new UInt192(a, b, c);
+        final UInt128 y = new UInt128(d, e);
+        BigInteger expected = x.toBigInteger().subtract(y.toBigInteger());
+        if (expected.signum() < 0) {
+            expected = expected.add(BigInteger.ONE.shiftLeft(192));
+        }
+        Assertions.assertEquals(expected, x.subtract(y).toBigInteger());
+    }
+
+    static Stream<Arguments> testSubtract() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            final long a = rng.nextLong();
+            final long b = rng.nextLong();
+            final long c = rng.nextLong();
+            final long d = rng.nextLong();
+            final long e = rng.nextLong();
+            builder.accept(Arguments.of(a, b, c, d, e));
+            builder.accept(Arguments.of(0, 0, 0, d, e));
+            builder.accept(Arguments.of(-1L, -1L, -1L, d, e));
+        }
+        builder.accept(Arguments.of(-1L, -1L, -1L, -1L, -1L));
+        return builder.build();
+    }
+}
diff --git a/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96Test.java b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96Test.java
new file mode 100644
index 0000000..57b142c
--- /dev/null
+++ b/commons-statistics-examples/examples-jmh/src/test/java/org/apache/commons/statistics/examples/jmh/descriptive/UInt96Test.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.statistics.examples.jmh.descriptive;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Arrays;
+import java.util.stream.Stream;
+import org.apache.commons.numbers.core.DD;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.simple.RandomSource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Test for {@link UInt96}.
+ */
+class UInt96Test {
+    @Test
+    void testCreate() {
+        final UInt96 v = UInt96.create();
+        Assertions.assertEquals(BigInteger.ZERO, v.toBigInteger());
+    }
+
+    @Test
+    void testAddLongMinValue() {
+        final UInt96 v = UInt96.of(5675757768682342956L);
+        final BigInteger x = BigInteger.ONE.shiftLeft(63);
+        BigInteger expected = v.toBigInteger();
+        for (int i = 1; i <= 5; i++) {
+            // Accepts a negative value without exception. This is
+            // computed correctly if the current low 32 bits
+            // added to the argument do not overflow. This is always
+            // true for min value as all lower 32-bits are zero.
+            v.addPositive(Long.MIN_VALUE);
+            expected = expected.add(x);
+            Assertions.assertEquals(expected, v.toBigInteger());
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLong(long a, long b) {
+        final BigInteger expected = BigInteger.valueOf(a).add(BigInteger.valueOf(b));
+        final UInt96 v = UInt96.of(a);
+        v.addPositive(b);
+        Assertions.assertEquals(expected, v.toBigInteger());
+    }
+
+    static Stream<Arguments> testAddLong() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final long[] x = {0, 1, Long.MAX_VALUE, 612783421678L, 42};
+        for (final long i : x) {
+            for (final long j : x) {
+                builder.accept(Arguments.of(i, j));
+            }
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddLongs(long[] a) {
+        final BigInteger expected = Arrays.stream(a).mapToObj(BigInteger::valueOf)
+            .reduce(BigInteger::add).orElse(BigInteger.ZERO);
+        final UInt96 v = UInt96.create();
+        for (final long x : a) {
+            Assertions.assertFalse(x < 0, "Value must be positive");
+            v.addPositive(x);
+        }
+        Assertions.assertEquals(expected, v.toBigInteger());
+        // Check floating-point representation
+        Assertions.assertEquals(
+            DD.from(new BigDecimal(expected)),
+            v.toDD());
+    }
+
+    static Stream<Arguments> testAddLongs() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (final int n : new int[] {50, 100}) {
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 1).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 2).toArray()));
+            builder.accept(Arguments.of(rng.longs(n).map(x -> x >>> 4).toArray()));
+        }
+        return builder.build();
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testAddInt128(long a, int b, long c, int d) {
+        final UInt96 x = new UInt96(a, b);
+        final UInt96 y = new UInt96(c, d);
+        Assertions.assertEquals(a, x.hi64());
+        Assertions.assertEquals(b, x.lo32());
+        BigInteger expected = x.toBigInteger().add(y.toBigInteger());
+        // The result is an unsigned 96-bit integer.
+        // This is subject to integer overflow.
+        // Clip the unlimited BigInteger result to the range [0, 2^96).
+        if (expected.testBit(96)) {
+            expected = expected.flipBit(96);
+        }
+        x.add(y);
+        Assertions.assertEquals(expected, x.toBigInteger(),
+            () -> String.format("(%d, %d) + (%d, %d)", a, b, c, d));
+        // Check floating-point representation
+        Assertions.assertEquals(
+            DD.from(new BigDecimal(expected)),
+            x.toDD());
+        // Check self-addition
+        expected = y.toBigInteger();
+        expected = expected.add(expected);
+        if (expected.testBit(96)) {
+            expected = expected.flipBit(96);
+        }
+        y.add(y);
+        Assertions.assertEquals(expected, y.toBigInteger(),
+            () -> String.format("(%d, %d) self-addition", c, d));
+    }
+
+    static Stream<Arguments> testAddInt128() {
+        final Stream.Builder<Arguments> builder = Stream.builder();
+        final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+        for (int i = 0; i < 50; i++) {
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextInt(), rng.nextLong() >>> 2, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 2, rng.nextInt(), rng.nextLong() >>> 1, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong() >>> 1, rng.nextInt(), rng.nextLong() >>> 2, rng.nextInt()));
+            builder.accept(Arguments.of(rng.nextLong(), rng.nextInt(), rng.nextLong(), rng.nextInt()));
+        }
+        return builder.build();
+    }
+}
diff --git a/src/conf/checkstyle/checkstyle-suppressions.xml b/src/conf/checkstyle/checkstyle-suppressions.xml
index 8216ef6..c363f46 100644
--- a/src/conf/checkstyle/checkstyle-suppressions.xml
+++ b/src/conf/checkstyle/checkstyle-suppressions.xml
@@ -41,4 +41,5 @@
   <suppress checks="MethodLength" files=".*[/\\]WilcoxonSignedRankTestTest.java" />
   <suppress checks="IllegalCatch" files=".*[/\\]TestHelper.java" lines="390-450" />
   <suppress checks="IllegalCatch" files=".*[/\\]BaseStatisticTest.java" lines="280-400" />
+  <suppress checks="IllegalCatch" files=".*[/\\]IntMathTest.java" lines="165-175" />
 </suppressions>