Skip to content

Commit

Permalink
Update remainderUnsigned and divideUnsigned
Browse files Browse the repository at this point in the history
Re-implement the methods from Hackers Delight 2.0.

Delegate to JDK method for integers.

Add notes that the JDK long method is fast from JDK 17 and an intrinsic
from JDK 19.
  • Loading branch information
aherbert committed Mar 3, 2024
1 parent 27ab685 commit 5fcb66a
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -470,56 +470,70 @@ public static boolean isPowerOfTwo(long n) {
* Returns the unsigned remainder from dividing the first argument
* by the second where each argument and the result is interpreted
* as an unsigned value.
* <p>This method does not use the {@code long} datatype.</p>
*
* <p>Implementation note
*
* <p>In v1.0 this method did not use the {@code long} datatype.
* Modern 64-bit processors make use of the {@code long} datatype
* faster than an algorithm using the {@code int} datatype. This method
* now delegates to {@link Integer#remainderUnsigned(int, int)}
* which uses {@code long} arithmetic; or from JDK 19 an intrinsic method.
*
* @param dividend the value to be divided
* @param divisor the value doing the dividing
* @return the unsigned remainder of the first argument divided by
* the second argument.
* @see Integer#remainderUnsigned(int, int)
*/
public static int remainderUnsigned(int dividend, int divisor) {
if (divisor >= 0) {
if (dividend >= 0) {
return dividend % divisor;
}
// The implementation is a Java port of algorithm described in the book
// "Hacker's Delight" (section "Unsigned short division from signed division").
final int q = ((dividend >>> 1) / divisor) << 1;
dividend -= q * divisor;
if (dividend < 0 || dividend >= divisor) {
dividend -= divisor;
}
return dividend;
}
return dividend >= 0 || dividend < divisor ? dividend : dividend - divisor;
return Integer.remainderUnsigned(dividend, divisor);
}

/**
* Returns the unsigned remainder from dividing the first argument
* by the second where each argument and the result is interpreted
* as an unsigned value.
* <p>This method does not use the {@code BigInteger} datatype.</p>
*
* <p>Implementation note
*
* <p>This method does not use the {@code BigInteger} datatype.
* The JDK implementation of {@link Long#remainderUnsigned(long, long)}
* uses {@code BigInteger} prior to JDK 17 and this method is 15-25x faster.
* From JDK 17 onwards the JDK implementation is as fast; or from JDK 19
* even faster due to use of an intrinsic method.
*
* @param dividend the value to be divided
* @param divisor the value doing the dividing
* @return the unsigned remainder of the first argument divided by
* the second argument.
* @see Long#remainderUnsigned(long, long)
*/
public static long remainderUnsigned(long dividend, long divisor) {
if (divisor >= 0L) {
if (dividend >= 0L) {
return dividend % divisor;
}
// The implementation is a Java port of algorithm described in the book
// "Hacker's Delight" (section "Unsigned short division from signed division").
final long q = ((dividend >>> 1) / divisor) << 1;
dividend -= q * divisor;
if (dividend < 0L || dividend >= divisor) {
dividend -= divisor;
}
return dividend;
// Adapts the divideUnsigned method to compute the remainder.
if (divisor < 0) {
// Using unsigned compare:
// if dividend < divisor: return dividend
// else: return dividend - divisor

// Subtracting divisor using masking is more complex in this case
// and we use a condition
return dividend >= 0 || dividend < divisor ? dividend : dividend - divisor;

}
return dividend >= 0L || dividend < divisor ? dividend : dividend - divisor;
// From Hacker's Delight 2.0, section 9.3
final long q = ((dividend >>> 1) / divisor) << 1;
final long r = dividend - q * divisor;
// unsigned r: 0 <= r < 2 * divisor
// if (r < divisor): r
// else: r - divisor

// The compare of unsigned r can be done using:
// return (r + Long.MIN_VALUE) < (divisor | Long.MIN_VALUE) ? r : r - divisor

// Here we subtract divisor if (r - divisor) is positive, else the result is r.
// This can be done by flipping the sign bit and
// creating a mask as -1 or 0 by signed shift.
return r - (divisor & (~(r - divisor) >> 63));
}

/**
Expand All @@ -531,28 +545,23 @@ public static long remainderUnsigned(long dividend, long divisor) {
* bit-wise identical if the two operands are regarded as both
* being signed or both being unsigned. Therefore separate {@code
* addUnsigned}, etc. methods are not provided.</p>
* <p>This method does not use the {@code long} datatype.</p>
*
* <p>Implementation note
*
* <p>In v1.0 this method did not use the {@code long} datatype.
* Modern 64-bit processors make use of the {@code long} datatype
* faster than an algorithm using the {@code int} datatype. This method
* now delegates to {@link Integer#divideUnsigned(int, int)}
* which uses {@code long} arithmetic; or from JDK 19 an intrinsic method.
*
* @param dividend the value to be divided
* @param divisor the value doing the dividing
* @return the unsigned quotient of the first argument divided by
* the second argument
* @see Integer#divideUnsigned(int, int)
*/
public static int divideUnsigned(int dividend, int divisor) {
if (divisor >= 0) {
if (dividend >= 0) {
return dividend / divisor;
}
// The implementation is a Java port of algorithm described in the book
// "Hacker's Delight" (section "Unsigned short division from signed division").
int q = ((dividend >>> 1) / divisor) << 1;
dividend -= q * divisor;
if (dividend < 0L || dividend >= divisor) {
q++;
}
return q;
}
return dividend >= 0 || dividend < divisor ? 0 : 1;
return Integer.divideUnsigned(dividend, divisor);
}

/**
Expand All @@ -564,28 +573,36 @@ public static int divideUnsigned(int dividend, int divisor) {
* bit-wise identical if the two operands are regarded as both
* being signed or both being unsigned. Therefore separate {@code
* addUnsigned}, etc. methods are not provided.</p>
* <p>This method does not use the {@code BigInteger} datatype.</p>
*
* <p>Implementation note
*
* <p>This method does not use the {@code BigInteger} datatype.
* The JDK implementation of {@link Long#divideUnsigned(long, long)}
* uses {@code BigInteger} prior to JDK 17 and this method is 15-25x faster.
* From JDK 17 onwards the JDK implementation is as fast; or from JDK 19
* even faster due to use of an intrinsic method.
*
* @param dividend the value to be divided
* @param divisor the value doing the dividing
* @return the unsigned quotient of the first argument divided by
* the second argument.
* @see Long#divideUnsigned(long, long)
*/
public static long divideUnsigned(long dividend, long divisor) {
if (divisor >= 0L) {
if (dividend >= 0L) {
return dividend / divisor;
}
// The implementation is a Java port of algorithm described in the book
// "Hacker's Delight" (section "Unsigned short division from signed division").
long q = ((dividend >>> 1) / divisor) << 1;
dividend -= q * divisor;
if (dividend < 0L || dividend >= divisor) {
q++;
}
return q;
// The implementation is a Java port of algorithm described in the book
// "Hacker's Delight 2.0" (section 9.3 "Unsigned short division from signed division").
// Adapts 6-line predicate expressions program with (u >=) an unsigned compare
// using the provided branchless variants.
if (divisor < 0) {
// line 1 branchless:
// q <- (dividend (u >=) divisor)
return (dividend & ~(dividend - divisor)) >>> 63;
}
return dividend >= 0L || dividend < divisor ? 0L : 1L;
final long q = ((dividend >>> 1) / divisor) << 1;
final long r = dividend - q * divisor;
// line 5 branchless:
// q <- q + (r (u >=) divisor)
return q + ((r | ~(r - divisor)) >>> 63);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import java.util.Arrays;
import java.math.BigInteger;
import java.util.Collections;

import java.util.concurrent.ThreadLocalRandom;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -492,12 +492,11 @@ private static int[] getIntSpecialCases() {
for (int j = 0; j < 20; j++) {
ints[i++] = j;
}
for (int j = i - 1; j >= 0; j--) {
for (int j = i; --j >= 0;) {
ints[i++] = ints[j] > 0 ? -ints[j] : Integer.MIN_VALUE;
}
java.util.Random r = new java.util.Random(System.nanoTime());
for (; i < ints.length;) {
ints[i++] = r.nextInt();
ints[i++] = ThreadLocalRandom.current().nextInt();
}
return ints;
}
Expand Down Expand Up @@ -529,12 +528,11 @@ private static long[] getLongSpecialCases() {
for (int j = 0; j < 20; j++) {
longs[i++] = j;
}
for (int j = i - 1; j >= 0; j--) {
for (int j = i; --j >= 0;) {
longs[i++] = longs[j] > 0L ? -longs[j] : Long.MIN_VALUE;
}
java.util.Random r = new java.util.Random(System.nanoTime());
for (; i < longs.length;) {
longs[i++] = r.nextLong();
longs[i++] = ThreadLocalRandom.current().nextLong();
}
return longs;
}
Expand Down Expand Up @@ -581,7 +579,8 @@ void testRemainderUnsignedIntSpecialCases() {
() -> ArithmeticUtils.remainderUnsigned(dividend, divisor)
);
} else {
Assertions.assertEquals(remainderUnsignedExpected(dividend, divisor), ArithmeticUtils.remainderUnsigned(dividend, divisor));
Assertions.assertEquals(remainderUnsignedExpected(dividend, divisor), ArithmeticUtils.remainderUnsigned(dividend, divisor),
() -> dividend + " % " + divisor);
}
}
}
Expand All @@ -605,7 +604,8 @@ void testRemainderUnsignedLongSpecialCases() {
// Success.
}
} else {
Assertions.assertEquals(remainderUnsignedExpected(dividend, divisor), ArithmeticUtils.remainderUnsigned(dividend, divisor));
Assertions.assertEquals(remainderUnsignedExpected(dividend, divisor), ArithmeticUtils.remainderUnsigned(dividend, divisor),
() -> dividend + " % " + divisor);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.concurrent.TimeUnit;
import java.util.function.IntBinaryOperator;
import java.util.function.LongBinaryOperator;
import org.apache.commons.numbers.core.ArithmeticUtils;
import org.apache.commons.rng.simple.RandomSource;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
Expand Down Expand Up @@ -49,9 +50,13 @@
@Fork(value = 1, jvmArgs = {"-server", "-Xms512M", "-Xmx512M"})
public class ArithmeticPerformance {
/** Method to compute the divide using unsigned arithmetic. */
private static final String DIVIDE_UNSIGNED = "divideUnsigned";
private static final String DIVIDE_UNSIGNED_1_0 = "divideUnsigned_1.0";
/** Method to compute the remainder using unsigned arithmetic. */
private static final String REMAINDER_UNSIGNED = "remainderUnsigned";
private static final String REMAINDER_UNSIGNED_1_0 = "remainderUnsigned_1.0";
/** Method to compute the divide using unsigned arithmetic. */
private static final String DIVIDE_UNSIGNED_1_1 = "divideUnsigned_1.1";
/** Method to compute the remainder using unsigned arithmetic. */
private static final String REMAINDER_UNSIGNED_1_1 = "remainderUnsigned_1.1";

/**
* Source of {@code long} array data.
Expand Down Expand Up @@ -117,8 +122,8 @@ public void setup() {
@State(Scope.Benchmark)
public static class LongFunctionSource {
/** Name of the source. */
@Param({"Long.divideUnsigned", DIVIDE_UNSIGNED,
"Long.remainderUnsigned", REMAINDER_UNSIGNED})
@Param({"Long.divideUnsigned", DIVIDE_UNSIGNED_1_0, DIVIDE_UNSIGNED_1_1,
"Long.remainderUnsigned", REMAINDER_UNSIGNED_1_0, REMAINDER_UNSIGNED_1_1})
private String name;

/** The action. */
Expand All @@ -138,12 +143,16 @@ public LongBinaryOperator getFunction() {
public void setup() {
if ("Long.divideUnsigned".equals(name)) {
function = Long::divideUnsigned;
} else if (DIVIDE_UNSIGNED.equals(name)) {
} else if (DIVIDE_UNSIGNED_1_0.equals(name)) {
function = ArithmeticPerformance::divideUnsigned;
} else if (DIVIDE_UNSIGNED_1_1.equals(name)) {
function = ArithmeticUtils::divideUnsigned;
} else if ("Long.remainderUnsigned".equals(name)) {
function = Long::remainderUnsigned;
} else if (REMAINDER_UNSIGNED.equals(name)) {
} else if (REMAINDER_UNSIGNED_1_0.equals(name)) {
function = ArithmeticPerformance::remainderUnsigned;
} else if (REMAINDER_UNSIGNED_1_1.equals(name)) {
function = ArithmeticUtils::remainderUnsigned;
} else {
throw new IllegalStateException("Unknown long function: " + name);
}
Expand All @@ -156,8 +165,8 @@ public void setup() {
@State(Scope.Benchmark)
public static class IntFunctionSource {
/** Name of the source. */
@Param({"Integer.divideUnsigned", DIVIDE_UNSIGNED,
"Integer.remainderUnsigned", REMAINDER_UNSIGNED})
@Param({"Integer.divideUnsigned", DIVIDE_UNSIGNED_1_0, DIVIDE_UNSIGNED_1_1,
"Integer.remainderUnsigned", REMAINDER_UNSIGNED_1_0, REMAINDER_UNSIGNED_1_1})
private String name;

/** The action. */
Expand All @@ -177,12 +186,16 @@ public IntBinaryOperator getFunction() {
public void setup() {
if ("Integer.divideUnsigned".equals(name)) {
function = Integer::divideUnsigned;
} else if (DIVIDE_UNSIGNED.equals(name)) {
} else if (DIVIDE_UNSIGNED_1_0.equals(name)) {
function = ArithmeticPerformance::divideUnsigned;
} else if (DIVIDE_UNSIGNED_1_1.equals(name)) {
function = ArithmeticUtils::divideUnsigned;
} else if ("Integer.remainderUnsigned".equals(name)) {
function = Integer::remainderUnsigned;
} else if (REMAINDER_UNSIGNED.equals(name)) {
} else if (REMAINDER_UNSIGNED_1_0.equals(name)) {
function = ArithmeticPerformance::remainderUnsigned;
} else if (REMAINDER_UNSIGNED_1_1.equals(name)) {
function = ArithmeticUtils::remainderUnsigned;
} else {
throw new IllegalStateException("Unknown int function: " + name);
}
Expand All @@ -195,7 +208,7 @@ public void setup() {
* as an unsigned value.
* <p>This method does not use the {@code long} datatype.</p>
*
* <p>This is a copy of the original method in {@code o.a.c.numbers.core.ArithmeticUtils}.
* <p>This is a copy of the original method in {@code o.a.c.numbers.core.ArithmeticUtils} v1.0.
*
* @param dividend the value to be divided
* @param divisor the value doing the dividing
Expand Down Expand Up @@ -225,7 +238,7 @@ public static int remainderUnsigned(int dividend, int divisor) {
* as an unsigned value.
* <p>This method does not use the {@code BigInteger} datatype.</p>
*
* <p>This is a copy of the original method in {@code o.a.c.numbers.core.ArithmeticUtils}.
* <p>This is a copy of the original method in {@code o.a.c.numbers.core.ArithmeticUtils} v1.0.
*
* @param dividend the value to be divided
* @param divisor the value doing the dividing
Expand Down
5 changes: 4 additions & 1 deletion src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ If the output is not quite correct, check for invisible trailing spaces!
<release version="1.2" date="TBD" description="
New features, updates and bug fixes.
">
<action dev="aherbert" type="add" due-to="Harald Kirsch" issue="NUMBERS-205">
<action dev="aherbert" type="update" due-to="Sebb, Alex Herbert">
"ArithmeticUtils": Improve performance of remainderUnsigned and divideUnsigned.
</action>
<action dev="aherbert" type="add" due-to="Harald Kirsch" issue="NUMBERS-205">
"Addition/Multiplication": Introduces isZero to Addition and isOne to Multiplication
interfaces. Override the default implementation in implementing classes to avoid
expensive equals(Object) operations.
Expand Down

0 comments on commit 5fcb66a

Please sign in to comment.