【问题标题】:Reading a CSV file with 50M lines, how to improve performance读取50M行的CSV文件,如何提高性能
【发布时间】:2021-08-03 19:18:25
【问题描述】:

我有一个 CSV(逗号分隔值)格式的数据文件,其中包含大约 5000 万行。

每一行都被读入一个字符串,解析,然后用于填充 FOO 类型对象的字段。然后该对象被添加到最终有 5000 万个项目的 List(of FOO) 中。

一切正常,并且适合内存(至少在 x64 机器上),但速度很慢。每次加载并将文件解析到列表中大约需要 5 分钟。我想让它更快。 我怎样才能让它更快?

代码的重要部分如下所示。

Public Sub LoadCsvFile(ByVal FilePath As String)
    Dim s As IO.StreamReader = My.Computer.FileSystem.OpenTextFileReader(FilePath)

    'Find header line
    Dim L As String
    While Not s.EndOfStream
        L = s.ReadLine()
        If L = "" Then Continue While 'discard blank line
        Exit While
    End While
    'Parse data lines
    While Not s.EndOfStream
        L = s.ReadLine()
        If L = "" Then Continue While 'discard blank line          
        Dim T As FOO = FOO.FromCSV(L)
       Add(T)
    End While
    s.Close()
End Sub


Public Class FOO
    Public time As Date
    Public ID As UInt64
    Public A As Double
    Public B As Double
    Public C As Double

    Public Shared Function FromCSV(ByVal X As String) As FOO
        Dim T As New FOO
        Dim tokens As String() = X.Split(",")
        If Not DateTime.TryParse(tokens(0), T.time) Then
            Throw New Exception("Could not convert CSV to FOO:  Invalid ISO 8601 timestamp")
        End If
        If Not UInt64.TryParse(tokens(1), T.ID) Then
            Throw New Exception("Could not convert CSV to FOO:  Invalid ID")
        End If
        If Not Double.TryParse(tokens(2), T.A) Then
            Throw New Exception("Could not convert CSV to FOO:  Invalid Format for A")
        End If
        If Not Double.TryParse(tokens(3), T.B) Then
            Throw New Exception("Could not convert CSV to FOO:  Invalid Format for B")
        End If
        If Not Double.TryParse(tokens(4), T.C) Then
            Throw New Exception("Could not convert CSV to FOO:  Invalid Format for C")
        End If
        Return T
    End Function
End Class

我做了一些基准测试,结果如下。

  • 上面的完整算法需要 314 秒来加载整个文件并将对象放入列表中。
  • FromCSV() 的主体被简化为只返回一个 FOO 类型的新对象和默认字段值,整个过程耗时 84 秒。因此,将文本行处理到对象字段中似乎需要 230 秒(占总时间的 73%)。
  • 除了解析 ISO 8601 日期字符串之外的所有操作都需要 175 秒。因此,处理日期字符串似乎需要 139 秒,这是文本处理时间的 60%,仅针对该字段。
  • 仅读取文件中的行而不进行任何处理或创建对象需要 41 秒。
  • 使用 StreamReader.ReadBlock 以大约 1KB 的块读取整个文件需要 24 秒,但它在总体方案上的改进很小,可能不值得增加复杂性。为了使用 TryParse,我现在需要手动创建临时字符串,而不是使用 String.Split()。

此时我看到的唯一途径是每隔几秒钟向用户显示一次状态,这样他们就不会怀疑程序是否被冻结或其他原因。

更新
我创建了两个新功能。可以使用 System.IO.BinaryWriter 将数据集从内存保存到二进制文件中。另一个函数可以使用 System.IO.BinaryReader 将该二进制文件加载回内存。二进制版本比 CSV 版本快得多,而且二进制文件占用的空间要少得多。

以下是基准测试结果(所有测试使用相同的数据集):

  • 加载 CSV:340 秒
  • 保存 CSV:312 秒
  • 保存箱:29 秒
  • 装载箱:41 秒
  • CSV 文件大小:3.86GB
  • BIN 文件大小:1.63GB

【问题讨论】:

标签: vb.net performance csv file-io


【解决方案1】:

我在 CSV 方面拥有丰富的经验,但坏消息是您无法让这一切变得更快。 CSV 库在这里不会有太大帮助。库试图处理的 CSV 的难题是处理嵌入了逗号或换行符的字段,这些字段需要引用和转义。您的数据集没有这个问题,因为所有列都不是字符串。

正如您所发现的,大部分时间都花在了解析方法上。 Andrew Morton 有一个很好的建议,对 DateTime 值使用 TryParseExact 可能比 TryParse 快很多。我自己的 CSV 库 Sylvan.Data.Csv(它是 .NET 中最快的),它使用了一种优化,它直接从流读取缓冲区中解析原始值,而不首先转换为字符串(仅在 .NET 核心上运行时) ,这也可以加快速度。但是,我不希望在坚持使用 CSV 的同时将处理时间缩短一半。

