处理大型数据集和即时加载

9
我有一个用C# (.NET 4.0)编写的.NET应用程序。在这个应用程序中,我们需要从文件中读取大量数据集,并在类似于网格的结构中显示内容。因此,为了实现这一点,我在窗体上放置了一个DataGridView。它有3列,所有列数据都来自文件。最初,文件中有约600,000条记录,对应于DataGridView中的600,000行。
我很快发现,当数据集很大时,DataGridView会崩溃,因此我必须切换到虚拟模式。为了实现这一点,我首先将文件完全读入3个不同的数组(对应于3个列),然后当CellValueNeeded事件触发时,我从数组中提供正确的值。
然而,这个文件可能有很多(非常多!)记录,正如我们很快发现的那样。当记录大小非常大时,将所有数据读入数组或List<>等中似乎是不可行的。我们很快遇到了内存分配错误(Out of memory exception)。
我们陷入了困境,但随后意识到,为什么要先将数据全部读入数组中,而不是在CellValueNeeded事件触发时按需读取文件呢?所以现在我们的做法是:我们打开文件,但不读取任何内容,当CellValueNeeded事件触发时,我们首先使用Seek()方法定位到文件中的正确位置,然后读取相应的数据。
这是我们能想到的最好的方法,但首先它非常慢,使应用程序变得迟缓且不友好。其次,我们无法不考虑是否有更好的方法来实现这一点。例如,某些二进制编辑器(如HXD)对于任何文件大小都非常快,因此我想知道如何实现这一点。
哦,还有一个问题,即在DataGridView的虚拟模式下,当我们将RowCount设置为文件中可用的行数(例如16,000,000行)时,DataGridView甚至需要一段时间才能初始化自身。对于这个“问题”,任何意见都将不胜感激。
谢谢
5个回答

5
如果您的整个数据集无法放入内存中,则需要一种缓冲方案。与其仅在响应于CellValueNeeded时读取所需填充DataGridView的数据量,您的应用程序应预测用户的操作并提前读取数据。因此,例如,当程序首次启动时,应读取前10,000条记录(或只有1,000条或者可能是100,000条——根据您的情况而定)。然后,可以立即从内存中填充CellValueNeeded请求。
随着用户在网格中移动,您的程序尽可能地保持领先一步。如果用户超越了您(例如,想要从前面跳到结尾),则可能会出现短暂的暂停,并且您必须访问磁盘以满足请求。
该缓冲通常最好由一个单独的线程来完成,尽管如果线程正在预期用户的下一个操作而进行读取,而用户却执行了完全意外的操作,例如跳转到列表的开头,则同步有时可能会成为问题。
1600万条记录并不是非常多,除非记录非常大,或者您的服务器上没有太多内存。当然,1600万远远不是List 的最大大小,除非T是值类型(结构体)。这里您说的数据有多少GB?

2
你好Jim,T是一个有4个双精度浮点数的结构体。因此,4816M = 512MB的数据。 - SomethingBetter
我尝试使用.NET MemoryMappedFile,但是一旦你创建一个视图,它似乎会尝试将文件加载到内存中,因为我遇到了内存不足的异常。我认为MemoryMappedFile可能会在内部将数据访问分段到页面,并且只将所需的页面加载到内存中。 - SomethingBetter
@SomethingBetter:如果你使用的是32位机器,那么512 MB可能是个问题。如果你使用内存映射文件,你需要将你的视图大小设置为小于整个文件大小。然后,当用户浏览数据时,你可以调整你的视图。 - Jim Mischel

4

好的,这里有一个看起来更好的解决方案:

步骤0:将dataGridView.RowCount设置为较低的值,例如25(或适合您的表单/屏幕的实际数字)

步骤1:禁用dataGridView的滚动条。

步骤2:添加自己的滚动条。

步骤3:在CellValueNeeded例程中,响应e.RowIndex + scrollBar.Value

步骤4:至于dataStore,我目前打开一个流,在CellValueNeeded例程中,首先执行Seek()和Read()所需的数据。

通过这些步骤,我可以非常合理地滚动数据网格以查看非常大的文件(已测试达到0.8GB)。

因此,总的来说,减速的实际原因似乎不是我们一直在执行Seek()和Read(),而是dataGridView本身。


没错。使用 StringBuilder(5000000) 将同一个 DataSet 显示在 TextBox 中,速度大约快了 4 倍。 - TomeeNS

1
管理可以滚动、小计、用于多列计算等的行和列,提出了一系列独特的挑战;这个问题与编辑器遇到的问题并不公平可比。第三方数据网格控件自VB6时代以来一直在解决客户端显示和操作大型数据集的问题。使用按需加载或自包含客户端巨大数据集实现真正快速的性能并非易事。按需加载可能会受到服务器端延迟的影响;在客户端上操作整个数据集可能会受到内存和CPU限制的影响。一些支持即时加载的第三方控件提供了客户端和服务器端逻辑,而其他控件则尝试100%在客户端解决问题。

1

0
为了解决这个问题,我建议不要一次性加载所有数据。相反,分块加载数据,并在需要时显示最相关的数据。我进行了快速测试,发现将DataGridViewDataSource属性设置为一个很好的方法,但是对于大量行,它也需要时间。因此,使用DataTable的Merge函数来分块加载数据并向用户显示最相关的数据。这里我演示了一个可以帮助您的示例。

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