トップOpenStreetMap > OSM Node座標値のメモリ管理

OSM Node座標値のメモリ管理

OSM Node座標値のファイル管理

はじめに

OSMのデータ構造[1]は分かりやすい。しかし、lineデータや polygonデータのノード(頂点)データは ノード番号であって、座標値ではない。

5年ほど使ってきたプログラムでは、日本全体のノード座標値データを複数のブロックに分けて、 圧縮してメモリに載せている。一部のブロックだけをキャッシュとして、解凍している。

もっとシンプルな方法でノードの座標値を管理したい。最初にファイルにおいてスワップする方法を 試みたが、実行時間がかかりすぎることが分かった。

ファイル管理での高速化も可能であるが、メモリ管理には及ばないであろう。 二分探索でいけることが分かったので、現状では、データ圧縮は要らない。 数年後には 5GB では厳しくなるかも知れない。そのときは、なるべく簡単なデータ圧縮方法を考えたい。

ブロック毎のノード座標値管理

現在の OSMのノード数は 10ギガ強である。4バイトは超えるが、5バイトあれば十分である。 ファイル管理では 下位の3バイトをサブID とした。 ブロック数(最大ブロック番号+1)は 610 になった。

メモリ管理の場合には、もっと大きくなっても問題はない。

ブロックサイズが小さくなると、ブロック内の座標値を2バイトの差分コードで表現できる ブロックが出てくるかも知れない。

少なくとも、部分的には差分コード化が可能であるから、 ブロック内にいくつかの差分コードサブブロックと4バイトコードを共存させてもよい。

最初のプログラム

ファイル管理よりも行数はかなり少なくなる。 実行時間はファイル管理でもスワップが起きない状態と比較すれば1割ほど高速化した。

最大メモリ使用量は日本地図の場合で 3.5GB弱となった。

c:\gisa>java -Dfile.encoding=UTF-8 -Xmx5000m Parser c:/osm/kanto.osm
100万 189950106 1.62分
200万 385886896 1.75分
300万 558946275 1.85分
400万 725347405 1.95分
500万 849865383 2.05分
600万 1012313038 2.15分
実行時間: 2.24分

c:\gisa>java -Dfile.encoding=UTF-8 -Xmx5000m Parser
100万 109558932 7.58分
200万 118896760 7.77分
300万 129656349 7.96分
400万 151799302 8.17分
500万 176647489 8.33分
600万 199159815 8.48分
700万 237257311 8.62分
800万 297680074 8.74分
900万 361676392 8.87分
1000万 389144232 8.98分
1100万 420773372 9.09分
1200万 449643588 9.19分
1300万 483282560 9.29分
1400万 523962051 9.39分
1500万 554035212 9.50分
1600万 577758689 9.59分
1700万 603957342 9.69分
1800万 625593406 9.78分
1900万 665255659 9.88分
2000万 704605019 9.99分
2100万 733490874 10.08分
2200万 756457716 10.17分
2300万 790622198 10.28分
2400万 829932859 10.38分
2500万 881160441 10.48分
2600万 930982044 10.58分
2700万 974634995 10.68分
2800万 1006947494 10.77分
2900万 1041709924 10.87分
3000万 1081645714 10.97分
3100万 1117508196 11.07分
実行時間: 11.11分
// javac Parser.java OSMLib.java Tag.java 
// java -Dfile.encoding=UTF-8 -Xmx5000m Parser c:/osm/japan.osm

import java.io.*;
import java.nio.file.*;
import java.util.*;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.DefaultHandler;


class NodeBlock {
    final static int BlockBits = 24;       // Block番号 id >> BlockBits
    final static int BlockSize = 5000000;

	static NodeBlock[] blocks = new NodeBlock[700];

    int[] sids;
    long[] lonlats;

    int sidLast = -1, ixLast = -1, blkLast = -1;

    public NodeBlock(int size) {
        sids = new int[size];
        lonlats = new long[size];
    }

    static int getBlockNumber(long id) { return (int)(id >> BlockBits); }
    static int getSubId(long id) { return (int)(id & ((long)(1<<BlockBits) - 1)); }

