jmhjavamicrobenchmark测试浮点打印的随机数据

jmhjavamicrobenchmark测试浮点打印的随机数据,java,performance-testing,floating-point-conversion,microbenchmark,jmh,Java,Performance Testing,Floating Point Conversion,Microbenchmark,Jmh,我正在为我编写的浮点打印代码编写一个JMH微基准。我并不过分担心确切的性能,但是要正确地使用基准代码 我想循环一些随机生成的数据,所以我制作了一些静态的数据数组,并尽可能简单地保持循环机制的增量和掩码。这是正确的方法还是我应该告诉JMH更多一些关于我丢失的注释的情况 此外,是否可以为测试创建显示组,而不仅仅是字典顺序?我基本上有两组测试,每组随机数据一组 完整资料来源于 以下是基准代码: package zerog.util.grisu; import java.util.Random; i

我正在为我编写的浮点打印代码编写一个JMH微基准。我并不过分担心确切的性能,但是要正确地使用基准代码

我想循环一些随机生成的数据,所以我制作了一些静态的数据数组,并尽可能简单地保持循环机制的增量和掩码。这是正确的方法还是我应该告诉JMH更多一些关于我丢失的注释的情况

此外,是否可以为测试创建显示组,而不仅仅是字典顺序?我基本上有两组测试,每组随机数据一组

完整资料来源于

以下是基准代码:

package zerog.util.grisu;

import java.util.Random;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

/* 
 * Current JMH bench, similar on small numbers (no fast path code yet)
 * and 40% faster on completely random numbers.
 * 
 * Benchmark                         Mode  Cnt         Score         Error  Units
 * JmhBenchmark.test_lowp_doubleto  thrpt   20  11439027.798 ± 2677191.952  ops/s
 * JmhBenchmark.test_lowp_grisubuf  thrpt   20  11540289.271 ±  237842.768  ops/s
 * JmhBenchmark.test_lowp_grisustr  thrpt   20   5038077.637 ±  754272.267  ops/s
 * 
 * JmhBenchmark.test_rand_doubleto  thrpt   20   1841031.602 ±  219147.330  ops/s
 * JmhBenchmark.test_rand_grisubuf  thrpt   20   2609354.822 ±   57551.153  ops/s
 * JmhBenchmark.test_rand_grisustr  thrpt   20   2078684.828 ±  298474.218  ops/s
 * 
 * This doens't account for any garbage costs either since the benchmarks
 * aren't generating enough to trigger GC, and Java internally uses per-thread
 * objects to avoid some allocations.
 * 
 * Don't call Grisu.doubleToString() except for testing. I think the extra
 * allocations and copying are killing it. I'll fix that.
 */

public class JmhBenchmark {

    static final int nmask = 1024*1024 - 1;
    static final double[] random_values = new double[nmask + 1];
    static final double[] lowp_values = new double[nmask + 1];

    static final byte[] buffer = new byte[30];
    static final byte[] bresults = new byte[30];

    static int i = 0;
    static final Grisu g = Grisu.fmt;

    static {

        Random r = new Random();
        int[] pows = new int[] { 1, 10, 100, 1000, 10000, 100000, 1000000 };

        for( int i = 0; i < random_values.length; ++i ) {
            random_values[i] = r.nextDouble();
        }

        for(int i = 0; i < lowp_values.length; ++i ) {
            lowp_values[i] = (1 + r.nextInt( 10000 )) / pows[r.nextInt( pows.length )];
        }
    }

    @Benchmark
    public String test_rand_doubleto() {
        String s = Double.toString( random_values[i] );
        i = (i + 1) & nmask;
        return s;
    }

    @Benchmark
    public String test_lowp_doubleto() {
        String s = Double.toString( lowp_values[i] );
        i = (i + 1) & nmask;
        return s;
    }

    @Benchmark
    public String test_rand_grisustr() {
        String s =  g.doubleToString( random_values[i] );
        i = (i + 1) & nmask;
        return s;
    }

