2

我正在学习有效的 Java,在本书的第 5 项中,Joshua Bloch 谈到了避免创建不必要的对象。一个示例演示了可变的 Date 对象,一旦它们的值被计算出来就永远不会被修改。

这里的“坏习惯”:

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 方法在每次调用时都会不必要地创建一个新的 Calendar、TimeZone 和两个 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;
}

Calendar、TimeZone 和 Date 实例在初始化时只创建一次。isBabyBoomer()Bloch 解释说,如果频繁调用该方法,这会显着提高性能。

在他的机器上:
错误版本:1000 万次调用需要 32,000 毫秒
改进版本:1000 万次调用需要 130毫秒

但是当我在我的系统上运行示例时,性能完全相同(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);
}

干杯,马库斯

4

2 回答 2

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));

    }
}
于 2013-02-09T21:53:25.933 回答
1

你的问题原来只是另一个错误的微基准测试。

但是,在某些特殊情况下(主要是简单的数据保存类),确实存在一种 JVM 优化,它丢弃了大多数对象实例化。您可能想查看下面的链接。

那里描述的方法显然不适用于您的情况,但它可能会在其他一些奇怪的情况下有所不同,其中对象实例化似乎没有任何时间。因此,当您实际遇到问题的工作示例时,请记住这一点:

最相关的部分:

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

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()方法不知道它的调用者将如何处理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);
}

结果是,如果所有字段都是公共的,我们将获得完全相同的性能,同时保持封装和防御性复制(以及其他安全编码技术)给我们的安全性。

于 2013-02-09T22:04:07.340 回答