将大量数据加载到内存中 - 最有效的方法是什么?

22

我正在开发一个基于Web的文档搜索/查看系统,用于为客户提供服务。该系统的一部分是搜索系统,允许客户搜索包含在文档中的术语[s]。我已经创建了必要的搜索数据文件,但需要加载大量数据,加载所有数据需要8-20秒。数据分成40-100个文件,具体取决于需要搜索哪些文档。每个文件大小在40-350kb之间。

此外,该应用程序必须能够在本地文件系统上运行,也可以通过Web服务器运行。

当网页加载完成后,我可以生成需要加载的所有搜索数据文件列表。在网页可以被认为是功能完善之前,必须加载整个列表。

在介绍完这些背景信息之后,让我们看看我现在的做法。

当我知道整个网页都已经加载完成后,我调用一个loadData()函数。

function loadData(){
            var d = new Date();
            var curr_min = d.getMinutes();
            var curr_sec = d.getSeconds();
         var curr_mil = d.getMilliseconds();
         console.log("test.js started background loading, time is: " + curr_min + ":" + curr_sec+ ":" + curr_mil);
          recursiveCall();
      }


   function recursiveCall(){
      if(file_array.length > 0){
         var string = file_array.pop();
         setTimeout(function(){$.getScript(string,recursiveCall);},1);
    }
    else{
        var d = new Date();
        var curr_min = d.getMinutes();
        var curr_sec = d.getSeconds();
        var curr_mil = d.getMilliseconds();
        console.log("test.js stopped background loading, time is: " + curr_min + ":" + curr_sec+ ":" + curr_mil);
    }
  }

这段代码的作用是按顺序处理一个文件数组,在文件之间暂停1毫秒。 这有助于防止浏览器在加载过程中完全被锁定,但浏览器仍然会因为加载数据而变得缓慢。 我正在加载的每个文件都如下所示:

AddToBookData(0,[0,1,2,3,4,5,6,7,8]);
AddToBookData(1,[0,1,2,3,4,5,6,7,8]);
AddToBookData(2,[0,1,2,3,4,5,6,7,8]);

每一行都是一个函数调用,这些函数调用都在往一个数组里添加数据。而 "AddToBookData" 函数只是做了以下操作:

    function AddToBookData(index1,value1){
         BookData[BookIndex].push([index1,value1]);
    }

这是现有的系统。在加载所有数据后,“AddToBookData”可能会被调用100,000+次。

我发现这相当低效,所以我编写了一个脚本来处理包含上述所有函数调用的test.js文件,并将其转换为一个巨大的数组,该数组等同于BookData正在创建的数据结构。 我不再像旧系统那样进行所有函数调用,而是执行以下操作:

var test_array[..........(data structure I need).......]
BookData[BookIndex] = test_array;

我原本以为,由于删去了所有上面的函数调用,这个方法的性能会有所提高,但实际上这个方法创建完全相同的数据结构需要稍微更多的时间。需要注意的是,在我的真实测试中,“test_array”包含略多于90,000个元素。

看起来两种数据加载方法的CPU利用率大致相同。我感到惊讶的是,由于数据结构在预先创建,我本以为第二种方法需要很少的CPU时间。

请指教?


2
你必须一次性加载所有数据吗?我会让它懒加载 - 即仅加载所需的记录。此外,为什么不使用更结构化的格式来存储数据,无论是关系型数据库还是BerkeleyDb等? - jira
很不幸,我需要一次性全部加载。这个应用程序是一个具有搜索功能的文档查看系统。当用户“搜索”关键字时,所有搜索数据都需要立即出现...在早期版本中,我们曾经有过初始加载时间,然后用户使用“搜索”功能时会有额外的加载时间,这是我们想要解决的最大投诉之一。 - user210099
4个回答

16

看起来可以考虑并分别解决两个基本数据加载优化领域:

  1. 从服务器下载数据。你可以通过同时加载多个小文件获得胜利,而不是一个大文件。尝试使用同时加载的数量,注意浏览器限制和过多并行连接的收益递减。请参阅我在 jsfiddle 上的parallelsequential实验,但要记住由于从github拉取测试数据的各种偶然性,结果会有所不同 - 最好在更加严格的条件下使用自己的数据进行测试。
  2. 尽可能高效地构建您的数据结构。您的结果看起来像是多维数组,关于 JavaScript 数组性能的这篇有趣的文章可能会给您一些在此领域进行实验的想法。

但是我不确定仅通过优化数据加载,您真正能够达到多少进展。为了解决应用程序的实际问题(浏览器锁定时间过长),您是否考虑过以下选项?

使用Web Workers

Web Workers 可能不适用于您的所有目标浏览器,但应该可以防止主浏览器线程在处理数据时锁定。

对于不支持 workers 的浏览器,您可以考虑略微增加 setTimeout 间隔,以便给浏览器时间为用户提供服务和处理您的 JS。这将使事情稍微变慢,但与下一个点结合起来可能会增加用户的满意度。

提供进度反馈

对于支持worker的浏览器和不支持worker的浏览器,花一些时间使用进度条更新DOM。由于您知道剩下要加载的文件数,因此进度应该相对稳定,尽管实际上可能会稍微慢一些,但如果用户得到反馈并且不认为浏览器已经锁定,他们会感觉更好,这篇文章中有相关论述。

延迟加载

