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?(也就是说,没有实际将内容加载到内存中的 Buffer 中。)
4

9 回答 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;
        }
    }
}

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

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

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

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

于 2009-06-05T13:49:02.120 回答
29

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

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

于 2011-03-03T22:55:38.683 回答
6

有多大length?您可能会更好地重新使用固定大小(中等大小,但不是淫秽)的缓冲区,然后忘记BinaryReader......只需使用Stream.Readand Stream.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;
        }
    }        
}
于 2009-06-05T13:48:11.043 回答
3

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

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

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

可以归为一读:

offset = 1234, length = 1074

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

于 2009-06-05T13:49:50.650 回答
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 端口,这会导致创建线程以执行 Split 方法中的代码。这会导致您多次打开文件,但无需同步。您可以提高内存效率,但您必须牺牲速度。

于 2009-06-05T14:57:07.380 回答
1

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

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

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

于 2009-06-05T13:52:43.093 回答
1

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

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

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

于 2009-06-05T15:31:38.527 回答
0

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

于 2009-06-05T14:21:59.043 回答
-1

(备查。)

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

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

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

于 2009-06-05T14:08:27.307 回答