    @Benchmark
    public String test_lowp_grisustr() {
        String s =  g.doubleToString( lowp_values[i] );
        i = (i + 1) & nmask;
        return s;
    }

    @Benchmark
    public byte[] test_rand_grisubuf() {
        g.doubleToBytes( bresults, 0, random_values[i] );
        i = (i + 1) & nmask;
        return bresults;
    }

    @Benchmark
    public byte[] test_lowp_grisubuf() {
        g.doubleToBytes( bresults, 0, lowp_values[i] );
        i = (i + 1) & nmask;
        return bresults;
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(".*" + JmhBenchmark.class.getSimpleName() + ".*")
                .warmupIterations(20)
                .measurementIterations(20)
                .forks(1)
                .build();

        new Runner(opt).run();
    }
}

不幸的是,您没有正确测量这一点。JVM有很多机会优化您的代码,因为尽管您试图添加一些随机控制流,但它是相当可预测的。例如:

String s = Double.toString( random_values[i] );
i = (i + 1) & nmask;
return s;
随机_值是静态最终字段中的固定数组。由于i的增量相当直接,因此在最坏的情况下,它的值可以完全确定,以便简单地设置s。i是动态的,但当nmask再次具有确定性时,它并没有真正逃逸。JVM仍然可以在这里优化代码,而无需查看程序集,我就可以告诉您具体内容


相反,应该使用非最终实例字段作为值,向类添加@State注释,并使用@setup注释的方法设置测试。如果您这样做,JMH会采取措施正确地脱离您的状态,以防止JVM在面对确定性值时进行优化。

不幸的是,您没有正确地度量这一点。JVM有很多机会优化您的代码,因为尽管您试图添加一些随机控制流,但它是相当可预测的。例如:

String s = Double.toString( random_values[i] );
i = (i + 1) & nmask;
return s;
随机_值是静态最终字段中的固定数组。由于i的增量相当直接,因此在最坏的情况下,它的值可以完全确定,以便简单地设置s。i是动态的,但当nmask再次具有确定性时,它并没有真正逃逸。JVM仍然可以在这里优化代码,而无需查看程序集,我就可以告诉您具体内容


相反,应该使用非最终实例字段作为值,向类添加@State注释,并使用@setup注释的方法设置测试。如果您这样做,JMH将采取措施正确脱离您的状态,以防止JVM在面对确定性值时进行优化。

您只能通过分析其结果来证明基准测试是正确的。基准代码只能引起您必须跟进的危险信号。我在您的代码中看到了这些危险信号:

依赖静态最终字段来存储状态。这些字段的内容可以常规地内联到计算中,从而使基准测试的部分内容无效。JMH只保存@State对象中的常规字段

使用静态初始值设定项。虽然这在当前JMH中没有影响,但预期的方法是使用@Setup方法初始化状态。对于您的情况,它也有助于获得真正随机的数据点,例如,如果您在开始下一次测试迭代之前设置@SetupLevel.Iteration以重新初始化值


就一般方法而言,这是实现安全循环的方法之一:将循环计数器置于方法之外。还有一个可以说是安全的:在方法中循环数组,但将每个迭代结果放入黑洞。消耗。

您只能通过分析其结果来证明基准测试是正确的。基准代码只能引起您必须跟进的危险信号。我在您的代码中看到了这些危险信号:

依赖静态最终字段来存储状态。这些字段的内容可以常规地内联到计算中,从而使基准测试的部分内容无效。JMH只保存@State对象中的常规字段

使用静态初始值设定项。虽然这在当前JMH中没有影响,但预期的方法是使用@Setup方法初始化状态。对于您的情况,它也有助于获得真正随机的数据点,例如,如果您在开始下一次测试迭代之前设置@SetupLevel.Iteration以重新初始化值


就一般方法而言,这是实现安全循环的方法之一:将循环计数器置于方法之外。还有一个可以说是安全的:在方法中循环数组,但将每个迭代结果都放入Blackhole.consume。

