如何在C#中编写超快的文件流代码?

42

我需要将一个巨大的文件分割成许多较小的文件。每个目标文件由偏移量和长度来定义,表示为字节数。我正在使用以下代码:

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?(也就是说,不将内容实际加载到内存中的缓冲区中。)

你是否完美地分割了文件,即只需将所有小文件组合在一起就能重建大文件?如果是这样,那么可以节省一些空间。如果不是,小文件的范围是否重叠?它们是否按偏移量排序? - jamie
9个回答

49
我不相信.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;
        }
    }
}

请注意,这也会关闭输出流(由于使用语句),而您的原始代码没有这样做。
重要的是,这将更有效地使用操作系统文件缓冲,因为您重用相同的输入流,而不是在开头重新打开文件,然后寻找。
我认为它会明显更快,但显然您需要尝试一下才能确定...
当然,这假定连续的块。如果您需要跳过文件的某些部分,则可以从方法外部执行该操作。此外,如果您正在编写非常小的文件,则可能还要针对该情况进行优化-最简单的方法可能是引入一个包装输入流的BufferedStream

我知道这是两年前的帖子,只是想知道...这仍然是最快的方法吗?(即:.Net 中没有新的需要注意的内容吗?)此外,在进入循环之前执行 Math.Min 是否会更快?或者更好的方法是删除长度参数,因为它可以通过缓冲区计算得出?抱歉挑剔并且发表迟来的评论!提前致谢。 - Smudge202
2
@Smudge202:鉴于这是执行IO操作,调用Math.Min显然在性能方面并不重要。具有长度参数和缓冲区长度的目的是允许您重复使用可能过大的缓冲区。 - Jon Skeet
谢谢你回复我。我不想在这里提出新问题,因为可能已经有足够好的答案了。但是,如果您想读取大量文件的前 x 个字节(为了从大量文件中获取 XMP 元数据),您是否会建议使用上述方法(稍加调整)? - Smudge202
是的,我对写作部分不太感兴趣,我只想确认读取一个文件的最快方法也是读取多个文件的最快方法。我想象能够P/Invoke文件指针/偏移量,然后能够使用相同/更少的流/缓冲区扫描多个文件,在我虚构的世界中,这可能会更快地实现我想要达到的目标(尽管不适用于OP)。如果我没有疯掉,最好我开始一个新问题。如果我疯了,请告诉我,这样我就不会浪费更多人的时间了 :-) - Smudge202
@Smudge202:你现在真的有性能问题吗?你是否编写了最简单的可用代码,并发现它运行得太慢?请记住,很多情况可能取决于上下文——例如,如果你使用固态硬盘,同时读取可能会有所帮助,但在普通硬盘上则无济于事。 - Jon Skeet
显示剩余2条评论

30

感谢您提供详细的博客信息。祝贺您获得了“最佳答案”徽章! - ouflak

6

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;
        }
    }        
}

1
结束时刷新的原因是什么?关闭它应该会这样做。此外,我认为您想在第一个循环中从长度中减去 :) - Jon Skeet
好眼力,Jon!Flush只是习惯使然;在我传递流而不是在方法中打开/关闭它们时,从很多代码中 - 如果要写入大量数据,则在返回之前刷新它是方便的。 - Marc Gravell

3

如果你正在写入到不同的文件中,你是否考虑使用CCR呢?因为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方法中的代码。这会让你多次打开文件,但却不需要同步。你可以使其更加高效利用内存,但需要牺牲一些速度。

1
请记住,使用此(或任何线程解决方案)时,您可能会遇到IO瓶颈:您已经达到了最佳吞吐量(即如果尝试同时写入数百/数千个小文件,几个大文件等)。 我始终发现,如果我可以使一个文件的读/写效率高效,那么我很难通过并行化来改进它(汇编可以帮助很多,使用汇编进行读/写操作可以非常出色,直到IO限制,但编写起来可能很麻烦,并且您需要确定是否需要直接访问硬件或BIOS级别的设备)。 - GMasucci

3
你在每次拷贝时都不应该重新打开源文件,最好只打开一次并将结果传递给复制函数的BinaryReader。此外,如果你按顺序定位,可以帮助你避免在文件内跳跃太大。
如果长度不太大,你也可以尝试通过将接近的偏移量分组来组合多个拷贝调用,并读取你需要的整个块,例如:
offset = 1234, length = 34
offset = 1300, length = 40
offset = 1350, length = 1000

可以归为一次读取:

offset = 1234, length = 1074

然后,您只需在缓冲区中“查找”,就可以从中写入三个新文件,而无需再次读取。


1

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

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

如果您没有对文件进行任何类型的转换,您是否需要BinaryWriter,还是可以使用filestream进行写入?(尝试一下,您是否获得相同的输出?是否节省时间?)


1

使用FileStream + StreamWriter,我知道可以在很短的时间内创建大文件(少于1分30秒)。我使用这种技术从一个文件生成三个总计700多兆字节的文件。

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

如果您提前知道要生成的文件的名称,可以将File.OpenWrite提取到单独的方法中;这将增加速度。没有看到确定如何拆分文件的代码,我认为您无法获得更快的速度。


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

线程可能有所帮助,但瓶颈肯定在I/O上——CPU可能花费大量时间等待磁盘。这并不是说线程不会有任何差异(例如,如果写入到不同的主轴,则他可能会获得比全部写入一个磁盘更好的性能提升)。 - JMarsch

-1
(供日后参考。)
可能最快的方法是使用内存映射文件(主要是复制内存,操作系统通过其分页/内存管理处理文件读取/写入)。
在.NET 4.0中,托管代码支持内存映射文件。
但是需要注意的是,您需要进行性能分析,并期望切换到本机代码以获得最大性能。

1
内存映射文件是页面对齐的,因此它们已经足够优化。这里的问题更可能是磁盘访问时间,而内存映射文件无论如何都不会有所帮助。操作系统将管理缓存文件,无论它们是否被内存映射。 - jamie

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