2013-02-09 63 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

相關問題