我认为根据和的建议展示一个实现会很有帮助

关键变化是:

向所有基准测试提供相同的随机数据集。 这是通过将数据集序列化为temp文件,通过@Param机制提供安装方法的路径,然后反序列化d文件来实现的 将ata导入实例字段

每个基准对整个数据集运行这些方法。我们使用OperationsPeriossion功能来获取准确的时间

所有操作的结果都通过黑洞机制消耗掉

我创建了两个示例,一个基于原始问题,使用可直接使用的可序列化数据集类,另一个测试每个人最喜欢的非序列化类(可选)

如果Aleksey或Rafael或任何人有任何建议,他们将不胜感激

使用可序列化的数据集

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import org.openjdk.jmh.annotations.Benchmark;
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.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

/**
 * In this example each benchmark loops over the entire randomly generated data set.
 * The same data set is used for all benchmarks.
 * And we black hole the results.
 */
@SuppressWarnings("javadoc")
@State(Scope.Benchmark)
public class JmhBenchmark {

    static final int DATA_SET_SAMPLE_SIZE = 1024 * 1024;

    static final Random RANDOM = new Random();

    static final Grisu g = Grisu.fmt;

    double[] random_values;

    double[] lowp_values;

    byte[] bresults;

    @Param("dataSetFilename")
    String dataSetFilename;

    @Setup
    public void setup() throws FileNotFoundException, IOException, ClassNotFoundException {

        try (FileInputStream fis = new FileInputStream(new File(this.dataSetFilename));
                ObjectInputStream ois = new ObjectInputStream(fis)) {

            final DataSet dataSet = (DataSet) ois.readObject();

            this.random_values = dataSet.random_values;
            this.lowp_values = dataSet.lowp_values;
        }

        this.bresults = new byte[30];
    }

    @Benchmark
    public void test_rand_doubleto(final Blackhole bh) {

        for (double random_value : this.random_values) {

            bh.consume(Double.toString(random_value));
        }
    }

    @Benchmark
    public void test_lowp_doubleto(final Blackhole bh) {

        for (double lowp_value : this.lowp_values) {

            bh.consume(Double.toString(lowp_value));
        }
    }

    @Benchmark
    public void test_rand_grisustr(final Blackhole bh) {

        for (double random_value : this.random_values) {

            bh.consume(g.doubleToString(random_value));
        }
    }

    @Benchmark
    public void test_lowp_grisustr(final Blackhole bh) {

        for (double lowp_value : this.lowp_values) {

            bh.consume(g.doubleToString(lowp_value));
        }
    }

    @Benchmark
    public void test_rand_grisubuf(final Blackhole bh) {

        for (double random_value : this.random_values) {

            bh.consume(g.doubleToBytes(this.bresults, 0, random_value));
        }
    }

    @Benchmark
    public void test_lowp_grisubuf(final Blackhole bh) {

        for (double lowp_value : this.lowp_values) {

            bh.consume(g.doubleToBytes(this.bresults, 0, lowp_value));
        }
    }

    /**
     * Serializes an object containing random data. This data will be the same for all benchmarks.
     * We pass the file name via the "dataSetFilename" parameter.
     *
     * @param args the arguments
     */
    public static void main(final String[] args) {

        try {
            // clean up any old runs as data set files can be large
            deleteTmpDirs(JmhBenchmark.class.getSimpleName());

            // create a tempDir for the benchmark
            final Path tempDirPath = createTempDir(JmhBenchmark.class.getSimpleName());

            // create a data set file
            final Path dateSetFilePath = Files.createTempFile(tempDirPath,
                    JmhBenchmark.class.getSimpleName() + "DataSet", ".ser");
            final File dateSetFile = dateSetFilePath.toFile();
            dateSetFile.deleteOnExit();

            // create the data
            final DataSet dataset = new DataSet();

            try (FileOutputStream fos = new FileOutputStream(dateSetFile);
                    ObjectOutputStream oos = new ObjectOutputStream(fos)) {
                oos.writeObject(dataset);
                oos.flush();
                oos.close();
            }

            final Options opt = new OptionsBuilder().include(JmhBenchmark.class.getSimpleName())
                .param("dataSetFilename", dateSetFile.getAbsolutePath())
                .operationsPerInvocation(DATA_SET_SAMPLE_SIZE)
                .mode(org.openjdk.jmh.annotations.Mode.All)
                .timeUnit(TimeUnit.MICROSECONDS)
                .forks(1)
                .build();

            new Runner(opt).run();

        } catch (final Exception e) {
            System.err.println(e.getMessage());
            e.printStackTrace();
            throw new RuntimeException(e);
        }

    }

