blob: 103989545058739fcdee33e4259408998b403f14 [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.numbers.complex.Complex;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.sampling.distribution.ZigguratNormalizedGaussianSampler;
import org.apache.commons.rng.simple.RandomSource;
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 org.openjdk.jmh.infra.Blackhole;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.ToDoubleFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
/**
* Executes a benchmark to measure the speed of operations in the {@link Complex} class.
*/
@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 ComplexPerformance {
/**
* An array of edge numbers that will produce edge case results from functions:
* {@code +/-inf, +/-max, +/-min, +/-0, nan}.
*/
private static final double[] EDGE_NUMBERS = {
Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.MAX_VALUE,
-Double.MAX_VALUE, Double.MIN_VALUE, -Double.MIN_VALUE, 0.0, -0.0, Double.NaN};
/** The range to use for uniform random numbers. */
private static final double RANGE = 3.456789;
/**
* Contains the size of numbers.
*/
@State(Scope.Benchmark)
public static class ComplexNumberSize {
/**
* The size of the data.
*/
@Param({"10000"})
private int size;
/**
* Gets the size.
*
* @return the size
*/
public int getSize() {
return size;
}
}
/**
* Contains an array of complex numbers.
*/
@State(Scope.Benchmark)
public static class ComplexNumbers extends ComplexNumberSize {
/** The numbers. */
protected Complex[] numbers;
/**
* The type of the data.
*/
@Param({"cis", "vector", "log-uniform", "uniform", "edge"})
private String type;
/**
* Gets the numbers.
*
* @return the numbers
*/
public Complex[] getNumbers() {
return numbers;
}
/**
* Create the complex numbers.
*/
@Setup
public void setup() {
numbers = createNumbers(RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP));
}
/**
* Creates the numbers.
*
* @param rng Random number generator.
* @return the random complex number
*/
Complex[] createNumbers(UniformRandomProvider rng) {
Supplier<Complex> generator;
if ("cis".equals(type)) {
generator = () -> Complex.ofCis(rng.nextDouble() * 2 * Math.PI);
} else if ("vector".equals(type)) {
// An unnormalised random vector is created using a Gaussian sample
// for each dimension. Normalisation would create a cis number.
// This is effectively a polar complex number with random modulus
// in [-pi, pi] and random magnitude in a range defined by a Chi-squared
// distribution with 2 degrees of freedom.
final ZigguratNormalizedGaussianSampler s = ZigguratNormalizedGaussianSampler.of(rng);
generator = () -> Complex.ofCartesian(s.sample(), s.sample());
} else if ("log-uniform".equals(type)) {
generator = () -> Complex.ofCartesian(createLogUniformNumber(rng), createLogUniformNumber(rng));
} else if ("uniform".equals(type)) {
generator = () -> Complex.ofCartesian(createUniformNumber(rng), createUniformNumber(rng));
} else if ("edge".equals(type)) {
generator = () -> Complex.ofCartesian(createEdgeNumber(rng), createEdgeNumber(rng));
} else {
throw new IllegalStateException("Unknown number type: " + type);
}
return Stream.generate(generator).limit(getSize()).toArray(Complex[]::new);
}
}
/**
* Contains two arrays of complex numbers.
*/
@State(Scope.Benchmark)
public static class TwoComplexNumbers extends ComplexNumbers {
/** The numbers. */
private Complex[] numbers2;
/**
* Gets the second set of numbers.
*
* @return the numbers
*/
public Complex[] getNumbers2() {
return numbers2;
}
/**
* Create the complex numbers.
*/
@Override
@Setup
public void setup() {
// Do not call super.setup() so we recycle the RNG and avoid duplicates
final UniformRandomProvider rng = RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP);
numbers = createNumbers(rng);
numbers2 = createNumbers(rng);
}
}
/**
* Contains an array of complex numbers and an array of real numbers.
*/
@State(Scope.Benchmark)
public static class ComplexAndRealNumbers extends ComplexNumbers {
/** The numbers. */
private double[] numbers2;
/**
* Gets the second set of numbers.
*
* @return the numbers
*/
public double[] getNumbers2() {
return numbers2;
}
/**
* Create the complex numbers.
*/
@Override
@Setup
public void setup() {
// Do not call super.setup() so we recycle the RNG and avoid duplicates
final UniformRandomProvider rng = RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP);
numbers = createNumbers(rng);
numbers2 = Arrays.stream(createNumbers(rng)).mapToDouble(Complex::real).toArray();
}
}
/**
* Define a function between a complex and real number.
*/
private interface ComplexRealFunction {
/**
* Applies this function to the given arguments.
*
* @param z the complex argument
* @param x the real argument
* @return the function result
*/
Complex apply(Complex z, double x);
}
/**
* 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. This samples randomly
* using the components of a double. The limiting distribution is the log-uniform distribution.
*
* @param rng Random number generator.
* @return the random number
* @see <a href="https://en.wikipedia.org/wiki/Reciprocal_distribution">Reciprocal (log-uniform) distribution</a>
*/
private static double createLogUniformNumber(UniformRandomProvider rng) {
// Create random doubles using random bits in the sign bit and the mantissa.
// Then create an exponent in the range -64 to 64. Thus the sum product
// of 4 max or min values will not over or underflow.
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 with a random sign and uniform range.
*
* @param rng Random number generator.
* @return the random number
*/
private static double createUniformNumber(UniformRandomProvider rng) {
// Note: [0, 1) - 1 is [-1, 0).
// Since the 1 is a 50/50 sample the result is the interval [-1, 1)
// using the 2^54 dyadic rationals in the interval.
// The range is not critical. The numbers will have approximately 50%
// with the same exponent, max, matching that of RANGE and the rest smaller
// exponents down to (max - 53) since the uniform deviate is limited to 2^-53.
return (rng.nextDouble() - rng.nextInt(1)) * RANGE;
}
/**
* Creates a random double number that will be an edge case:
* {@code +/-inf, +/-max, +/-min, +/-0, nan}.
*
* @param rng Random number generator.
* @return the random number
*/
private static double createEdgeNumber(UniformRandomProvider rng) {
return EDGE_NUMBERS[rng.nextInt(EDGE_NUMBERS.length)];
}
/**
* Apply the function to all the numbers.
*
* @param numbers Numbers.
* @param fun Function.
* @param bh Data sink.
*/
private static void apply(Complex[] numbers, Predicate<Complex> fun, Blackhole bh) {
for (int i = 0; i < numbers.length; i++) {
bh.consume(fun.test(numbers[i]));
}
}
/**
* Apply the function to all the numbers.
*
* @param numbers Numbers.
* @param fun Function.
* @param bh Data sink.
*/
private static void apply(Complex[] numbers, ToDoubleFunction<Complex> fun, Blackhole bh) {
for (int i = 0; i < numbers.length; i++) {
bh.consume(fun.applyAsDouble(numbers[i]));
}
}
/**
* Apply the function to all the numbers.
*
* @param numbers Numbers.
* @param fun Function.
* @param bh Data sink.
*/
private static void apply(Complex[] numbers, UnaryOperator<Complex> fun, Blackhole bh) {
for (int i = 0; i < numbers.length; i++) {
bh.consume(fun.apply(numbers[i]));
}
}
/**
* Apply the function to the paired numbers.
*
* @param numbers First numbers of the pairs.
* @param numbers2 Second numbers of the pairs.
* @param fun Function.
* @param bh Data sink.
*/
private static void apply(Complex[] numbers, Complex[] numbers2,
BiFunction<Complex, Complex, Complex> fun, Blackhole bh) {
for (int i = 0; i < numbers.length; i++) {
bh.consume(fun.apply(numbers[i], numbers2[i]));
}
}
/**
* Apply the function to the paired numbers.
*
* @param numbers First numbers of the pairs.
* @param numbers2 Second numbers of the pairs.
* @param fun Function.
* @param bh Data sink.
*/
private static void apply(Complex[] numbers, double[] numbers2,
ComplexRealFunction fun, Blackhole bh) {
for (int i = 0; i < numbers.length; i++) {
bh.consume(fun.apply(numbers[i], numbers2[i]));
}
}
/**
* Identity function. This can be used to measure overhead of object array creation.
*
* @param z Complex number.
* @return the complex number
*/
private static Complex identity(Complex z) {
return z;
}
/**
* Copy function. This can be used to measure overhead of object array creation plus
* new Complex creation.
*
* @param z Complex number.
* @return a copy of the complex number
*/
private static Complex copy(Complex z) {
return Complex.ofCartesian(z.real(), z.imag());
}
// 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 complex numbers.
// Tests show that explicit programming of the same benchmarks run in the same time.
// For reference examples are provided for the fastest operations: real() and conj().
/**
* Explicit benchmark without using a method reference.
* This should run in the same time as {@link #real(ComplexNumbers, Blackhole)}.
* This is commented out as it exists for reference purposes.
*
* @param numbers Numbers.
* @param bh Data sink.
*/
//@Benchmark
public void real2(ComplexNumbers numbers, Blackhole bh) {
final Complex[] z = numbers.getNumbers();
for (int i = 0; i < z.length; i++) {
bh.consume(z[i].real());
}
}
/**
* Explicit benchmark without using a method reference.
* This should run in the same time as {@link #conj(ComplexNumbers, Blackhole)}.
* This is commented out as it exists for reference purposes.
*
* @param numbers Numbers.
* @param bh Data sink.
*/
//@Benchmark
public void conj2(ComplexNumbers numbers, Blackhole bh) {
final Complex[] z = numbers.getNumbers();
for (int i = 0; i < z.length; i++) {
bh.consume(z[i].conj());
}
}
/**
* Baseline the JMH overhead for the loop execute to consume Complex objects.
*
* @param numbers Numbers.
* @param bh Data sink.
*/
@Benchmark
public void baselineIdentity(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), ComplexPerformance::identity, bh);
}
/**
* Baseline the creation of a copy complex number. This
* measures the overhead of creation of new complex numbers including field access
* to the real and imaginary parts.
*
* @param numbers Numbers.
* @param bh Data sink.
*/
@Benchmark
public void baselineCopy(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), ComplexPerformance::copy, bh);
}
// Unary operations that a boolean
@Benchmark
public void isNaN(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::isNaN, bh);
}
@Benchmark
public void isInfinite(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::isInfinite, bh);
}
@Benchmark
public void isFinite(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::isFinite, bh);
}
// Unary operations that a double
@Benchmark
public void real(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::real, bh);
}
@Benchmark
public void imag(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::imag, bh);
}
@Benchmark
public void abs(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::abs, bh);
}
@Benchmark
public void arg(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::arg, bh);
}
@Benchmark
public void norm(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::norm, bh);
}
/**
* This test demonstrates that the method used in abs() is not as fast as using square
* root of the norm. The C99 standard for the abs() function requires over/underflow
* protection in the intermediate computation and infinity edge case handling. This
* has a performance overhead.
*
* @param numbers Numbers.
* @param bh Data sink.
*/
@Benchmark
public void sqrtNorm(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), (ToDoubleFunction<Complex>) z -> Math.sqrt(z.norm()), bh);
}
/**
* This test demonstrates that the {@link Math#hypot(double, double)} method
* is not as fast as the custom implementation in abs().
*
* @param numbers Numbers.
* @param bh Data sink.
*/
@Benchmark
public void absMathHypot(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), (ToDoubleFunction<Complex>) z -> Math.hypot(z.real(), z.imag()), bh);
}
// Unary operations that a complex number
@Benchmark
public void conj(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::conj, bh);
}
@Benchmark
public void negate(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::negate, bh);
}
@Benchmark
public void proj(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::proj, bh);
}
@Benchmark
public void cos(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::cos, bh);
}
@Benchmark
public void cosh(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::cosh, bh);
}
@Benchmark
public void exp(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::exp, bh);
}
@Benchmark
public void log(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::log, bh);
}
@Benchmark
public void log10(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::log10, bh);
}
@Benchmark
public void sin(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::sin, bh);
}
@Benchmark
public void sinh(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::sinh, bh);
}
@Benchmark
public void sqrt(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::sqrt, bh);
}
@Benchmark
public void tan(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::tan, bh);
}
@Benchmark
public void tanh(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::tanh, bh);
}
@Benchmark
public void acos(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::acos, bh);
}
@Benchmark
public void acosh(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::acosh, bh);
}
@Benchmark
public void asin(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::asin, bh);
}
@Benchmark
public void asinh(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::asinh, bh);
}
@Benchmark
public void atan(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::atan, bh);
}
@Benchmark
public void atanh(ComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), Complex::atanh, bh);
}
// Binary operations on two complex numbers.
@Benchmark
public void pow(TwoComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::pow, bh);
}
@Benchmark
public void multiply(TwoComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::multiply, bh);
}
@Benchmark
public void divide(TwoComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::divide, bh);
}
@Benchmark
public void add(TwoComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::add, bh);
}
@Benchmark
public void subtract(TwoComplexNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::subtract, bh);
}
// Binary operations on a complex and a real number.
// These only benchmark methods on the real component as the
// following are expected to be the same speed as the real-only operations
// given the equivalent primitive operations:
// - multiplyImaginary
// - divideImaginary
// - addImaginary
// - subtractImaginary
// - subtractFrom
// - subtractFromImaginary
@Benchmark
public void powReal(ComplexAndRealNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::pow, bh);
}
@Benchmark
public void multiplyReal(ComplexAndRealNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::multiply, bh);
}
@Benchmark
public void divideReal(ComplexAndRealNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::divide, bh);
}
@Benchmark
public void addReal(ComplexAndRealNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::add, bh);
}
@Benchmark
public void subtractReal(ComplexAndRealNumbers numbers, Blackhole bh) {
apply(numbers.getNumbers(), numbers.getNumbers2(), Complex::subtract, bh);
}
}