如何优化一个数据解析器?

注:本文所示代码均为伪代码

假设我们要编写一个函数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。

imiskolee
495 声望55 粉丝