使用二进制格式存储double数组,加载速度提升100倍

问题

遇到一个配置文件,里面有近10000个double类型的数字,每个数字1行,加载到内存里变成List<Double>,在小米3上加载时间需要200多ms。这种写法可读性好,但牺牲了速度和文件大小。而这个文件的使用场景则是越快起好,使用文本方式储存double数组就显得非常naive了。

文件内容类似下面这样

1
2
3
4
5
6
-0.07442092964745924 
-0.2802236054615799
0

-9.125085940770578e-018
0.264550123930026

......

解决方案

先Google了一下,StackOverflow上有人问了这个问题,答案是在DataOutputStream, ObjectOutputStream, FileChannel三者之间FileChannel最快,因此直接上FileChannel。

研究了一下Java的NIO(FileChannel, DoubleBuffer等)。使用内存映射重写了加载的逻辑。重写后加载时间只需要2ms了,速度快了100倍。文件大小也减小了约 60%。

经过研究发现StackOverflow的回答者给的代码还有很大优化空间,写int数组时使用提for循环,一个一个写,其实可以使用IntBuffer一次性写进去,这个速度应该快很多。

下面是写入文件的代码,使用了Magic Number 技巧。实际使用中可以再增加版本号等其他 header 字段做进一步的校验,方便以后扩展文件格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static final int INT_SIZE = Integer.SIZE / Byte.SIZE;
private static final int DOUBLE_SIZE = Double.SIZE / Byte.SIZE;

private static final int MAGIC_NUMBER = 0x23424123;

private static void writeFileByChannel(File outputFile, double[] data) {
RandomAccessFile fos = null;
try {
fos = new RandomAccessFile(outputFile, "rw");
FileChannel channel = fos.getChannel();
long length = INT_SIZE * 2 + data.length * DOUBLE_SIZE;
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, length);

buffer.putInt(MAGIC_NUMBER);
buffer.putInt(data.length);

DoubleBuffer doubleBuffer = buffer.asDoubleBuffer();
doubleBuffer.put(data);
} catch (IOException e) {
e.printStackTrace();
} finally {
closeQuietly(fos);
}
}

读取的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static double[] readFileByChannel(File inputFile) {
FileInputStream fis = null;
try {
fis = new FileInputStream(inputFile);
FileChannel channel = fis.getChannel();
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
int magicNumber = buffer.getInt();
if (magicNumber != MAGIC_NUMBER) {
Log.e(TAG, "wrong file format, magic=" + Integer.toHexString(magicNumber));
return null;
}

int size = buffer.getInt();

double[] doubles = new double[size];
buffer.asDoubleBuffer().get(doubles);
channel.close();
return doubles;
} catch (IOException e) {
e.printStackTrace();
} finally {
closeQuietly(fis);
}
return null;
}

Note:

需要注意的一点是,验证修改前后两种方法读取的数据是否相同时,比较符点数是否相等,不能使用 ==,而应该使用 Double.compare(double double1, double double2)Float.compare(float float1, float float2)

Android兼容性:

FileChannelMappedByteBufferFileChannel.map()DoubleBufferDouble.compare这些API从API Level 1就有的,可以放心使用。(但NIO2的API则无法使用。)