    static Path createTempDir(String prefix) throws IOException {
        final Path tempDirPath = Files.createTempDirectory(prefix);
        tempDirPath.toFile()
            .deleteOnExit();
        return tempDirPath;
    }

    static void deleteTmpDirs(final String prefix) throws IOException {

        for (Path dir : Files.newDirectoryStream(new File(System.getProperty("java.io.tmpdir")).toPath(),
                prefix + "*")) {
            for (Path toDelete : Files.walk(dir)
                .sorted(Comparator.reverseOrder())
                .toArray(Path[]::new)) {
                Files.delete(toDelete);
            }
        }
    }

    static final class DataSet implements Serializable {

        private static final long serialVersionUID = 2194487667134930491L;

        private static final int[] pows = new int[] { 1, 10, 100, 1000, 10000, 100000, 1000000 };

        final double[] random_values = new double[DATA_SET_SAMPLE_SIZE];

        final double[] lowp_values = new double[DATA_SET_SAMPLE_SIZE];

        DataSet() {

            for (int i = 0; i < DATA_SET_SAMPLE_SIZE; i++) {
                this.random_values[i] = RANDOM.nextDouble();
            }

            for (int i = 0; i < DATA_SET_SAMPLE_SIZE; i++) {
                this.lowp_values[i] = (1 + RANDOM.nextInt(10000)) / pows[RANDOM.nextInt(pows.length)];
            }
        }

    }
}

我认为根据和的建议展示一个实现是有帮助的

关键变化是:

向所有基准测试提供相同的随机数据集。 这是通过将数据集序列化为temp文件,通过@Param机制提供安装方法的路径,然后将数据反序列化为实例字段来实现的

每个基准对整个数据集运行这些方法。我们使用OperationsPeriossion功能来获取准确的时间

所有操作的结果都通过黑洞机制消耗掉

我创建了两个示例,一个基于原始问题,使用可直接使用的可序列化数据集类,另一个测试每个人最喜欢的非序列化类(可选)

如果Aleksey或Rafael或任何人有任何建议,他们将不胜感激

使用可序列化的数据集

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import org.openjdk.jmh.annotations.Benchmark;
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.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

/**
 * In this example each benchmark loops over the entire randomly generated data set.
 * The same data set is used for all benchmarks.
 * And we black hole the results.
 */
@SuppressWarnings("javadoc")
@State(Scope.Benchmark)
public class JmhBenchmark {

    static final int DATA_SET_SAMPLE_SIZE = 1024 * 1024;

    static final Random RANDOM = new Random();

    static final Grisu g = Grisu.fmt;

    double[] random_values;

    double[] lowp_values;

    byte[] bresults;

    @Param("dataSetFilename")
    String dataSetFilename;

    @Setup
    public void setup() throws FileNotFoundException, IOException, ClassNotFoundException {

        try (FileInputStream fis = new FileInputStream(new File(this.dataSetFilename));
                ObjectInputStream ois = new ObjectInputStream(fis)) {

            final DataSet dataSet = (DataSet) ois.readObject();

            this.random_values = dataSet.random_values;
            this.lowp_values = dataSet.lowp_values;
        }

        this.bresults = new byte[30];
    }

