如何用C#编写超高速文件流代码?

时间:2009-06-05 13:41:38

标签: c# performance streaming cpu utilization

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

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? (即,实际上没有将内容加载到内存中的缓冲区中。)

9 个答案:

答案 0 :(得分:45)

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

答案 1 :(得分:25)

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

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

答案 2 :(得分: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;
        }
    }        
}

答案 3 :(得分:3)

每次复制时都不应重新打开源文件,最好打开一次并将生成的BinaryReader传递给复制功能。此外,如果你订购你的搜索,它可能会有所帮助,所以你不要在文件中大幅跳跃。

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

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

可以分组为一个读取:

offset = 1234, length = 1074

然后你只需要在你的缓冲区中“寻找”并可以从那里写下三个新文件,而不必再读一遍。

答案 4 :(得分:3)

您是否考虑过使用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端口,这会导致创建一个Thread来执行Split方法中的代码。这会导致您多次打开文件,但无需同步。你可以让它更有效率,但你必须牺牲速度。

答案 5 :(得分:1)

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

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

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

答案 6 :(得分:1)

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

您使用的代码的主要问题是您每次都在打开文件。那就是创建文件I / O开销。

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

答案 7 :(得分:0)

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

答案 8 :(得分:-1)

(供将来参考。)

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

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

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