Skip to content

Commit

Permalink
Math API Division Optimizations (#6988)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisewright committed Feb 15, 2021
1 parent d40d607 commit 7f826a9
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/api/java/mekanism/api/MekanismAPI.java
Expand Up @@ -22,7 +22,7 @@ private MekanismAPI() {
/**
* The version of the api classes - may not always match the mod's version
*/
public static final String API_VERSION = "10.0.18";
public static final String API_VERSION = "10.0.21";
public static final String MEKANISM_MODID = "mekanism";
/**
* Mekanism debug mode
Expand Down
119 changes: 116 additions & 3 deletions src/api/java/mekanism/api/math/FloatingLong.java
Expand Up @@ -37,6 +37,10 @@ public class FloatingLong extends Number implements Comparable<FloatingLong> {
* The value which represents 1.0, this is one more than the value of {@link #MAX_DECIMAL}
*/
private static final short SINGLE_UNIT = MAX_DECIMAL + 1;
/**
* The maximum value where the decimal can be eliminated without {@link #value} overflowing
*/
private static final long MAX_LONG_SHIFT = Long.divideUnsigned(-1L, SINGLE_UNIT);
/**
* A constant holding the value {@code 0}
*/
Expand Down Expand Up @@ -327,13 +331,122 @@ public FloatingLong timesEqual(FloatingLong toMultiply) {
public FloatingLong divideEquals(FloatingLong toDivide) {
if (toDivide.isZero()) {
throw new ArithmeticException("Division by zero");
} else if (this.isZero()) {
return FloatingLong.ZERO;
} else if (toDivide.decimal == 0) {
//If we are dividing by a whole number, use our more optimized division algorithm
return divideEquals(toDivide.value);
}
BigDecimal divide = new BigDecimal(toString()).divide(new BigDecimal(toDivide.toString()), DECIMAL_DIGITS, RoundingMode.HALF_EVEN);
long value = divide.longValue();
short decimal = parseDecimal(divide.toPlainString());
return setAndClampValues(value, decimal);
}

/**
* Divides this {@link FloatingLong} by the given unsigned long primitive, modifying the current object unless it is a constant in which case it instead returns the
* result in a new object. Rounds to the nearest 0.0001
*
* @param toDivide The value to divide by represented as an unsigned long.
*
* @return The {@link FloatingLong} representing the value of dividing this {@link FloatingLong} by the given unsigned long.
*
* @throws ArithmeticException if {@code toDivide} is zero.
* @apiNote It is recommended to set this to itself to reduce the chance of accidental calls if calling this on a constant {@link FloatingLong}
* <br>
* {@code value = value.divideEquals(toDivide)}
*/
public FloatingLong divideEquals(long toDivide) {
if (toDivide == 0) {
throw new ArithmeticException("Division by zero");
} else if (this.isZero()) {
return FloatingLong.ZERO;
}
long val = Long.divideUnsigned(this.value, toDivide);
long rem = Long.remainderUnsigned(this.value, toDivide);

//just need to figure out remainder -> decimal
long dec;

//okay, now what if rem * SINGLE_UNIT * 10L will overflow?
if (Long.compareUnsigned(rem * 10, MAX_LONG_SHIFT) >= 0) {
//if that'll overflow, then toDivide also has to be big. let's just lose some denominator precision and use that
dec = Long.divideUnsigned(rem, Long.divideUnsigned(toDivide, SINGLE_UNIT * 10L)); //same as multiplying numerator
} else {
dec = Long.divideUnsigned(rem * SINGLE_UNIT * 10L, toDivide); //trivial case
dec += Long.divideUnsigned(this.decimal * 10L, toDivide); //need to account for dividing decimal too in case toDivide < 10k
}

//usually will expect to round to nearest, so we have to do that here
if (Long.remainderUnsigned(dec, 10) >= 5) {
dec += 10;
}
dec /= 10;
return setAndClampValues(val, (short) dec);
}

/**
* Divides this {@link FloatingLong} by the given {@link FloatingLong} rounded down to an integer value. This gets clamped at the upper bound of {@link
* Long#MAX_VALUE} rather than overflowing.
*
* @param toDivide The {@link FloatingLong} to divide by.
*
* @return A long representing the value of dividing this {@link FloatingLong} by the given {@link FloatingLong}.
*
* @throws ArithmeticException if {@code toDivide} is zero.
*/
public long divideToLong(FloatingLong toDivide) {
if (toDivide.isZero()) {
throw new ArithmeticException("Division by zero");
} else if (this.smallerThan(toDivide)) {
// Return early if operation will return < 1
return 0;
}
if (toDivide.greaterOrEqual(ONE)) {
//If toDivide >=1, then we don't care about this.decimal, so can optimize out accounting for that
if (Long.compareUnsigned(toDivide.value, MAX_LONG_SHIFT) <= 0) { //don't case if *this* is < or > than shift
long div = toDivide.value * MAX_DECIMAL + toDivide.decimal;
return (Long.divideUnsigned(this.value, div) * MAX_DECIMAL) + (this.value % div * MAX_DECIMAL / div);
}
// we already know toDivide is > max_long_shift, and other case is impossible
if (Long.compareUnsigned(toDivide.value, Long.divideUnsigned(-1L, 2) + 1L) >= 0) {
//need to check anyways to avoid overflow on toDivide.value +1, so might as well return early
return 1;
}
long q = Long.divideUnsigned(this.value, toDivide.value);
if (q != Long.divideUnsigned(this.value, toDivide.value + 1)) {
// check if we need to account for toDivide.decimal in this case
if (toDivide.value * q + Long.divideUnsigned(toDivide.decimal * q, MAX_DECIMAL) > this.value) {
// if we do, reduce the result
return q - 1;
}
}
return q;
}
// In this case, we're really multiplying (definitely need to account for decimal as well
if (Long.compareUnsigned(this.value, MAX_LONG_SHIFT) >= 0) {
return (this.value / toDivide.decimal) * MAX_DECIMAL //lose some precision here, have to add modulus
+ (this.value % toDivide.decimal) * MAX_DECIMAL / toDivide.decimal
+ (long) this.decimal * MAX_DECIMAL / toDivide.decimal;
}
long d = this.value * MAX_DECIMAL;
return d / toDivide.decimal + (long) this.decimal * MAX_DECIMAL / toDivide.decimal; //don't care about modulus since we're returning integers
}

/**
* Divides this {@link FloatingLong} by the given {@link FloatingLong} rounded down to an integer value. This gets clamped at the upper bound of {@link
* Integer#MAX_VALUE} rather than overflowing.
*
* @param toDivide The {@link FloatingLong} to divide by.
*
* @return An int representing the value of dividing this {@link FloatingLong} by the given {@link FloatingLong}.
*
* @throws ArithmeticException if {@code toDivide} is zero.
*/
public int divideToInt(FloatingLong toDivide) {
return MathUtils.clampToInt(divideToLong(toDivide));
}

/**
* Adds the given {@link FloatingLong} to this {@link FloatingLong} and returns the result in a new object. This gets clamped at the upper bound of {@link
* FloatingLong#MAX_VALUE} rather than overflowing.
Expand Down Expand Up @@ -482,7 +595,7 @@ public FloatingLong divide(FloatingLong toDivide) {
* @throws ArithmeticException if {@code toDivide} is zero.
*/
public FloatingLong divide(long toDivide) {
return divide(FloatingLong.create(toDivide));
return copy().divideEquals(toDivide);
}

/**
Expand All @@ -509,8 +622,8 @@ public FloatingLong divide(double toDivide) {
*
* @param toDivide The {@link FloatingLong} to divide by.
*
* @return The {@link FloatingLong} representing the value of dividing this {@link FloatingLong} by the given {@link FloatingLong}, or {@code 1} if the given {@link
* FloatingLong} is {@code 0}.
* @return A double representing the value of dividing this {@link FloatingLong} by the given {@link FloatingLong}, or {@code 1} if the given {@link FloatingLong} is
* {@code 0}.
*
* @implNote This caps the returned value at {@code 1}
*/
Expand Down
23 changes: 23 additions & 0 deletions src/test/java/mekanism/api/math/FloatingLongPropertyTest.java
Expand Up @@ -93,4 +93,27 @@ void testDivision() {
return b.isZero() || a.divide(b).equals(divideViaBigDecimal(a, b));
});
}

@Test
@DisplayName("Test dividing to long works correctly")
void testDivisionToLong() {
theoryForAllPairs().check((v1, d1, v2, d2) -> {
FloatingLong a = FloatingLong.createConst(v1, d1.shortValue());
FloatingLong b = FloatingLong.createConst(v2, d2.shortValue());
return b.isZero() || a.divideToLong(b) == a.divide(b).longValue();
});
}

@Test
@DisplayName("Test dividing by long works correctly")
void testDivisionByLong() {
qt().forAll(
longs().all(),
integers().between(0, 9_999),
longs().all()
).check((v1, d1, b) -> {
FloatingLong a = FloatingLong.createConst(v1, d1.shortValue());
return b == 0 || a.divide(b).equals(divideViaBigDecimal(a, FloatingLong.create(b)));
});
}
}
16 changes: 16 additions & 0 deletions src/test/java/mekanism/api/math/FloatingLongTest.java
Expand Up @@ -78,6 +78,22 @@ void testDivisionLargeNumerator() {
Assertions.assertEquals(FloatingLong.create((long) 649_657 * 337), a.divide(b));
}

@Test
@DisplayName("Test division with a very large denominator")
void testDivisionLargeDenominator() {
FloatingLong a = FloatingLong.create(922355340224119L);
FloatingLong b = FloatingLong.create(-1L);
Assertions.assertEquals(FloatingLong.create(0L, (short) 1), a.divide(b));
}

@Test
@DisplayName("Test division denominator underflow to 0")
void testDivisionSmallDenominator() {
FloatingLong a = FloatingLong.create(0, (short) 1);
long b = 2L;
Assertions.assertEquals(FloatingLong.create(0L, (short) 1), a.divide(b));
}

@Test
@DisplayName("Test to string as two decimals")
void testConvertingStringToDecimal() {
Expand Down

0 comments on commit 7f826a9

Please sign in to comment.