4
\$\begingroup\$

Intro

(The previous version is here.)

(The next version is here.)

Now I have incorporated nice answers made by Chris and Chip01.

What changed:

  1. Fixed the computation of median,
  2. RunStatistics is now a record.

Code


package io.github.coderodde.statistics.run;

import java.text.NumberFormat;
import java.util.Locale;

/**
 * This record class encapsulates the run statistics.
 * 
 * @author Rodion "rodde" Efremov
 */
public final record RunStatistics(long minimumDuration,
                                  long maximumDuration,
                                  double meanDuration,
                                  double medianDuration,
                                  double standardDeviation) {
    
    @Override
    public String toString() {
        // NumberFormat.format() is not thread-safe, so instantiate and call 
        // here:
        final NumberFormat nf = NumberFormat.getInstance(Locale.getDefault());
        
        return new StringBuilder("min = ")
          .append(nf.format(minimumDuration))
          .append(" ns, max = ")
          .append(nf.format(maximumDuration))
          .append(" ns, mean = ")
          .append(meanDuration)
          .append(" ns, median = ")
          .append(medianDuration)
          .append(" ns, sd = ")
          .append(standardDeviation)
          .append(" ns")
          .toString();
    }
}

package io.github.coderodde.statistics.run;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * This class provides methods for obtaining the running time statistics.
 * 
 * @author Rodion "rodde" Efremov
 */
public final class Runner {
    
    private static final int MINIMUM_ITERATIONS = 1;

    public static RunStatistics measure(final Runnable runnable, 
                                        final int iterations) {
        Objects.requireNonNull(runnable, "The input runnable is null");
        
        checkIterations(iterations);
        
        long minimumDuration = Long.MAX_VALUE;
        long maximumDuration = Long.MIN_VALUE;
        long meanDuration = 0;
        double medianDuration;
        double standardDeviation;
        final List<Long> durations = new ArrayList<>(iterations);
        
        for (int iteration = 0;
                 iteration < iterations;
                 iteration++) {
            
            final long duration = measure(runnable);
            
            minimumDuration = Math.min(minimumDuration, duration);
            maximumDuration = Math.max(maximumDuration, duration);
            meanDuration += duration;
            durations.add(duration);
        }
        
        meanDuration /= iterations;
        medianDuration = computeMedianDuration(durations);
        standardDeviation = computeStandardDeviation(durations, meanDuration);
        
        return new RunStatistics(minimumDuration,
                                 maximumDuration,
                                 meanDuration, 
                                 medianDuration,
                                 standardDeviation);
    }
    
    public static RunStatistics measure(final List<Runnable> runnables) {
        Objects.requireNonNull(runnables, "The input runnables is null");
        
        if (runnables.isEmpty()) {
            throw new IllegalArgumentException("Nothing to measure");
        }
        
        long minimumDuration = Long.MAX_VALUE;
        long maximumDuration = Long.MIN_VALUE;
        long meanDuration = 0;
        double medianDuration;
        double standardDeviation;
        final List<Long> durations = new ArrayList<>(runnables.size());
        
        for (final Runnable runnable : runnables) {
            
            final long duration = measure(runnable);
            
            minimumDuration = Math.min(minimumDuration, duration);
            maximumDuration = Math.max(maximumDuration, duration);
            meanDuration += duration;
            durations.add(duration);
        }
        
        meanDuration /= runnables.size();
        medianDuration = computeMedianDuration(durations);
        standardDeviation = computeStandardDeviation(durations, meanDuration);
        
        return new RunStatistics(minimumDuration,
                                 maximumDuration,
                                 meanDuration, 
                                 medianDuration,
                                 standardDeviation);
        
    }
    
    private static long measure(final Runnable runnable) {
        final long ta = System.nanoTime();
        runnable.run();
        final long tb = System.nanoTime();
        final long duration = tb - ta;
        return duration;
    }
    
    private static double computeMedianDuration(final List<Long> durations) {
        Collections.sort(durations);
        
        if (durations.size() % 2 == 1) {
            return durations.get(durations.size() / 2);
        } else {
            final int index1 = durations.size() / 2;
            final int index2 = index1 - 1;
            return (durations.get(index1) + durations.get(index2)) / 2.0;
        }
    }
    
    private static double computeStandardDeviation(final List<Long> durations,
                                                   final long meanDuration) {
        double sum = 0.0;
        
        for (final Long duration : durations) {
            sum += Math.pow(duration - meanDuration, 2.0);
        }
        
        return Math.sqrt(sum / durations.size());
    }
    
    private static void checkIterations(final int iterations) {
        if (iterations < MINIMUM_ITERATIONS) {
            final String exceptionMessage = 
                    String.format("Number of iterations (%d) is too small. " + 
                                  "Must be at least %d.", 
                                  iterations,
                                  MINIMUM_ITERATIONS);
            
            throw new IllegalArgumentException(exceptionMessage);
        }
    }
}

package io.github.coderodde.statistics.run.demo;

import io.github.coderodde.statistics.run.RunStatistics;
import io.github.coderodde.statistics.run.Runner;
import java.util.Random;

public class Demo {

    private static final Random RANDOM = new Random(13L);
    
    public static void main(String[] args) {
        final RunStatistics runStatistics = Runner.measure(() -> {
            try {
                Thread.sleep(RANDOM.nextInt(100));
            } catch (InterruptedException ex) {}
        }, 20);
        
        System.out.println(runStatistics);
    }
}

Typical output

min = 18,600 ns, max = 98,308,200 ns, mean = 4.873858E7 ns, median = 5.643875E7 ns, sd = 3.0280325961531524E7 ns

Critique request

Please, tell me anything that comes to mind.

\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

DRY

If you're sorting your durations to calculate the median, why bother calculating the minimum and maximum on a rolling basis as you collect them, rather than just accessing the first and last value once they're sorted?

Doing it the latter way reduces repeated effort.

\$\endgroup\$
0
4
\$\begingroup\$

I was only able to find three points this time:

  1. Mean precision: You still accumulate meanDuration as a long. Dividing later truncates precision. Switching to double throughout would avoid loss.
  2. Standard deviation formula: You’re using population SD (n in denominator). If you want sample SD, IMO, you should use (n - 1). Right now, results may understate variability for small samples.
  3. Formatting consistency: toString() mixes formatted longs with raw doubles. Consider formatting all values with NumberFormat or DecimalFormat for uniformity.

You might as well put MINIMUM_ITERATIONS high enough to get reasonable results. That is all I was able to spot.

New contributor
Chip01 is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct.
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Regarding accuracy, accumulating integral values using the widest integral type is perfect until there is overflow. Which can be handled. Computing the mean should be done dividing the total by (the count converted to a wide floating point representation). (When accumulating in a floating point value, care has to be taken lest small addends get dropped due to limited width of mantissa.) \$\endgroup\$ Commented 14 hours ago

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.