    @Benchmark
    public void test_rand_doubleto(final Blackhole bh) {

        for (double random_value : this.random_values) {

            bh.consume(Double.toString(random_value));
        }
    }

    @Benchmark
    public void test_lowp_doubleto(final Blackhole bh) {

        for (double lowp_value : this.lowp_values) {

            bh.consume(Double.toString(lowp_value));
        }
    }

    @Benchmark
    public void test_rand_grisustr(final Blackhole bh) {

        for (double random_value : this.random_values) {

            bh.consume(g.doubleToString(random_value));
        }
    }

    @Benchmark
    public void test_lowp_grisustr(final Blackhole bh) {

        for (double lowp_value : this.lowp_values) {

            bh.consume(g.doubleToString(lowp_value));
        }
    }

    @Benchmark
    public void test_rand_grisubuf(final Blackhole bh) {

        for (double random_value : this.random_values) {

            bh.consume(g.doubleToBytes(this.bresults, 0, random_value));
        }
    }

    @Benchmark
    public void test_lowp_grisubuf(final Blackhole bh) {

        for (double lowp_value : this.lowp_values) {

            bh.consume(g.doubleToBytes(this.bresults, 0, lowp_value));
        }
    }

    /**
     * Serializes an object containing random data. This data will be the same for all benchmarks.
     * We pass the file name via the "dataSetFilename" parameter.
     *
     * @param args the arguments
     */
    public static void main(final String[] args) {

        try {
            // clean up any old runs as data set files can be large
            deleteTmpDirs(JmhBenchmark.class.getSimpleName());

            // create a tempDir for the benchmark
            final Path tempDirPath = createTempDir(JmhBenchmark.class.getSimpleName());

            // create a data set file
            final Path dateSetFilePath = Files.createTempFile(tempDirPath,
                    JmhBenchmark.class.getSimpleName() + "DataSet", ".ser");
            final File dateSetFile = dateSetFilePath.toFile();
            dateSetFile.deleteOnExit();

            // create the data
            final DataSet dataset = new DataSet();

            try (FileOutputStream fos = new FileOutputStream(dateSetFile);
                    ObjectOutputStream oos = new ObjectOutputStream(fos)) {
                oos.writeObject(dataset);
                oos.flush();
                oos.close();
            }

            final Options opt = new OptionsBuilder().include(JmhBenchmark.class.getSimpleName())
                .param("dataSetFilename", dateSetFile.getAbsolutePath())
                .operationsPerInvocation(DATA_SET_SAMPLE_SIZE)
                .mode(org.openjdk.jmh.annotations.Mode.All)
                .timeUnit(TimeUnit.MICROSECONDS)
                .forks(1)
                .build();

            new Runner(opt).run();

        } catch (final Exception e) {
            System.err.println(e.getMessage());
            e.printStackTrace();
            throw new RuntimeException(e);
        }

    }

    static Path createTempDir(String prefix) throws IOException {
        final Path tempDirPath = Files.createTempDirectory(prefix);
        tempDirPath.toFile()
            .deleteOnExit();
        return tempDirPath;
    }

    static void deleteTmpDirs(final String prefix) throws IOException {

        for (Path dir : Files.newDirectoryStream(new File(System.getProperty("java.io.tmpdir")).toPath(),
                prefix + "*")) {
            for (Path toDelete : Files.walk(dir)
                .sorted(Comparator.reverseOrder())
                .toArray(Path[]::new)) {
                Files.delete(toDelete);
            }
        }
    }

    static final class DataSet implements Serializable {

        private static final long serialVersionUID = 2194487667134930491L;

        private static final int[] pows = new int[] { 1, 10, 100, 1000, 10000, 100000, 1000000 };

        final double[] random_values = new double[DATA_SET_SAMPLE_SIZE];

        final double[] lowp_values = new double[DATA_SET_SAMPLE_SIZE];

