如何优化一个数据解析器?
注:本文所示代码均为伪代码
假设我们要编写一个函数function parse_data(String Raw):Object[]
用来解析一个列表数据到应用程序之中。在二十一世纪的今天,我们会很自然的联想到使用JSON
来完成这项需求。因为我们可以脱口而出JSON
所有的优点:
1. 兼容性良好。
2. 开源支持力度好。
3. 人类阅读友好。
因此,我们可以很快速的得到一个实现版本。
function parse_data(String raw): Object[] {
let lst: Object[];
lst = JSON.parse_into<Object[]>)(raw)
return lst
}
在相当多的场景里面,以上的实现都堪称完美。假设我们将程序的运行条件限定一下:
1. raw String 可能超过 1GB
2. 物理内存只有256MB
在以上场景的约束下,第一个实现已经无法正常工作了。因为JSON格式必须将其完整的载入内存才可以进行解析,从约束条件来看,raw string 大小已经远远超过了内存限制;同时,将所有数据都解析到内存也是很大的内存开销。从这个分析来看,我们得出了一个结论:第一个版本无法在内存条件苛刻的情况下工作,因此我们需要进行优化。
那么,我们的优化的思考点应该是怎么样的嘞?
我们需要先回过头了看问题,第一个实现的问题是内存占用过大引起的。那么,我们就需要一个方案来减少内存的占用。 减少运行时内存占用 -- 这个就是我们本次优化的哲学指导。
我们已经有了基本的哲学指导思想,那么我们开始进入到具体问题具体分析的阶段去寻找解决方案。我们再深入的思考第一个实现为什么会造成内存占用高:
1. 需要全部载入数据
2. 需要全部将解析结果存到内存
也就是说,我们的新方案只需要解决上面两个问题,也就完成了目标。
经过一番搜索与学习,我们发现可以使用Streaming友好的数据格式(Msgpack,CSV)来作为raw string,同时将数据使用sqlite来存储解析后的结果。
此时此刻,我们就得到了第二个实现版本。
function parse_data(String filePath): Object[] {
let db_conn = Sqlite.open("./cache.db")
CsvReader
.open(filePath)
.each((line)=>{
db_conn.insert_into(convert_line_to_object(line))
})
}
这个实现通过将raw string 放入磁盘,同时利用csv行间隔离的特性。通过流式的方式将数据迁移到本地db中。对于物理内存的需求基本是趋近于O(1)的。从而解决了我们上面提出的两个问题。
那么,这个版本就完美了吗?
从代码逻辑上来看,这个版本对于数据量的限制从内存转移到了磁盘,在实际过程中可以认为解决了数据量代码无法工作的问题。但是它仍然存在一个问题 -- 磁盘IO过于频繁,磁盘IO表现在两个方面:
1. 读取csv
2. 写入sqlite
那么,我们是否有可能再次优化嘞?
function parse_data(String filePath): Object[] {
let file_lst = CsvUtil.split_file_by_lines(filePath,1000) // per csv file 1000 line
let db_conn = Sqlite.open("./cache.db")
foreach file_lst as file:
Thread.run(()=>{
let lst: Object[];
CsvReader
.open(file)
.each((line)=>{
lst.append(convert_line_to_object(line))
})
db_conn.batch_insert(lst);
})
end
wait_all_thread_done()
}
基于我们提出的疑问,我们编写了这个版本。这个版本提出了批处理的概念。也就是将一个任务拆分成数个互相独立的子任务,同时引入了多线程来发挥多核优势。
到此,我们就实现了一个多核加速、数据量无上限(假设磁盘无上限)的数据解析器
。当然,同时代码复杂度也指数级上。
总结
本文主要目的是利用了一个实际案例,来讨论我们做性能优化的思辨过程:
1. 在优化之前,发现主要矛盾
2. 根据主要矛盾的来推演我们需要的解决方案
3. 寻找解决方案,并且确定该解决方案可以解决问题
4. 根据需要看是否需要继续优化,如果优化就回到#1。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。