【问题标题】:How to write super-fast file-streaming code in C#?如何在 C# 中编写超快速的文件流式处理代码?
【发布时间】:2010-10-31 15:39:14
【问题描述】:

我必须将一个大文件拆分成许多小文件。每个目标文件都由偏移量和长度定义为字节数。我正在使用以下代码:

private void copy(string srcFile, string dstFile, int offset, int length)
{
    BinaryReader reader = new BinaryReader(File.OpenRead(srcFile));
    reader.BaseStream.Seek(offset, SeekOrigin.Begin);
    byte[] buffer = reader.ReadBytes(length);

    BinaryWriter writer = new BinaryWriter(File.OpenWrite(dstFile));
    writer.Write(buffer);
}

考虑到我必须调用这个函数大约 100,000 次,它非常慢。

  1. 有没有办法让 Writer 直接连接到 Reader? (也就是说,没有实际将内容加载到内存中的 Buffer 中。)

【问题讨论】:

  • File.OpenRead 和 File.OpenWrite 100,000 会很慢好吗...
  • 您是否完美地拆分文件,即您可以通过将所有小文件连接在一起来重建大文件吗?如果是这样,那里就有节省。如果不是,小文件的范围是否重叠?它们是按偏移量排序的吗?

标签: c# performance streaming cpu utilization


【解决方案1】:

我不相信 .NET 中有任何东西允许复制文件的一部分而不将其缓冲在内存中。然而,我觉得这无论如何都是低效的,因为它需要打开输入文件并多次查找。如果您只是拆分文件,为什么不打开输入文件一次,然后只写如下内容:

public static void CopySection(Stream input, string targetFile, int length)
{
    byte[] buffer = new byte[8192];

    using (Stream output = File.OpenWrite(targetFile))
    {
        int bytesRead = 1;
        // This will finish silently if we couldn't read "length" bytes.
        // An alternative would be to throw an exception
        while (length > 0 && bytesRead > 0)
        {
            bytesRead = input.Read(buffer, 0, Math.Min(length, buffer.Length));
            output.Write(buffer, 0, bytesRead);
            length -= bytesRead;
        }
    }
}

这在每次调用时创建缓冲区时效率低下 - 您可能希望创建一次缓冲区并将其传递给方法:

public static void CopySection(Stream input, string targetFile,
                               int length, byte[] buffer)
{
    using (Stream output = File.OpenWrite(targetFile))
    {
        int bytesRead = 1;
        // This will finish silently if we couldn't read "length" bytes.
        // An alternative would be to throw an exception
        while (length > 0 && bytesRead > 0)
        {
            bytesRead = input.Read(buffer, 0, Math.Min(length, buffer.Length));
            output.Write(buffer, 0, bytesRead);
            length -= bytesRead;
        }
    }
}

请注意,这也会关闭原始代码未关闭的输出流(由于 using 语句)。

重要的一点是,这将更有效地使用操作系统文件缓冲,因为您重复使用相同的输入流,而不是在开始时重新打开文件然后搜索。

认为它会明显更快,但显然你需要尝试一下才能看到......

当然,这假设是连续的块。如果您需要跳过文件的某些部分,您可以从方法外部执行此操作。此外,如果您正在编写非常小的文件,您可能也希望针对这种情况进行优化 - 最简单的方法可能是引入 BufferedStream 包装输入流。