        DataSet() {

            for (int i = 0; i < DATA_SET_SAMPLE_SIZE; i++) {
                this.random_values[i] = RANDOM.nextDouble();
            }

            for (int i = 0; i < DATA_SET_SAMPLE_SIZE; i++) {
                this.lowp_values[i] = (1 + RANDOM.nextInt(10000)) / pows[RANDOM.nextInt(pows.length)];
            }
        }

    }
}

为什么是非决赛?既然数组内容不能是最终的,我就不应该受到任何持续的影响吗?我要玩-pref perfasm。以前我试着查看堆积如山的装配,但生成的太多了。当不使用决赛时,邹可以更改设置范围。Shoild没有多大区别,但是要避免static.FTY,在perfasm开始工作后,static没有得到优化,正如您所期望的那样,长度是一个常量。我之所以让它们保持静态,是因为我基本上运行三个不同的基准测试,每次测试都需要相同的数据。没有真正为基准组想法提供注释。我正在将它们制作成3个不同的基准测试——更多的代码、更多的复制和粘贴,但灵活性更好,然后我将用设置注释修复这些静态。谢谢你的建议。为什么是非决赛?既然数组内容不能是最终的,我就不应该受到任何持续的影响吗?我要玩-pref perfasm。以前我试着查看堆积如山的装配,但生成的太多了。当不使用决赛时,邹可以更改设置范围。Shoild没有多大区别,但是要避免static.FTY,在perfasm开始工作后,static没有得到优化,正如您所期望的那样,长度是一个常量。我之所以让它们保持静态,是因为我基本上运行三个不同的基准测试,每次测试都需要相同的数据。没有真正为基准组想法提供注释。我正在将它们制作成3个不同的基准测试——更多的代码、更多的复制和粘贴,但灵活性更好,然后我将用设置注释修复这些静态。感谢您的建议。在1上,只有数组引用是final,而java final的内容基本上没有任何用处,而且我只在早期的编译中看到了final的优化。代码达到了相同的结束状态,但由于final,可能需要更少的comp迭代次数。现在正在研究如何执行2和perfasm。另外,如何将循环计数器置于方法之外?你的意思是在调用级方法中使用增量?我最终希望每组基准测试都有相同的静态数据,例如,所有的兰德基准测试都有相同的双精度,所以我没有进行静态初始化。我添加了一些几乎需要这样做的其他基准。但我确实向类添加了线程状态注释。谢谢你的提示。你上面所做的就是把循环计数器放在外面,这是实现安全循环的方法之一@Setup/TearDownInvocation是有代价的,因此它将循环计数器增量放在@Benchmark本身可能是较小的缺点。我基本上是懒惰的,在同一个类中保留了3个不同的基准。在我将它们正确地放入单独的基准类/文件之后,我将能够用bechmark级别的注释修复设置。JITed代码在基准主体周围看起来也不错,除了一个我似乎无法理解的令人费解的arraycopy提升。再次感谢。在1上,只有数组引用是final,而不是java final的内容,因为final的存在,我只在早期的编译中看到过优化。代码达到了相同的结束状态,但由于final,可能需要更少的comp迭代次数。现在正在研究如何执行2和perfasm。另外,如何将循环计数器置于方法之外?你的意思是在调用级方法中使用增量?我最终还是想要同样的
每组基准测试的静态数据,例如,所有的兰德基准测试都要获得相同的双倍,所以我只剩下静态初始化。我添加了一些几乎需要这样做的其他基准。但我确实向类添加了线程状态注释。谢谢你的提示。你上面所做的就是把循环计数器放在外面,这是实现安全循环的方法之一@Setup/TearDownInvocation是有代价的,因此它将循环计数器增量放在@Benchmark本身可能是较小的缺点。我基本上是懒惰的,在同一个类中保留了3个不同的基准。在我将它们正确地放入单独的基准类/文件之后,我将能够用bechmark级别的注释修复设置。JITed代码在基准主体周围看起来也不错,除了一个我似乎无法理解的令人费解的arraycopy提升。再次感谢。