jira在他的评论中建议的那样。如果Google Instant可以在我们输入时搜索整个网络,那么服务器是否真的无法返回一个包含当前书籍中搜索关键字所有位置的文件?这个文件应该比所有单词位置的文件要小得多,且速度更快,我想这就是您目前正在尝试尽快加载的内容吧?


感谢您的回复和实验性代码!我将查看以半并行方式下载搜索数据。然而,我认为现在的瓶颈不是下载文件,而是一旦下载后处理文件。Web Workers是我之前调查过的解决方案,虽然它们看起来像一个好主意,但我正在一个必须支持IE的企业环境中工作,所以这排除了Web Workers。进度条是一个很好的想法,我已经在我的真实代码中实现了它,但是非常感谢您的建议!它使等待变得可忍。 - user210099

5

我测试了将同一个900万点数据集加载到Firefox 3.64中的三种方法。

1: Stephen's GetJSON Method
2) My function based push method
3) My pre-processed array appending method:

我以两种方式进行了测试:第一次测试我导入了100个文件,每个文件包含10,000行数据,每行数据包含9个数据元素[0,1,2,3,4,5,6,7,8]。
第二次测试中,我尝试将文件合并,这样我只需导入一个包含900万数据点的文件。
虽然这比我将要使用的数据集要大得多,但它可以帮助展示各种导入方法的速度。
Separate files:                 Combined file:

JSON:        34 seconds         34
FUNC-BASED:  17.5               24
ARRAY-BASED: 23                 46

结果很有趣,至少可以这样说。我在每个网页加载完毕后关闭了浏览器,并对每个测试运行了4次以最小化网络流量/变化的影响(通过使用文件服务器跨网络运行)。您看到的数字是平均值,尽管单个运行的差异最多只有一两秒。


1
在你的问题中,你说:“这个程序会按顺序处理一个文件数组,在文件之间需要休眠1毫秒。”但实际上,看着你发布的代码,它并没有这样做。recursiveCall在再次调用recursiveCall()之前要休眠1毫秒,几乎是并行加载所有100个文件而不是按顺序。必须将setTimeout(function(){$.getScript(string,recursiveCall());},1);更改为setTimeout(function(){$.getScript(string,recursiveCall);},1);才能获得你描述的行为,这可能会改变你的结果? - Day
对不起,那是一个打错字!在我的“真实”代码中,它是 $.getScript(string,recursiveCall); 就像你说的一样。不过你发现得真好,我会更新原帖的。 - user210099

0

不要使用$.getScript来加载包含函数调用的JavaScript文件,考虑使用$.getJSON。这可能会提高性能。文件现在应该如下所示:

{
    "key" : 0,
    "values" : [0,1,2,3,4,5,6,7,8]
}

在收到JSON响应后,您可以像这样调用AddToBookData

function AddToBookData(json) {
     BookData[BookIndex].push([json.key,json.values]);
}

如果您的文件中有多组对AddToBookData的调用,您可以按照以下方式进行结构化:
[
    {
        "key" : 0,
        "values" : [0,1,2,3,4,5,6,7,8]
    },
    {
        "key" : 1,
        "values" : [0,1,2,3,4,5,6,7,8]
    },
    {
        "key" : 2,
        "values" : [0,1,2,3,4,5,6,7,8]
    }
]

然后更改AddToBookData函数以适应新的结构:

function AddToBookData(json) {
    $.each(json, function(index, data) {
        BookData[BookIndex].push([data.key,data.values]);
    });
}  

附录
我怀疑无论您使用什么方法将数据从文件传输到BookData数组,真正的瓶颈都在于请求的数量之多。这些文件必须分成40-100个吗?如果您改用JSON格式,则可以加载一个看起来像这样的单个文件:

{
    "file1" : [
        {
            "key" : 0,
            "values" : [0,1,2,3,4,5,6,7,8]
        },
        // all the rest...
    ],
    "file2" : [
        {
            "key" : 1,
            "values" : [0,1,2,3,4,5,6,7,8]
        },
        // yadda yadda
    ]
}

然后你可以做一个请求,加载所有你需要的数据,然后继续下一步...虽然浏览器可能会一开始卡住(也许不会),但这样做可能会快得多。
如果你对JSON还不熟悉,这里有一个不错的教程:http://www.webmonkey.com/2010/02/get_started_with_json/

谢谢,我们曾考虑使用JSON作为数据格式,但由于某些原因它被搁置了。我将尝试将我的测试应用程序转换为JSON格式并报告结果。在测试的初始版本中,我们发现一个文件锁定浏览器的时间过长,导致用户认为浏览器已经死机。也许通过您所概述的新的数据传输方法,这个问题不会那么严重了。 - user210099
是的,我认为最初的锁定是由于加载一个包含90,000个函数调用的单个文件所致。 - Stephen
2
这个 JSON 对象有很多额外的字节和处理要求,因为它具有重复的值名称。将 JSON 数据格式与最终出现在 BookData[BookIndex] 中的格式相同如何?即 JSON 数据更加紧凑,例如 [[0,[0,1,2,3,4,5,6,7,8]],[1,[0,1,2,3,4,5,6,7,8]],...],而 AddToBookData 只需变成 Array.prototype.push.apply(BookData[BookIndex], json)。这样能进一步提高速度吗? - Day
我按照你建议的方式实现了JSON数据下载方法,Day。平均而言,它比Stephen最初的实现快1-3秒。根据Firebug的控制台报告,实际文件传输大约需要2-3毫秒。我认为大部分时间都花在下载后处理这些数据上。 - user210099

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接