这是使用我的库Sylvan.Data.Csv 在 C# 中处理 CSV 的示例。

static List<Foo> Read(string file)
{
    // estimate of the average row length based on Andrew Morton's 4GB/50m
    const int AverageRowLength = 80;

    var textReader = File.OpenText(file);
    // specifying the DateFormat will cause TryParseExact to be used.
    var csvOpts = new CsvDataReaderOptions { DateFormat = "yyyy-MM-ddTHH:mm:ss" };
    var csvReader = CsvDataReader.Create(textReader, csvOpts);

// estimate number of rows to avoid growing the list.
    var estimatedRows = (int)(textReader.BaseStream.Length / AverageRowLength);            
    var data = new List<Foo>(estimatedRows);

    while (csvReader.Read())
    {
        if (csvReader.RowFieldCount < 5) continue;
        var item = new Foo()
        {
            time = csvReader.GetDateTime(0),
            ID = csvReader.GetInt64(1),
            A = csvReader.GetDouble(2),
            B = csvReader.GetDouble(3),
            C = csvReader.GetDouble(4)
        };
        data.Add(item);
    }
    return data;
}

我希望这比您当前的实现要快一些,只要您在 .NET 核心上运行。在 .NET 框架上运行,差异(如果有的话)不会很重要。但是,我不希望您的用户可以接受这样的速度,读取整个文件仍然可能需要几十秒或几分钟。

鉴于此,我的建议是完全放弃 CSV,这意味着您可以放弃解析,这会减慢速度。相反,以二进制形式读取和写入数据。您的数据记录有一个很好的属性,它们是固定宽度:每条记录包含 5 个 8 字节(64 位)宽的字段,因此每条记录需要 40 字节的二进制形式。 50 米 x 40 = 2GB。因此,假设 Andrew Morton 对 CSV 的 4GB 估计是正确的,转向二进制将使存储需求减半。即刻,这意味着读取相同数据所需的磁盘 IO 是原来的一半。但除此之外,您不需要解析任何内容,值的二进制表示基本上会直接复制到内存中。

以下是一些如何在 C# 中执行此操作的示例(不太了解 VB,抱歉)。


static List<Foo> Read(string file)
{
    var stream = File.OpenRead(file);
    // the exact number of records can be determined by looking at the length of the file.
    var recordCount = stream.Length / 40;
    var data = new List<Foo>(recordCount);
    var br = new BinaryReader(stream);
    for (int i = 0; i < recordCount; i++)
    {
        var ticks = br.ReadInt64();
        var id = br.ReadInt64();
        var a = br.ReadDouble();
        var b = br.ReadDouble();
        var c = br.ReadDouble();
        var f = new Foo()
        {
            time = new DateTime(ticks),
            ID = id,
            A = a,
            B = b,
            C = c,
        };
        data.Add(f);
    }
    return data;
}

static void Write(List<Foo> data, string file)
{
    var stream = File.Create(file);
    var bw = new BinaryWriter(stream);
    foreach(var item in data)
    {
        bw.Write(item.time.Ticks);
        bw.Write(item.ID);
        bw.Write(item.A);
        bw.Write(item.B);
        bw.Write(item.C);
    }
}

这几乎肯定比基于 CSV 的解决方案快一个数量级。那么问题就变成了:您是否有某些理由必须使用 CSV?如果数据的来源不在您的控制范围内,而您必须使用 CSV,那么我会问:数据文件会每次都更改,还是只会附加新数据?如果它被附加到,我会研究一个解决方案,每次应用程序启动时只转换附加的 CSV 数据的新部分并将其添加到二进制文件中,然后您将从中加载所有内容。那么你只需要支付每次处理新的CSV数据的成本,并且会从二进制形式快速加载所有内容。

这可以通过创建固定布局结构 (Foo)、分配它们的数组以及使用基于跨度的技巧直接从 FileStream 读取数组数据来更快。之所以可以这样做,是因为您的所有数据元素都是“可blittable”的。这将是将此数据加载到您的程序中的绝对最快的方法。从 BinaryReader/Writer 开始,如果您发现它仍然不够快,请进行调查。

如果您发现此解决方案有效,我很想听听结果。

【讨论】:

  • 有一个随着时间增长的大型数据集。数据可能以 CSV 文件或 JSON 格式从 Web 服务接收。一遍又一遍地分析相同的增长数据集。因此,将数据转换为二进制,然后再添加就很有意义。我可能会采用这种方法。
猜你喜欢
  • 2019-04-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-03-21
  • 1970-01-01
  • 2011-09-26
  • 2012-08-09
  • 2020-12-09
相关资源
最近更新 更多