Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiply by power of 5 and then shift left #73

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
import java.math.BigInteger;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.TreeMap;

class FastIntegerMath {

private static final int USE_POWER_OF_FIVE_AND_SHIFT_MIN_THRESHOLD = 800; // stated by benchmark results

private static final int USE_POWER_OF_FIVE_AND_SHIFT_MAX_THRESHOLD = FftMultiplier.FFT_THRESHOLD;

public static final BigInteger FIVE = BigInteger.valueOf(5);
final static BigInteger TEN_POW_16 = BigInteger.valueOf(10_000_000_000_000_000L);
final static BigInteger FIVE_POW_16 = BigInteger.valueOf(152_587_890_625L);
Expand Down Expand Up @@ -90,6 +96,22 @@ static NavigableMap<Integer, BigInteger> createPowersOfTenFloor16Map() {
powersOfTen.put(16, TEN_POW_16);
return powersOfTen;
}
static boolean usePowerOfFiveAndShift(int n) {
return n > USE_POWER_OF_FIVE_AND_SHIFT_MIN_THRESHOLD && n < USE_POWER_OF_FIVE_AND_SHIFT_MAX_THRESHOLD;
}

static Map<Integer, BigInteger> createPowersOfFive(Map<Integer, BigInteger> powersOfTen) {
Map<Integer, BigInteger> powersOfFive = new TreeMap<>();
for (Entry<Integer, BigInteger> entry : powersOfTen.entrySet()) {
int exponent = entry.getKey();
if (usePowerOfFiveAndShift(exponent)) {
BigInteger powerOfTen = entry.getValue();
BigInteger powerOfFive = powerOfTen.shiftRight(exponent);
powersOfFive.put(exponent, powerOfFive);
}
}
return powersOfFive;
}

public static long estimateNumBits(long numDecimalDigits) {
// For the decimal number 10 we need log_2(10) = 3.3219 bits.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class FftMultiplier {
* the mag arrays is greater than this threshold, then FFT
* multiplication will be used.
*/
private static final int FFT_THRESHOLD = 33220;
static final int FFT_THRESHOLD = 33220;
/**
* This constant limits {@code mag.length} of BigIntegers to the supported
* range.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Map;
import java.util.NavigableMap;

import static ch.randelshofer.fastdoubleparser.FastIntegerMath.*;
Expand Down Expand Up @@ -271,6 +272,7 @@ private BigDecimal valueOfBigDecimalString(char[] str, int integerPartIndex, int
int fractionDigitsCount = exponentIndicatorIndex - nonZeroFractionalPartIndex;
int integerDigitsCount = decimalPointIndex - integerPartIndex;
NavigableMap<Integer, BigInteger> powersOfTen = null;
Map<Integer, BigInteger> powersOfFive;

// Parse the significand
// ---------------------
Expand All @@ -283,9 +285,10 @@ private BigDecimal valueOfBigDecimalString(char[] str, int integerPartIndex, int
if (integerDigitsCount > RECURSION_THRESHOLD) {
powersOfTen = createPowersOfTenFloor16Map();
fillPowersOfNFloor16Recursive(powersOfTen, integerPartIndex, decimalPointIndex);
integerPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, integerPartIndex, decimalPointIndex, powersOfTen);
powersOfFive = createPowersOfFive(powersOfTen);
integerPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, integerPartIndex, decimalPointIndex, powersOfTen, powersOfFive);
} else {
integerPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, integerPartIndex, decimalPointIndex, null);
integerPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, integerPartIndex, decimalPointIndex, null, null);
}
} else {
integerPart = BigInteger.ZERO;
Expand All @@ -300,9 +303,10 @@ private BigDecimal valueOfBigDecimalString(char[] str, int integerPartIndex, int
powersOfTen = createPowersOfTenFloor16Map();
}
fillPowersOfNFloor16Recursive(powersOfTen, decimalPointIndex + 1, exponentIndicatorIndex);
fractionalPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, decimalPointIndex + 1, exponentIndicatorIndex, powersOfTen);
powersOfFive = createPowersOfFive(powersOfTen);
fractionalPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, decimalPointIndex + 1, exponentIndicatorIndex, powersOfTen, powersOfFive);
} else {
fractionalPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, decimalPointIndex + 1, exponentIndicatorIndex, null);
fractionalPart = ParseDigitsTaskCharArray.parseDigitsRecursive(str, decimalPointIndex + 1, exponentIndicatorIndex, null, null);
}
// If the integer part is not 0, we combine it with the fraction part.
if (integerPart.signum() == 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.math.BigInteger;
import java.util.Map;

import static ch.randelshofer.fastdoubleparser.FastIntegerMath.createPowersOfFive;
import static ch.randelshofer.fastdoubleparser.FastIntegerMath.fillPowersOf10Floor16;

class JavaBigIntegerFromCharArray extends AbstractBigIntegerParser {
Expand Down Expand Up @@ -111,7 +112,8 @@ private BigInteger parseManyDecDigits(char[] str, int from, int to, boolean isNe
int numDigits = to - from;
checkDecBigIntegerBounds(numDigits);
Map<Integer, BigInteger> powersOfTen = fillPowersOf10Floor16(from, to);
BigInteger result = ParseDigitsTaskCharArray.parseDigitsRecursive(str, from, to, powersOfTen);
Map<Integer, BigInteger> powersOfFive = createPowersOfFive(powersOfTen);
BigInteger result = ParseDigitsTaskCharArray.parseDigitsRecursive(str, from, to, powersOfTen, powersOfFive);
return isNegative ? result.negate() : result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ static BigInteger parseDigitsIterative(char[] str, int from, int to) {
* We achieve better performance by performing multiplications of long bit sequences
* in the frequencey domain.
*/
static BigInteger parseDigitsRecursive(char[] str, int from, int to, Map<Integer, BigInteger> powersOfTen) {
static BigInteger parseDigitsRecursive(char[] str, int from, int to, Map<Integer, BigInteger> powersOfTen, Map<Integer, BigInteger> powersOfFive) {
int numDigits = to - from;

// Base case: Short sequences can be parsed iteratively.
Expand All @@ -76,10 +76,15 @@ static BigInteger parseDigitsRecursive(char[] str, int from, int to, Map<Integer

// Recursion case: Split large sequences up into two parts. The lower part is a multiple of 16 digits.
int mid = splitFloor16(from, to);
BigInteger high = parseDigitsRecursive(str, from, mid, powersOfTen);
BigInteger low = parseDigitsRecursive(str, mid, to, powersOfTen);
BigInteger high = parseDigitsRecursive(str, from, mid, powersOfTen, powersOfFive);
BigInteger low = parseDigitsRecursive(str, mid, to, powersOfTen, powersOfFive);

high = FftMultiplier.multiply(high, powersOfTen.get(to - mid));
int n = to - mid;
if (FastIntegerMath.usePowerOfFiveAndShift(n)) {
high = FftMultiplier.multiply(high, powersOfFive.get(n)).shiftLeft(n);
} else {
high = FftMultiplier.multiply(high, powersOfTen.get(n));
}
return low.add(high);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private ParseDigitsTaskCharSequence() {
* recursive algorithm. We speculate that we break even somewhere at twice
* the threshold value.
*/
public static final int RECURSION_THRESHOLD = 400;
public static int RECURSION_THRESHOLD = 400;


/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* @(#)JmhMultiplyByFftVsByBigInteger.java
* Copyright © 2023 Werner Randelshofer, Switzerland. MIT License.
*/
package ch.randelshofer.fastdoubleparser;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.math.BigInteger;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* Benchmarks for selected floating point strings.
* <pre>
* # JMH version: 1.36
* # VM version: JDK 20.0.1, OpenJDK 64-Bit Server VM, 20.0.1+9-29
* # Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
*
* Benchmark (bits) Mode Cnt Score Error Units
* bigInteger 44000 avgt 4 348.081 ± 8.025 us/op
* bigInteger 44500 avgt 4 350.277 ± 35.341 us/op
* bigInteger 45000 avgt 4 356.652 ± 24.410 us/op
* fft 44000 avgt 4 350.903 ± 5.170 us/op
* fft 44500 avgt 4 344.830 ± 2.741 us/op
* fft 45000 avgt 4 343.596 ± 3.054 us/op
*
* Process finished with exit code 0
* </pre>
*/

@Fork(value = 1, jvmArgsAppend = {
"-XX:+UnlockExperimentalVMOptions", "--add-modules", "jdk.incubator.vector"
, "--enable-preview"
})
@Measurement(iterations = 4, time = 1)
@Warmup(iterations = 10, time = 1)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Benchmark)
public class JmhFftThreshold {

@Param({"44000",
"44500", // where both variants are equal - FftMultiplier.FFT_THRESHOLD
"45000"})
public int bits;
private BigInteger a;
private BigInteger b;
private BigInteger bs;
public int zeroes;

@Setup(Level.Trial)
public void setUp() {
int length = (bits + 7) / 8;
byte[] bytesA = new byte[length];
byte[] bytesB = new byte[length];
Random rng = new Random(0);
rng.nextBytes(bytesA);
rng.nextBytes(bytesB);

// to be positive
bytesA[0] &= ~(1L << 63);
bytesB[0] &= ~(1L << 63);

// set for b rightmost zeroes like 10^n has
final double lg5 = Math.log(5) / Math.log(2);
zeroes = (int) (length / (lg5 + 1));
for (int i = 0; i < zeroes; i++) {
bytesB[length - 1 - i] = 0;
}

a = new BigInteger(1, bytesA);
b = new BigInteger(1, bytesB);
bs = b.shiftRight(zeroes * 8); // preshift value - imitate 5^n
System.out.println(b.getLowestSetBit() + " bits from " + length * 8 + " in total are zero");
}


@Benchmark
public void fft(Blackhole blackhole) {
blackhole.consume(FftMultiplier.multiplyFft(a, b));
}

@Benchmark
public void bigInteger(Blackhole blackhole) {
blackhole.consume(a.multiply(bs).shiftLeft(zeroes * 8));
}
}





Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* @(#)JmhMultiplyByPowerOfFiveAndShiftMultiplier.java
* Copyright © 2023 Werner Randelshofer, Switzerland. MIT License.
*/
package ch.randelshofer.fastdoubleparser;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.math.BigInteger;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* Benchmarks for selected floating point strings.
* <pre>
* # JMH version: 1.36
* # VM version: JDK 20.0.1, OpenJDK 64-Bit Server VM, 20.0.1+9-29
* # Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
*
* Benchmark (bits) Mode Cnt Score Error Units
* optimized 700 avgt 4 250.376 ± 3.773 ns/op
* optimized 800 avgt 4 317.921 ± 30.518 ns/op
* optimized 900 avgt 4 405.584 ± 2.953 ns/op
* optimized 2000 avgt 4 1371.457 ± 15.372 ns/op
* optimized 10000 avgt 4 30015.376 ± 1073.798 ns/op
* optimized 30000 avgt 4 180735.583 ± 825.962 ns/op
* original 700 avgt 4 254.843 ± 1.414 ns/op
* original 800 avgt 4 339.564 ± 2.898 ns/op
* original 900 avgt 4 463.033 ± 4.538 ns/op
* original 2000 avgt 4 1828.848 ± 121.480 ns/op
* original 10000 avgt 4 33663.669 ± 3189.014 ns/op
* original 30000 avgt 4 204170.758 ± 3690.179 ns/op
* </pre>
*/

@Fork(value = 1, jvmArgsAppend = {
"-XX:+UnlockExperimentalVMOptions", "--add-modules", "jdk.incubator.vector"
, "--enable-preview"
})
@Measurement(iterations = 4, time = 1)
@Warmup(iterations = 4, time = 1)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@State(Scope.Benchmark)
public class JmhMultiplyByPowerOfFiveAndShiftMultiplier {


@Param({
"700" // above this value, variant with bit shift leads
, "800"
, "900"
, "2000"
, "10000"
, "30000"
})
public int bits;
private BigInteger a;
private BigInteger b;
private BigInteger bs;
public int zeroes;

@Setup(Level.Trial)
public void setUp() {
int length = (bits + 7) / 8;
byte[] bytesA = new byte[length];
byte[] bytesB = new byte[length];
Random rng = new Random(0);
rng.nextBytes(bytesA);
rng.nextBytes(bytesB);

// to be positive
bytesA[0] &= ~(1L << 63);
bytesB[0] &= ~(1L << 63);

// set for b rightmost zeroes like 10^n has
final double lg5 = Math.log(5) / Math.log(2);
zeroes = (int) (length / (lg5 + 1));
for (int i = 0; i < zeroes; i++) {
bytesB[length - 1 - i] = 0;
}

a = new BigInteger(1, bytesA);
b = new BigInteger(1, bytesB);
bs = b.shiftRight(zeroes * 8); // preshift value - imitate 5^n
System.out.println(b.getLowestSetBit() + " bits from " + length * 8 + " in total are zero");
}


@Benchmark
public void original(Blackhole blackhole) {
blackhole.consume(FftMultiplier.multiply(a, b));
}

@Benchmark
public void optimized(Blackhole blackhole) {
blackhole.consume(FftMultiplier.multiply(a, bs).shiftLeft(zeroes * 8));
}
}





Loading