【讨论】:

  • 我知道这是一个两年前的帖子,只是想知道......这仍然是最快的方式吗? (即 .Net 中没有什么新东西需要注意?)。另外,在进入循环之前执行Math.Min 会更快吗?或者更好的是,删除可以通过缓冲区计算的长度参数?抱歉,我很挑剔,把它弄死了!提前致谢。
  • @Smudge202:鉴于这是执行 IO,对 Math.Min 的调用肯定 与性能无关。同时拥有长度参数和缓冲区长度的目的是允许您重用可能超大的缓冲区。
  • 明白了,感谢您回复我。当这里可能有足够好的答案时,我不想开始一个新问题,但你会说,如果你想读取大量文件的前 x 个字节(对于从大量文件中获取 XMP 元数据的目的),仍然建议使用上述方法(进行一些调整)?
  • @Smudge202:上面的代码用于复制。如果您只想读取前x个字节,我仍然会循环,但只是读入一个大小合适的缓冲区,增加读取将在每个字节上适当写入缓冲区的索引迭代。
  • 是的,我对写作部分不太感兴趣,我只是想确认读取一个文件的最快方法也是读取多个文件的最快方法。我想象能够 P/Invoke 文件指针/偏移量,并从那里能够扫描具有相同/更少流/缓冲区的多个文件,在我想象的世界中,这对于我想要的可能会更快实现(尽管不适用于 OP)。如果我不是疯了,最好我开始一个新问题。如果我是,你能告诉我,这样我就不会浪费更多人的时间了吗? :-)
【解决方案2】:

从 C# 执行文件 I/O 的最快方法是使用 Windows ReadFile 和 WriteFile 函数。我编写了一个封装此功能的 C# 类以及一个查看不同 I/O 方法的基准测试程序,包括 BinaryReader 和 BinaryWriter。请参阅我的博文:

http://designingefficientsoftware.wordpress.com/2011/03/03/efficient-file-io-from-csharp/

【讨论】:

  • 感谢您提供详细的博客信息。拥有“好答案”徽章!
【解决方案3】:

length 有多大?您最好重新使用固定大小(中等大小,但不是淫秽)的缓冲区,而忘记 BinaryReader... 只需使用 Stream.ReadStream.Write

(编辑)类似:

private static void copy(string srcFile, string dstFile, int offset,
     int length, byte[] buffer)
{
    using(Stream inStream = File.OpenRead(srcFile))
    using (Stream outStream = File.OpenWrite(dstFile))
    {
        inStream.Seek(offset, SeekOrigin.Begin);
        int bufferLength = buffer.Length, bytesRead;
        while (length > bufferLength &&
            (bytesRead = inStream.Read(buffer, 0, bufferLength)) > 0)
        {
            outStream.Write(buffer, 0, bytesRead);
            length -= bytesRead;
        }
        while (length > 0 &&
            (bytesRead = inStream.Read(buffer, 0, length)) > 0)
        {
            outStream.Write(buffer, 0, bytesRead);
            length -= bytesRead;
        }
    }        
}

【讨论】:

  • 最后刷新有什么原因吗?关闭它应该这样做。另外,我认为您想在第一个循环中减去长度:)
  • 好眼睛乔恩!同花顺是习惯的力量;当我传入流而不是在方法中打开/关闭它们时,从大量代码中提取 - 在返回之前刷新它很方便(如果写入大量数据)。
【解决方案4】:

您不应该在每次复制时都重新打开源文件,最好打开一次并将生成的 BinaryReader 传递给复制函数。此外,如果您对搜索进行排序,这可能会有所帮助,这样您就不会在文件中进行大跳跃。

如果长度不太大,您还可以尝试通过将彼此靠近的偏移量分组并读取它们所需的整个块来对多个复制调用进行分组,例如:

offset = 1234, length = 34
offset = 1300, length = 40
offset = 1350, length = 1000

可以分组为一个阅读:

offset = 1234, length = 1074

然后你只需要在你的缓冲区中“寻找”并且可以从那里写入三个新文件而无需再次读取。

