2

私はEffectiveJavaを研究しており、本の項目5で、JoshuaBlochが不要なオブジェクトの作成の回避について説明しています。例は、値が計算された後は変更されない可変の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、および2つの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インスタンスは、初期化時に1回だけ作成されます。isBabyBoomer()Blochは、メソッドが頻繁に呼び出されると、パフォーマンスが大幅に向上すると説明しています。

彼のマシン上:
悪いバージョン:1000万回の呼び出しで32,000ミリ秒
改善されたバージョン:1000万回の呼び出しで130ミリ秒

しかし、システムで例を実行すると、パフォーマンスはまったく同じです(14ms)。これは、インスタンスが1回だけ作成されるコンパイラ機能ですか?

編集:
これが私のベンチマークです:

    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

あなたのベンチマークは間違っています。最新のJava7と適切なウォームアップを使用すると、2つの方法に劇的な違いが生じます。

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 に答える