2

为了实现一些图像分析算法而不必过多担心数据类型(即没有太多重复代码),我正在为 Java 中的原始数组设置访问者模式。

在下面的示例中,我定义了两种类型的访问者

  • 原始类型,其中visit方法的签名是visit(int, int double)
  • 泛型类型,其中visit方法的签名是visit(int, int Double).

Appart 由此,两个访问者执行完全相同的操作。我的想法是尝试衡量装箱/拆箱的成本。

所以这是完整的程序

public class VisitorsBenchmark {
    public interface Array2DGenericVisitor<TYPE, RET> {

        void begin(int width, int height);

        RET end();

        void visit(int x, int y, TYPE value);
    }

    public interface Array2DPrimitiveVisitor<RET> {

        void begin(final int width, final int height);

        RET end();

        void visit(final int x, final int y, final double value);
    }

    public static <RET>
        RET
        accept(final int width,
               final int height,
               final double[] data,
               final Array2DGenericVisitor<Double, RET> visitor) {

        final int size = width * height;
        visitor.begin(width, height);
        for (int i = 0, x = 0, y = 0; i < size; i++) {
            visitor.visit(x, y, data[i]);
            x++;
            if (x == width) {
                x = 0;
                y++;
                if (y == height) {
                    y = 0;
                }
            }
        }
        return visitor.end();
    }

    public static <RET> RET accept(final int width,
                                   final int height,
                                   final double[] data,
                                   final Array2DPrimitiveVisitor<RET> visitor) {

        final int size = width * height;
        visitor.begin(width, height);
        for (int i = 0, x = 0, y = 0; i < size; i++) {
            visitor.visit(x, y, data[i]);
            x++;
            if (x == width) {
                x = 0;
                y++;
                if (y == height) {
                    y = 0;
                }
            }
        }
        return visitor.end();
    }

    private static final Array2DGenericVisitor<Double, double[]> generic;

    private static final Array2DPrimitiveVisitor<double[]> primitive;

    static {
        generic = new Array2DGenericVisitor<Double, double[]>() {
            private double[] sum;

            @Override
            public void begin(final int width, final int height) {

                final int length = (int) Math.ceil(Math.hypot(WIDTH, HEIGHT));
                sum = new double[length];
            }

            @Override
            public void visit(final int x, final int y, final Double value) {

                final int r = (int) Math.round(Math.sqrt(x * x + y * y));
                sum[r] += value;
            }

            @Override
            public double[] end() {

                return sum;
            }
        };

        primitive = new Array2DPrimitiveVisitor<double[]>() {
            private double[] sum;

            @Override
            public void begin(final int width, final int height) {

                final int length = (int) Math.ceil(Math.hypot(WIDTH, HEIGHT));
                sum = new double[length];
            }

            @Override
            public void visit(final int x, final int y, final double value) {

                final int r = (int) Math.round(Math.sqrt(x * x + y * y));
                sum[r] += value;
            }

            @Override
            public double[] end() {

                return sum;
            }
        };
    }

    private static final int WIDTH = 300;

    private static final int HEIGHT = 300;

    private static final int NUM_ITERATIONS_PREHEATING = 10000;

    private static final int NUM_ITERATIONS_BENCHMARKING = 10000;

    public static void main(String[] args) {

        final double[] data = new double[WIDTH * HEIGHT];
        for (int i = 0; i < data.length; i++) {
            data[i] = Math.random();
        }

        /*
         * Pre-heating.
         */
        for (int i = 0; i < NUM_ITERATIONS_PREHEATING; i++) {
            accept(WIDTH, HEIGHT, data, generic);
        }
        for (int i = 0; i < NUM_ITERATIONS_PREHEATING; i++) {
            accept(WIDTH, HEIGHT, data, primitive);
        }

        /*
         * Benchmarking proper.
         */
        double[] sumPrimitive = null;
        double[] sumGeneric = null;

        double aux = System.nanoTime();
        for (int i = 0; i < NUM_ITERATIONS_BENCHMARKING; i++) {
            sumGeneric = accept(WIDTH, HEIGHT, data, generic);
        }
        final double timeGeneric = System.nanoTime() - aux;

        aux = System.nanoTime();
        for (int i = 0; i < NUM_ITERATIONS_BENCHMARKING; i++) {
            sumPrimitive = accept(WIDTH, HEIGHT, data, primitive);
        }
        final double timePrimitive = System.nanoTime() - aux;

        System.out.println("prim = " + timePrimitive);
        System.out.println("generic = " + timeGeneric);
        System.out.println("generic / primitive = "
                           + (timeGeneric / timePrimitive));
    }
}

我知道 JIT 非常聪明,所以当两位访问者的表现同样出色时,我并没有太惊讶。更令人惊讶的是,通用访问者的执行速度似乎比原始访问者稍快,这是出乎意料的。我知道基准测试有时会很困难,所以我一定做错了什么。你能发现错误吗?

非常感谢你的帮助!!!塞巴斯蒂安

[编辑]我更新了代码以说明预热阶段(为了让 JIT 编译器完成它的工作)。这不会改变始终低于 1 (0.95 - 0.98) 的结果。

4

3 回答 3

2

小贴士:

  • 不要Math.random()用于执行基准测试,因为结果是不确定的。你需要 smth like new Random(xxx)
  • 总是打印操作的结果。在一次执行中混合基准类型是不好的做法,因为它可能导致不同的调用站点优化(但不是你的情况)
  • 辅助 = System.nanoTime(); - 并非所有人都longs适合双打 - 正确。
  • 发布您执行基准测试的环境和硬件规格
  • 在启用打印编译-XX:-PrintCompilation垃圾收集 -verbosegc -XX:+PrintGCDetails的同时打印'staring test' - GC 可以在'错误'测试期间启动,足以扭曲结果。


编辑:

我确实检查了生成的汇编程序,但它们都不是真正的原因。Double.valueOf() 没有分配,因为该方法完全内联并优化了 - 它仅使用 CPU 寄存器。然而,没有硬件规范/JVM,没有真正的答案。

我发现了一个 JVM (1.6.0.26),其中通用版本 ( Double) 具有更好的循环展开 (!),这是由于更深入的分析(显然需要 EA Double.valueOf())和可能的 WIDTH/HEIGHT 的恒定折叠。将 WIDTH/HEIGHT 更改为一些素数,结果应该不同。


底线是:除非您知道 JVM 如何优化并检查生成的机器代码,否则不要使用微基准测试。


免责声明:我不是 JVM 工程师

于 2012-09-10T14:30:16.327 回答
2

我知道基准测试有时会很困难,所以我一定做错了什么。你能发现错误吗?

我认为问题在于您的基准测试没有考虑 JVM 预热。将您的主要方法的主体放入另一个方法中。然后让您的main方法在循环中重复调用该新方法。最后,检查结果,并丢弃前几个因 JIT 编译和其他预热效果而失真的结果。

于 2012-09-10T12:46:45.903 回答
0

这是一个完全“疯狂的猜测”,但我认为它与将字节复制到堆栈上有关。传递原始双精度涉及在堆栈上复制 8 个字节。传递 Double 只需要复制指针。

于 2012-09-10T14:50:07.090 回答