    static long getLonLat(long id) {
        int blockNumber = getBlockNumber(id);
        int sid = getSubId(id);     // sub id

        NodeBlock blk = blocks[blockNumber];

        if (Math.abs(sid - blk.sidLast) < 10 && blk.blkLast == blockNumber) {
            if (sid > blk.sidLast) {
                for (int ix = blk.ixLast; blk.sids[ix] <= sid; ix++) {
                    if (blk.sids[ix] == sid) {
                        blk.sidLast = sid;
                        blk.ixLast = ix;
                        blk.blkLast = blockNumber;
                        return blk.lonlats[ix];
                    }
                }
            } else if (sid < blk.sidLast) {
                for (int ix = blk.ixLast; blk.sids[ix] >= sid; ix--) {
                    if (blk.sids[ix] == sid) {
                        blk.sidLast = sid;
                        blk.ixLast = ix;
                        blk.blkLast = blockNumber;
                        return blk.lonlats[ix];
                    }
                }
            }
        }

        int ix = Arrays.binarySearch(blk.sids, sid);
        if (ix >= 0) {
            blk.sidLast = sid;
            blk.ixLast = ix;
            blk.blkLast = blockNumber;
        } else {
            System.out.printf("no! id=%x blk=%x sid=%x\n", id, blockNumber, sid);
        }
        return ix >= 0 ? blk.lonlats[ix] : Long.MIN_VALUE;
    }
}

public class Parser extends DefaultHandler {
    final double E7 = 10000000.0;
    static long start = System.currentTimeMillis();

    int currBlock=-1;
    int cntNodes=0, maxNodes=0;
    boolean fNodeSection = false;
    boolean fWaySection = false;
    int cntWay = 0;
    int[] sids = new int[NodeBlock.BlockSize];  // ノード座標値のパースで使用
    long[] lonlats = new long[NodeBlock.BlockSize];

    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        if (!fWaySection && "node".equals(qName)) {
            if (!fNodeSection) fNodeSection = true; 
            procNode(atts); // nodeオブジェクトの処理
        } else if ("way".equals(qName)) {
            long id = Long.parseLong(atts.getValue("id"));
            if (fNodeSection) {
                fNodeSection = false;   // nodeセクションは終わった
                procNodeEnd();  // nodeセクションの後処理
            }
            if (!fWaySection) {
                fWaySection = true;
            }
            if (++cntWay % 1000000 == 0) {
                System.out.printf("%d万 %d %.2f分\n", cntWay/10000, id,
                             (System.currentTimeMillis() - start)/60000.0);
            }
        } else if (fWaySection && "nd".equals(qName)) {
            long nid = Long.parseLong(atts.getValue("ref"));
            long lonlat = NodeBlock.getLonLat(nid);
            //System.out.printf("%d %d %d %d\n", 
            //  NodeBlock.getBlockNumber(nid), nid, lonlat>>32, lonlat&0xffffffffL);
        }
    }

	void putNodeBlock(int blk, int[] sids, long[] lonlats, int size) {
		NodeBlock block = NodeBlock.blocks[blk] = new NodeBlock(size);
		System.arraycopy(sids, 0, block.sids, 0, size);
		System.arraycopy(lonlats, 0, block.lonlats, 0, size);
	}

    void procNode(Attributes atts) {
        long id = Long.parseLong(atts.getValue("id"));
        int blockNumber = NodeBlock.getBlockNumber(id);
        if (blockNumber != currBlock) {     // 新しいブロックが始まった
            if (currBlock >= 0) {           // 前のブロックをファイルに書き込む
                putNodeBlock(currBlock, sids, lonlats, cntNodes);
                if (cntNodes > maxNodes) maxNodes = cntNodes;
                cntNodes = 0;
            }
            currBlock = blockNumber;
        }
        sids[cntNodes] = NodeBlock.getSubId(id);
        int lon = (int)(Double.parseDouble(atts.getValue("lon")) * E7);
        int lat = (int)(Double.parseDouble(atts.getValue("lat")) * E7);
        lonlats[cntNodes++] = (((long)lon)<<32) + (lat&0xffffffffL);
    }

    void procNodeEnd() {
        putNodeBlock(currBlock, sids, lonlats, cntNodes);
    }

    public static void main( String[] args ) throws Exception {
        long start = System.currentTimeMillis();
        String src = args.length >= 1 ? args[0] : "c:/osm/japan.osm";
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser parser = factory.newSAXParser();
        Parser handler = new Parser();
        parser.parse(Paths.get(src).toFile(), handler);
        System.out.printf("実行時間: %.2f分\n", (System.currentTimeMillis()-start)/60000.0);
    }
}

