2

メモリ不足にならないようにこのコードを最適化する方法はありますか?

import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Random;
import java.util.Stack;

public class TilePuzzle {

    private final static byte ROWS = 4;
    private final static byte COLUMNS = 4;
    private static String SOLUTION = "123456789ABCDEF0";
    private static byte RADIX = 16;

    private char[][] board = new char[ROWS][COLUMNS];
    private byte x; // row of the space ('0')
    private byte y; // column of the space ('0') private String representation;
    private boolean change = false; // Has the board changed after the last call to toString?

    private TilePuzzle() {
        this(SOLUTION);
        int times = 1000;
        Random rnd = new Random();
        while(times-- > 0) {
            try {
                move((byte)rnd.nextInt(4));
            } catch(RuntimeException e) {
            }
        }
        this.representation = asString();
    }

    public TilePuzzle(String representation) {
        this.representation = representation;
        final byte SIZE = (byte)SOLUTION.length();
        if (representation.length() != SIZE) {
            throw new IllegalArgumentException("The board must have " + SIZE + "numbers.");
        }
        boolean[] used = new boolean[SIZE];
        byte idx = 0;
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                char digit = representation.charAt(idx++);
                byte number = (byte)Character.digit(digit, RADIX);
                if (number < 0 || number >= SIZE) {
                    throw new IllegalArgumentException("The character " + digit + " is not valid.");
                } else if(used[number]) {
                    throw new IllegalArgumentException("The character " + digit + " is repeated.");
                }
                used[number] = true;
                board[i][j] = digit;
                if (digit == '0') {
                    x = i;
                    y = j;
                }
            }
        }
    }

    /**
     * Swap position of the space ('0') with the number that's up to it.
     */
    public void moveUp() {
        try {
            move((byte)(x - 1), y);
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    /**
     * Swap position of the space ('0') with the number that's down to it.
     */
    public void moveDown() {
        try {
            move((byte)(x + 1), y);
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    /**
     * Swap position of the space ('0') with the number that's left to it.
     */
    public void moveLeft() {
        try {
            move(x, (byte)(y - 1));
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    /**
     * Swap position of the space ('0') with the number that's right to it.
     */
    public void moveRight() {
        try {
            move(x, (byte)(y + 1));
        } catch(IllegalArgumentException e) {
            throw new RuntimeException("Move prohibited " + e.getMessage());
        }
    }

    private void move(byte movement) {
        switch(movement) {
        case 0: moveUp(); break;
        case 1: moveRight(); break;
        case 2: moveDown(); break;
        case 3: moveLeft(); break;
        }
    }

    private boolean areValidCoordinates(byte x, byte y) {
        return (x >= 0 && x < ROWS && y >= 0 && y < COLUMNS);
    }

    private void move(byte nx, byte ny) {
        if (!areValidCoordinates(nx, ny)) {
            throw new IllegalArgumentException("(" + nx + ", " + ny + ")");
        }
        board[x][y] = board[nx][ny];
        board[nx][ny] = '0';
        x = nx;
        y = ny;
        change = true;
    }

    public String printableString() {
        StringBuilder sb = new StringBuilder();
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                sb.append(board[i][j] + " ");
            }
            sb.append("\r\n");
        }
        return sb.toString();
    }

    private String asString() {
        StringBuilder sb = new StringBuilder();
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                sb.append(board[i][j]);
            }
        }
        return sb.toString();
    }

    public String toString() {
        if (change) {
            representation = asString();
        }
        return representation;
    }

    private static byte[] whereShouldItBe(char digit) {
        byte idx = (byte)SOLUTION.indexOf(digit);
        return new byte[] { (byte)(idx / ROWS), (byte)(idx % ROWS) };
    }

    private static byte manhattanDistance(byte x, byte y, byte x2, byte y2) {
        byte dx = (byte)Math.abs(x - x2);
        byte dy = (byte)Math.abs(y - y2);
        return (byte)(dx + dy);
    }

    private byte heuristic() {
        byte total = 0;
        for (byte i = 0; i < ROWS; ++i) {
            for (byte j = 0; j < COLUMNS; ++j) {
                char digit = board[i][j];
                byte[] coordenates = whereShouldItBe(digit);
                byte distance = manhattanDistance(i, j, coordenates[0], coordenates[1]);
                total += distance;
            }
        }
        return total;
    }

    private class Node implements Comparable<Node> {
        private String puzzle;
        private byte moves; // number of moves from original configuration
        private byte value; // The value of the heuristic for this configuration.
        public Node(String puzzle, byte moves, byte value) {
            this.puzzle = puzzle;
            this.moves = moves;
            this.value = value;
        }
        @Override
        public int compareTo(Node o) {
            return (value + moves) - (o.value + o.moves);
        }
    }

    private void print(Map<String,String> antecessor) {
        Stack toPrint = new Stack();
        toPrint.add(SOLUTION);
        String before = antecessor.get(SOLUTION);
        while (!before.equals("")) {
            toPrint.add(before);
            before = antecessor.get(before);
        }
        while (!toPrint.isEmpty()) {
            System.out.println(new TilePuzzle(toPrint.pop()).printableString());
        }
    }

    private byte solve() {
        if(toString().equals(SOLUTION)) {
            return 0;
        }

        PriorityQueue<Node> toProcess = new PriorityQueue();
        Node initial = new Node(toString(), (byte)0, heuristic());
        toProcess.add(initial);

        Map<String,String> antecessor = new HashMap<String,String>();
        antecessor.put(toString(), "");

        while(!toProcess.isEmpty()) {
            Node actual = toProcess.poll();
            for (byte i = 0; i < 4; ++i) {
                TilePuzzle t = new TilePuzzle(actual.puzzle);
                try {
                    t.move(i);
                } catch(RuntimeException e) {
                    continue;
                }
                if (t.toString().equals(SOLUTION)) {
                    antecessor.put(SOLUTION, actual.puzzle);
                    print(antecessor);
                    return (byte)(actual.moves + 1);
                } else if (!antecessor.containsKey(t.toString())) {
                    byte v = t.heuristic();
                    Node neighbor = new Node(t.toString(), (byte)(actual.moves + 1), v);
                    toProcess.add(neighbor);
                    antecessor.put(t.toString(), actual.puzzle);
                }
            }
        }
        return -1;
    }

    public static void main(String... args) {
        TilePuzzle puzzle = new TilePuzzle();
        System.out.println(puzzle.solve());
    }
}
4

1 に答える 1

43

問題

根本的な原因は、作成してtoProcessキューとantecessorマップに格納している大量の String オブジェクトです。どうしてそんなことをするのか?

あなたのアルゴリズムを見てください。それぞれに 200 万を超えるノードと 500 万の文字列を格納する必要があるかどうかを確認してください。

調査

プログラムが複雑なため、これを見つけるのは困難でした。実際、私はすべてのコードを理解しようとさえしませんでした。代わりに、Java プロファイラー、サンプラー、および CPU/メモリ使用量モニターであるVisualVMを使用しました。

私はそれを立ち上げました:

そして、メモリ使用量を調べました。私が最初に気づいたのは、大量のオブジェクトを作成しているという (明らかな) 事実です。

これはアプリのスクリーンショットです:

ご覧のとおり、メモリの使用量は膨大です。わずか 40 秒で 2 GB が消費され、ヒープ全体がいっぱいになりました。

行き止まり

を実装していても を実装していないため、最初は問題がNodeクラスに関係していると思っていました。だから私は方法を提供しました:Comparableequals

public boolean equals( Object o ) {
    if( o instanceof Node ) {
        Node other = ( Node ) o;
        return this.value == other.value && this.moves == other.moves;
    }
    return false;
}

しかし、それは問題ではありませんでした。

実際の問題は、上部に記載されているものであることが判明しました。

回避策

前に述べたように、本当の解決策はアルゴリズムを再考することです。それまでの間、他にできることは何でも、問題を遅らせるだけです。

ただし、回避策が役立つ場合があります。1 つは、生成している文字列を再利用することです。あなたはこのTilePuzzle.toString()方法を非常に集中的に使用しています。これにより、重複する文字列が頻繁に作成されます。

文字列順列を生成しているので12345ABCD、数秒で多くの文字列を作成できます。それらが同じ文字列である場合、同じ値で何百万ものインスタンスを作成しても意味がありません。

このString.intern()メソッドを使用すると、文字列を再利用できます。ドキュメントは次のように述べています。

文字列オブジェクトの正規表現を返します。

最初は空である文字列のプールは、クラス String によってプライベートに維持されます。

intern メソッドが呼び出されたときに、 equals() メソッドによって決定されたこの String オブジェクトと等しい文字列がプールにすでに含まれている場合は、プールからの文字列が返されます。それ以外の場合は、この String オブジェクトがプールに追加され、この String オブジェクトへの参照が返されます。

通常のアプリケーションの場合、String.intern()インスタンスを GC で再利用できないため、 を使用するのはお勧めできません。しかし、この場合、いずれにせよマップとキューに参照を保持しているので、それは理にかなっています。

したがって、この変更を行う:

public String toString() {
    if (change) {
        representation = asString();
    }
    return representation.intern(); // <-- Use intern
}

メモリの問題をかなり解決します。

これは変更後のスクリーンショットです。

現在、数分経ってもヒープ使用量が 100 MB に達しません。

補足事項

備考#1

動きが有効かどうかを検証するために例外を使用していますが、これは問題ありません。しかし、それらをキャッチすると、それらを無視するだけです:

try {
    t.move(i);
} catch(RuntimeException e) {
    continue;
}

とにかくそれらを使用しない場合は、最初から例外を作成しないことで、多くの計算を節約できます。そうしないと、何百万もの未使用の例外が作成されます。

次の変更を行います。

if (!areValidCoordinates(nx, ny)) {
    // REMOVE THIS LINE:
    // throw new IllegalArgumentException("(" + nx + ", " + ny + ")");

    // ADD THIS LINE:
    return;
}

代わりに検証を使用します。

// REMOVE THESE LINES:
// try {
//     t.move(i);
// } catch(RuntimeException e) {
//     continue;
// }

// ADD THESE LINES:
if(t.isValidMovement(i)){
    t.move(i);
} else {
    continue;
}

備考 2

新しいインスタンスRandomごとに新しいオブジェクトを作成しています。TilePuzzleプログラム全体で 1 つだけ使用した方がよいでしょう。結局のところ、単一のスレッドしか使用していません。

備考 3

この回避策により、ヒープ メモリの問題は解決されましたが、PermGen に関連する別の問題が発生しました。次のように、単純に PermGen のサイズを増やしました。

java -Xmx1g -Xms1g -XX:MaxPermSize=1g TilePuzzle

備考 4

出力は 49 の場合もあれば 50 の場合もありました。行列は次のように出力されました。

1 2 3 4 
5 6 7 8 
9 A B C 
D E 0 F 

1 2 3 4 
5 6 7 8 
9 A B C 
D E F 0 

... 50回

于 2010-06-22T16:27:20.410 に答える