2013-02-09 59 views
2

我学习有效的Java并在书中,关于避免创建不必要的对象的约书亚·布洛克会谈的第5项。一个示例演示可变Date对象,它们的值一经计算就不会被修改。有效的Java - 相同的方法调用时,尽管创建多个实例

这里的“坏习惯”:

public Person(Date birthDate) { 
    this.birthDate = new Date(birthDate.getTime()); 
} 

// DON'T DO THIS! 
public boolean isBabyBoomer() { 
    // Unnecessary allocation of expensive object 
    Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); 
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0); 
    Date boomStart = gmtCal.getTime(); 
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); 
    Date boomEnd = gmtCal.getTime(); 
    return birthDate.compareTo(boomStart) >= 0 
      && birthDate.compareTo(boomEnd) < 0; 
} 

的isBabyBoomer方法不必要地创建了一个新的日历,时区和两个Date每次被调用的时刻 - 这显然对我来说很有意义。

这里改进的代码:创建只有一次,当它被初始化

public Person(Date birthDate) { 
    this.birthDate = new Date(birthDate.getTime()); 
} 

/** 
* The starting and ending dates of the baby boom. 
*/ 
private static final Date BOOM_START; 
private static final Date BOOM_END; 

static { 
    Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); 
    gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0); 
    BOOM_START = gmtCal.getTime(); 
    gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); 
    BOOM_END = gmtCal.getTime(); 
} 

public boolean isBabyBoomer() { 
    return birthDate.compareTo(BOOM_START) >= 0 
      && birthDate.compareTo(BOOM_END) < 0; 
} 

日历,时区和日期的情况。 布洛赫解释道,这会导致显著的性能提升,如果该方法isBabyBoomer()被频繁调用。

在他的机器:
坏的版本:32,000毫秒1000万调用
改进版:130MS 1000万调用

但是当我运行在我的系统示例中的表现是完全一样的( 14毫秒)。 这是一个编译器功能,实例只创建一次?

编辑:
这里是我的标杆:

public static void main(String[] args) { 
    Calendar cal = Calendar.getInstance(); 
    cal.set(1960, Calendar.JANUARY, 1, 1, 1, 0); 
    Person p = new Person(cal.getTime()); 
    long startTime = System.nanoTime(); 
    for (int i = 0; i < 10000000; i++) { 
     p.isBabyBoomer(); 
    } 
    long stopTime = System.nanoTime(); 
    long elapsedTime = stopTime - startTime; 
    double mseconds = (double) elapsedTime/1000000.0; 
    System.out.println(mseconds); 
} 

干杯,马库斯

+0

您可能需要阅读:http://stackoverflow.com/questions/504103/how-do-i-write-a-correct-micro-benchmark-in-java – 2013-02-09 21:33:08

+1

与现代JVM我希望有与新推出的多种genreation GC技术没有什么不同。 – 2013-02-09 21:36:12

+0

是否**你也调用它1000万次(循环中)(检查循环前后的时间)?如果你只运行过一次,它们都会创建一次对象(虽然在不同的时间),但实际上并不多。 – Dukeling 2013-02-09 21:38:34

回答

4

你的基准是错误的。随着最新的Java 7和适当的热身,我得到这两种方法之间的巨大差异:

Person::main: estimatedSeconds 1 = '8,42' 
Person::main: estimatedSeconds 2 = '0,01' 

以下是完整的可运行代码:

import java.util.Calendar; 
import java.util.Date; 
import java.util.TimeZone; 

public class Person { 
    private Date birthDate; 
    static Date BOOM_START; 
    static Date BOOM_END; 

    public Person(Date birthDate) { 
     this.birthDate = new Date(birthDate.getTime()); 
    } 

