blob: d9e0bfa394e9bf853d277af42c9cf5a92ec0134e [file] [log] [blame]
/*
* 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.numbers.examples.jmh.complex;
import org.apache.commons.math3.util.FastMath;
import org.apache.commons.numbers.core.Precision;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
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 java.util.SplittableRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.DoubleSupplier;
import java.util.function.DoubleUnaryOperator;
import java.util.function.Supplier;
import java.util.stream.DoubleStream;
/**
* Executes a benchmark to estimate the speed of sin/cos operations.
* This compares the Math implementation to FastMath. It would be possible
* to adapt FastMath to compute sin/cos together as they both use a common
* initial stage to map the value to the domain [0, pi/2).
*/
@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 SinCosPerformance {
/**
* An array of edge numbers that will produce edge case results from sin/cos functions:
* {@code +/-inf, +/-0, nan}.
*/
private static final double[] EDGE_NUMBERS = {
Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 0.0, -0.0, Double.NaN};
/**
* Contains the size of numbers.
*/
@State(Scope.Benchmark)
public static class NumberSize {
/**
* The size of the data.
*/
@Param({"1000"})
private int size;
/**
* Gets the size.
*
* @return the size
*/
public int getSize() {
return size;
}
}
/**
* Contains an array of numbers.
*/
@State(Scope.Benchmark)
public static class Numbers extends NumberSize {
/** The numbers. */
protected double[] numbers;
/**
* The type of the data.
*/
@Param({"pi", "pi/2", "random", "edge"})
private String type;
/**
* Gets the numbers.
*
* @return the numbers
*/
public double[] getNumbers() {
return numbers;
}
/**
* Create the complex numbers.
*/
@Setup
public void setup() {
numbers = createNumbers(new SplittableRandom());
// Verify functions
for (final double x : numbers) {
final double sin = Math.sin(x);
assertEquals(sin, FastMath.sin(x), 1, () -> "sin " + x);
final double cos = Math.cos(x);
assertEquals(cos, FastMath.cos(x), 1, () -> "cos " + x);
}
}
/**
* Creates the numbers.
*
* @param rng Random number generator.
* @return the random number
*/
private double[] createNumbers(SplittableRandom rng) {
DoubleSupplier generator;
if ("pi".equals(type)) {
generator = () -> rng.nextDouble() * 2 * Math.PI - Math.PI;
} else if ("pi/2".equals(type)) {
generator = () -> rng.nextDouble() * Math.PI - Math.PI / 2;
} else if ("random".equals(type)) {
generator = () -> createRandomNumber(rng);
} else if ("edge".equals(type)) {
generator = () -> createEdgeNumber(rng);
} else {
throw new IllegalStateException("Unknown number type: " + type);
}
return DoubleStream.generate(generator).limit(getSize()).toArray();
}
/**
* Assert the values are equal to the given ulps, else throw an AssertionError.
*
* @param x the x
* @param y the y
* @param maxUlps the max ulps for equality
* @param msg the message upon failure
*/
private static void assertEquals(double x, double y, int maxUlps, Supplier<String> msg) {
if (!Precision.equalsIncludingNaN(x, y, maxUlps)) {
throw new AssertionError(msg.get() + ": " + x + " != " + y);
}
}
}
/**
* Creates a random double number with a random sign and mantissa and a large range for
* the exponent. The numbers will not be uniform over the range.
*
* @param rng Random number generator.
* @return the random number
*/
private static double createRandomNumber(SplittableRandom rng) {
// Create random doubles using random bits in the sign bit and the mantissa.
// Then create an exponent in the range -64 to 64.
final long mask = ((1L << 52) - 1) | 1L << 63;
final long bits = rng.nextLong() & mask;
// The exponent must be unsigned so + 1023 to the signed exponent
final long exp = rng.nextInt(129) - 64 + 1023;
return Double.longBitsToDouble(bits | (exp << 52));
}
/**
* Creates a random double number that will be an edge case:
* {@code +/-inf, +/-0, nan}.
*
* @param rng Random number generator.
* @return the random number
*/
private static double createEdgeNumber(SplittableRandom rng) {
return EDGE_NUMBERS[rng.nextInt(EDGE_NUMBERS.length)];
}
/**
* Apply the function to all the numbers.
*
* @param numbers Numbers.
* @param fun Function.
* @return the result of the function.
*/
private static double[] apply(double[] numbers, DoubleUnaryOperator fun) {
final double[] result = new double[numbers.length];
for (int i = 0; i < numbers.length; i++) {
result[i] = fun.applyAsDouble(numbers[i]);
}
return result;
}
/**
* Identity function. This can be used to measure overhead of copy array creation.
*
* @param z Complex number.
* @return the number
*/
private static double identity(double z) {
return z;
}
// Benchmark methods.
//
// The methods are partially documented as the names are self-documenting.
// CHECKSTYLE: stop JavadocMethod
// CHECKSTYLE: stop DesignForExtension
//
// Benchmarks use function references to perform different operations on the numbers.
// Tests show that explicit programming of the same benchmarks run in the same time.
// For reference examples are provided for sin(x).
/**
* Explicit benchmark without using a method reference.
* This is commented out as it exists for reference purposes.
*/
//@Benchmark
public double[] mathSin2(Numbers numbers) {
final double[] x = numbers.getNumbers();
final double[] result = new double[x.length];
for (int i = 0; i < x.length; i++) {
result[i] = Math.sin(x[i]);
}
return result;
}
/**
* Baseline the creation of the new array of numbers with the same number (an identity).
* This contains the baseline JMH overhead for all the benchmarks that create numbers.
* All other methods are expected to be slower than this.
*/
@Benchmark
public double[] baselineIdentity(Numbers numbers) {
return apply(numbers.getNumbers(), SinCosPerformance::identity);
}
@Benchmark
public double[] mathSin(Numbers numbers) {
return apply(numbers.getNumbers(), Math::sin);
}
@Benchmark
public double[] mathCos(Numbers numbers) {
return apply(numbers.getNumbers(), Math::cos);
}
@Benchmark
public double[] fastMathSin(Numbers numbers) {
return apply(numbers.getNumbers(), FastMath::sin);
}
@Benchmark
public double[] fastMathCos(Numbers numbers) {
return apply(numbers.getNumbers(), FastMath::cos);
}
}