【讨论】:

    【解决方案5】:

    您是否考虑过使用 CCR,因为您正在写入单独的文件,您可以并行执行所有操作(读取和写入),而 CCR 使执行此操作变得非常容易。

    static void Main(string[] args)
        {
            Dispatcher dp = new Dispatcher();
            DispatcherQueue dq = new DispatcherQueue("DQ", dp);
    
            Port<long> offsetPort = new Port<long>();
    
            Arbiter.Activate(dq, Arbiter.Receive<long>(true, offsetPort,
                new Handler<long>(Split)));
    
            FileStream fs = File.Open(file_path, FileMode.Open);
            long size = fs.Length;
            fs.Dispose();
    
            for (long i = 0; i < size; i += split_size)
            {
                offsetPort.Post(i);
            }
        }
    
        private static void Split(long offset)
        {
            FileStream reader = new FileStream(file_path, FileMode.Open, 
                FileAccess.Read);
            reader.Seek(offset, SeekOrigin.Begin);
            long toRead = 0;
            if (offset + split_size <= reader.Length)
                toRead = split_size;
            else
                toRead = reader.Length - offset;
    
            byte[] buff = new byte[toRead];
            reader.Read(buff, 0, (int)toRead);
            reader.Dispose();
            File.WriteAllBytes("c:\\out" + offset + ".txt", buff);
        }
    

    此代码将偏移量发布到 CCR 端口,这会导致创建线程以执行 Split 方法中的代码。这会导致您多次打开文件,但无需同步。您可以提高内存效率,但您必须牺牲速度。

    【讨论】:

    • 请记住,使用这个(或任何线程解决方案),您可以达到最大化 IO 的阶段:您将达到最佳吞吐量(即,如果尝试在同时,几个大文件等)。我一直发现,如果我可以有效地使一个文件读/写,那么我可以通过并行化来改进它(Assembly 可以帮助很多,在汇编器中进行读/写,它可以是壮观的,直到 IO限制,但是编写起来可能会很痛苦,并且您需要确保您希望直接访问您的设备的硬件或 BIOS 级别
    【解决方案6】:

    我建议的第一件事是进行测量。你在哪里浪费时间?是读还是写?

    超过 100,000 次访问(总和): 分配缓冲区数组花费了多少时间? 打开文件进行读取花费了多少时间(每次都是同一个文件吗?) 读写操作花费了多少时间?

    如果您不对文件进行任何类型的转换,您需要 BinaryWriter,还是可以使用文件流进行写入? (试试看,你得到相同的输出吗?它节省时间吗?)

    【讨论】:

      【解决方案7】:

      使用 FileStream + StreamWriter 我知道可以在短时间内(不到 1 分 30 秒)创建大量文件。我使用该技术从一个文件生成了三个总大小超过 700 兆字节的文件。

      您使用的代码的主要问题是您每次都打开一个文件。这会产生文件 I/O 开销。

      如果您提前知道要生成的文件的名称,您可以将 File.OpenWrite 提取到一个单独的方法中;它会提高速度。如果没有看到决定您如何拆分文件的代码,我认为您不会变得更快。

      【讨论】:

        【解决方案8】:

        没有人建议线程?编写较小的文件看起来像教科书示例,说明线程在哪里有用。设置一堆线程来创建较小的文件。这样,您可以并行创建它们,而无需等待每个完成。我的假设是创建文件(磁盘操作)将比拆分数据花费更长的时间。当然你应该首先验证顺序方法是不够的。

        【讨论】:

        • 线程可能会有所帮助,但他的瓶颈肯定在 I/O 上——CPU 可能会花费大量时间在磁盘上等待。这并不是说线程不会产生任何影响(例如,如果写入到不同的主轴,那么他可能会比全部在一个磁盘上获得更好的性能提升)
        【解决方案9】:

        (供将来参考。)

        很可能最快的方法是使用内存映射文件(主要是复制内存,操作系统通过其分页/内存管理处理文件读/写)。

        .NET 4.0 的托管代码支持内存映射文件。

        但如前所述,您需要进行分析,并期望切换到本机代码以获得最佳性能。

        【讨论】:

        • 内存映射文件是页面对齐的,所以它们不在了。这里的问题更可能是磁盘访问时间,而内存映射文件无论如何也无济于事。操作系统将管理缓存文件,无论它们是否是内存映射的。
        猜你喜欢
        • 1970-01-01
        • 2014-12-09
        • 2017-02-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-10-14
        相关资源
        最近更新 更多