    static { 
     Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); 
     gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0); 
     BOOM_START = gmtCal.getTime(); 
     gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); 
     BOOM_END = gmtCal.getTime(); 
    } 

    public boolean isBabyBoomerWrong() { 
     // Unnecessary allocation of expensive object 
     Calendar gmtCal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); 
     gmtCal.set(1946, Calendar.JANUARY, 1, 0, 0, 0); 
     Date boomStart = gmtCal.getTime(); 
     gmtCal.set(1965, Calendar.JANUARY, 1, 0, 0, 0); 
     Date boomEnd = gmtCal.getTime(); 
     return birthDate.compareTo(boomStart) >= 0 
       && birthDate.compareTo(boomEnd) < 0; 
    } 

    public boolean isBabyBoomer() { 
     return birthDate.compareTo(BOOM_START) >= 0 
       && birthDate.compareTo(BOOM_END) < 0; 
    } 

    public static void main(String[] args) { 
     Person p = new Person(new Date()); 

     for (int i = 0; i < 10_000_000; i++) { 
      p.isBabyBoomerWrong(); 
      p.isBabyBoomer(); 
     } 

     long startTime = System.nanoTime(); 

     for (int i = 0; i < 10_000_000; i++) { 
      p.isBabyBoomerWrong(); 
     } 

     double estimatedSeconds = (System.nanoTime() - startTime)/1000000000.0; 
     System.out.println(String.format("Person::main: estimatedSeconds 1 = '%.2f'", estimatedSeconds)); 

     startTime = System.nanoTime(); 

     for (int i = 0; i < 10_000_000; i++) { 
      p.isBabyBoomer(); 
     } 

     estimatedSeconds = (System.nanoTime() - startTime)/1000000000.0; 
     System.out.println(String.format("Person::main: estimatedSeconds 2 = '%.2f'", estimatedSeconds)); 

    } 
} 
1

你的问题竟然是只是另一种情况一个错误的微基准。

然而,在某些特殊情况下(主要是用简单的数据保持类),实在是一个JVM的优化,它放弃对象实例的大多数。你可能想看看下面的链接。

的方法描述有明显不适合你的情况,但可能会导致其他一些奇怪的情况下,对象实例化只是似乎哪里都不taky任何时间上的差异。所以记住这个当你真正在你的问题的工作实例来为:

最相关的部分:

典型的防御性复制方法返回一个复合值 (真的不担心代码,它只是一个Point将 实例化和访问通过当被调用时 getDistanceFrom()方法getter方法):

public class Point { 
    private int x, y; 
    public Point(int x, int y) { 
     this.x = x; this.y = y; 
    } 
    public Point(Point p) { this(p.x, p.y); } 
    public int getX() { return x; } 
    public int getY() { return y; } 
} 

public class Component { 
    private Point location; 
    public Point getLocation() { return new Point(location); } 
    public double getDistanceFrom(Component other) { 
     Point otherLocation = other.getLocation(); 
     int deltaX = otherLocation.getX() - location.getX(); 
     int deltaY = otherLocation.getY() - location.getY(); 
     return Math.sqrt(deltaX*deltaX + deltaY*deltaY); 
    } 
} 

getLocation()方法不硝酸钾w它的来电者将如何 做Point它返回;它可能会保留对它的引用,例如将它放在一个集合中,所以getLocation()的防守编码为 。然而在这个例子中,getDistanceFrom()不会去 来做到这一点;它只是在短时间内使用Point,然后丢弃它,这看起来像浪费了一个完美的物体。

智能JVM可以看到发生了什么并优化了防御副本的分配 。首先,调用getLocation()将是 内联,如将要getX()getY(),导致 getDistanceFrom()呼叫有效地表现这样的:

(伪代码描述施加内联优化 到getDistanceFrom()的结果)

public double getDistanceFrom(Component other) { 
    Point otherLocation = new Point(other.x, other.y); 
    int deltaX = otherLocation.x - location.x; 
    int deltaY = otherLocation.y - location.y; 
    return Math.sqrt(deltaX*deltaX + deltaY*deltaY); 
} 

此时,逃逸分析可以显示 第一行中分配的对象永远不会从其基本块中逃脱,而 getDistanceFrom()永远不会不同于其他组件的状态。 (通过转义,我们的意思是指对它的引用不存储在堆 中或传递给可能保留副本的未知代码。)假定 Point是真正的线程本地的并且其生命周期已知有界 通过在它被分配的基本块,它可以是 堆栈分配的或优化掉完全,如图这里:

伪代码描述 getDistanceFrom()优化掉分配结果:

public double getDistanceFrom(Component other) { 
    int tempX = other.x, tempY = other.y; 
    int deltaX = tempX - location.x; 
    int deltaY = tempY - location.y; 
    return Math.sqrt(deltaX*deltaX + deltaY*deltaY); 
} 

结果是我们得到的结果与我们在 所有字段都是公开的情况下的性能完全相同,同时保留了封装和防御性复制(以及其他安全编码 技术)给我们的安全性。

+0

感谢这个伟大的解释! – Markus 2013-02-09 22:24:41

相关问题