[英]Java performance puzzler: wrapper classes faster than primitive types?
为了实现某些图像分析算法,而不必太担心数据类型(即,没有太多重复的代码),我为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)。
我知道基准测试有时会很困难,所以我一定做错了。 您能发现错误吗?
我认为问题在于基准测试未考虑JVM预热。 将主体方法的主体放在另一个方法中。 然后让您的main
方法在循环中重复调用该新方法。 最后,检查结果,并丢弃因JIT编译和其他预热效果而失真的前几个结果。
小提示:
Math.random()
进行基准测试,因为结果不确定。 您需要像new Random(xxx)
。 longs
适合双打-正确。 -XX:-PrintCompilation
和垃圾回收的同时打印' -verbosegc -XX:+PrintGCDetails
- -verbosegc -XX:+PrintGCDetails
-GC可以在“错误”测试期间启动,仅足以-verbosegc -XX:+PrintGCDetails
结果。 我确实检查了生成的汇编程序,但它们都不是真正的原因。 没有对Double.valueOf()进行分配,因为该方法已全部内联并已优化,仅使用CPU寄存器。 但是,没有硬件规格/ JVM,没有真正的答案。
我发现了一个JVM(1.6.0.26),其中,由于进行了更深入的分析(对EA Double.valueOf()
显然是必需的)以及WIDTH / HEIGHT可能不断折叠 ,因此通用版本( Double
)具有更好的循环展开(!)。 将WIDTH / HEIGHT更改为一些质数 ,结果应有所不同。
免责声明:我不是JVM工程师
这完全是“疯狂的猜测”,但我认为这与将字节复制到堆栈有关。 传递原始双精度型涉及在堆栈上复制8个字节。 传递Double仅需要复制指针。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.