wayのパース

wayオブジェクトでサイズが大きいのはノード番号列データである。 大半はwayオブジェクト単独で道路・鉄道や川などのラインデータであるから、 その都度、ラインレコードとしてファイル出力すればそれで終わりであるが、 一部の wayオブジェクトは relation オブジェクトのメンバーとなっており、 メンバーかどうかは relation をパースしなければ分からない。 nodeオブジェクトについても同じである。

wayデータを全てメモリ上に置くのであれば、1パスでよいが、 メモリ使用量から厳しい。nodeデータ、wayデータとも可変長バイトコード[4]で圧縮するのであれば 可能であろう。

もう一つの方法は2パス方式である。1パス目は nodeセクション、wayセクションは空読みして、 relationセクションのパースを先に実行する。これで、relationのメンバーとなっている node と way の情報を掴んでおく。2パス目の wayセクションでは、 relationメンバーのみメモリ上に残しておく。

1パス目の空読みのロスタイムはSAXパーサーでは、日本地図の場合で、10分前後になるであろう。

まずは、SAXでの2パスのロスタイムを調べる。7、8分で済むならば、 2パス方式の方がプログラムが簡単であろう。

2パス方式の事前調査

2パス方式の事前調査結果およびそのプログラムを下に示す。

現在のところ、1パス目が8分、2パス目が12分、全体で20分となった。

現在使用中の OSMParser は SAX を使わず、完全な自前プログラムである。 1パス目は4分強である。nodeのパース時間は5分弱である。 SAXでは 1.5~2 倍の時間がかかる。 しかし、パース以外の処理にかかる時間の方が大きいため、 SAXを使うことによる時間増加は2割前後であろう。

また、SAXでも時間短縮の余地はあるだろう。

現在のノード数は 約230M であるから、ノードのサブID、lon、lat に必要なメモリは 正味 230M * 12 ≒ 2.8GB である。実行中の最大メモリ使用量は 3.5GB であるから、 700MB 程度がどこかに使われていることになる。

c:\gisa>java -Dfile.encoding=UTF-8 -Xmx5000m Parser c:/osm/kanto.osm
relation: 1.57分
ブロック数=610 maxNodes=888899 実行時間: 3.64分

c:\gisa>java -Dfile.encoding=UTF-8 -Xmx5000m Parser
relation: 7.97分
ブロック数=610 maxNodes=4329823 totalNodes=227849492 実行時間: 20.05分
    // 第1パス: PreParser  relationセクションのみをパースする
    static class PreParser extends DefaultHandler {

        boolean fRelation = false;
 
        public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
            if (!fRelation && qName.equals("relation")) {
                //System.out.println("relation");
                // relationオブジェクトの開始処理
                if (!fRelation) {
                    fRelation = true;
                }
            }
        }

        public void endElement(String namespaceURI, String localName, String qName) {
            if (fRelation && qName.equals("relation")) {
                // relationオブジェクトの終了処理
                fRelation = false;
            }
        }
    }


    public static void main( String[] args ) throws Exception {
        long start = System.currentTimeMillis();
        String src = args.length >= 1 ? args[0] : "c:/osm/japan.osm";
        SAXParserFactory factory = SAXParserFactory.newInstance();
        SAXParser saxparser = factory.newSAXParser();

        // 第1パス: relationパース
        PreParser preparser = new PreParser();
        saxparser.parse(Paths.get(src).toFile(), preparser);
        System.out.printf("relation: %.2f分\n", (System.currentTimeMillis()-start)/60000.0);

        // 第2パス: node/wayパース&処理、relation処理
        Parser parser = new Parser();
        saxparser.parse(Paths.get(src).toFile(), parser);
        System.out.printf("ブロック数=%d maxNodes=%d 実行時間: %.2f分\n", 
            parser.currBlock+1, parser.maxNodes, (System.currentTimeMillis()-start)/60000.0);
    }

リファレンス

[1] OpenStreetMapのデータ構造
[2] 【Java】binarySearchを使って配列(Array)の要素から値を検索する!
[3] SAXによるXML文書の操作
[4] 可変長バイトコード