在C#中将大文件读入字节数组的最佳方法是什么?

478

我有一个Web服务器,将读取大型二进制文件(几兆字节)到字节数组中。服务器可能会同时读取多个文件(不同的页面请求),因此我正在寻找在不过度消耗CPU的情况下完成此操作的最佳优化方式。下面的代码是否足够好?

public byte[] FileToByteArray(string fileName)
{
    byte[] buff = null;
    FileStream fs = new FileStream(fileName, 
                                   FileMode.Open, 
                                   FileAccess.Read);
    BinaryReader br = new BinaryReader(fs);
    long numBytes = new FileInfo(fileName).Length;
    buff = br.ReadBytes((int) numBytes);
    return buff;
}

74
你的示例可以简写为 byte[] buff = File.ReadAllBytes(fileName) - Jesse C. Slicer
3
为什么作为第三方网络服务,文件在发送到网络服务之前需要完全存储在内存中,而不是以流的方式传输呢?这样做网络服务也无法区分两种方式。 - Brian
@Brian,有些客户不知道如何处理.NET流,例如Java。在这种情况下,唯一能做的就是将整个文件读入字节数组中。 - sjeffrey
4
@sjeffrey说:“我说的是数据应该被流式传输,而不是作为.NET流传递。无论哪种方式,客户端都不会知道差别。” - Brian
12个回答

908

只需将整个内容替换为:

return File.ReadAllBytes(fileName);

然而,如果你担心内存消耗,那么你就不应该一次性将整个文件读入内存中。你应该分块读取。


55
这种方法只适用于2^32个字节(4.2 GB)以下的文件。 - Mahmoud Farahat
17
使用File.ReadAllBytes读取大文件(测试了630 MB的文件并且失败了)会抛出OutOfMemoryException异常。 - jdearana
6
@juanjo.arana 嗯,当然...肯定会有一些东西无法适应内存,这种情况下就没有答案了。一般来说,你应该将文件流化,而不是完全存储在内存中。你可以考虑使用这个临时措施:http://msdn.microsoft.com/en-us/library/hh285054%28v=vs.110%29.aspx - Mehrdad Afshari
5
在.NET中,数组大小有限制,但在.NET 4.5中,您可以通过特殊的配置选项打开对大型数组(> 2GB)的支持,请参见http://msdn.microsoft.com/en-us/library/hh285054.aspx。 - illegal-immigrant
8
对于大文件的读取,至少给出的代码而言,这不应该成为接受或排名最高的答案。"你不应该一次性将整个文件都读入内存中,而是应该分块读取" 这个说法是正确的,并且应该有相应的代码支持。如果没有对应的代码来证明这个回答,就应该对其进行负评,因为这个答案的代码非常具有误导性,并且与那个非常正确的说法相矛盾。 - vapcguy
显示剩余5条评论

84

我认为这里的答案通常是“不要”。除非你绝对需要一次性获取所有数据,否则请考虑使用基于Stream的API(或某种阅读器/迭代器变体)。特别是当你有多个并行操作(如问题中所建议的)时,这是非常重要的,以最小化系统负载并最大化吞吐量。

例如,如果你正在向调用者流式传输数据:

Stream dest = ...
using(Stream source = File.OpenRead(path)) {
    byte[] buffer = new byte[2048];
    int bytesRead;
    while((bytesRead = source.Read(buffer, 0, buffer.Length)) > 0) {
        dest.Write(buffer, 0, bytesRead);
    }
}

3
补充您的陈述,我甚至建议在像向客户端流式传输文件这样的 I/O 绑定操作时考虑使用异步 ASP.NET 处理程序。然而,如果出于某种原因必须将整个文件读取到 byte[] 中,则建议避免使用流或其他任何东西,只需使用系统提供的 API。 - Mehrdad Afshari
是的,我需要一次性获取所有数据。这些数据将被发送到第三方网络服务。 - Tony_Henrich
1
@Tony:我在我的回答中提到了 File.ReadAllBytes - Mehrdad Afshari
1
通过每次更改“偏移量”来增加读取的字节数,并且每次减少相同数量的要读取的字节数(从bytesToRead = target.Length开始); 因此:int offset = 0; int toRead = target.Length; while((bytesRead - source.Read(target, offset, toRead)) > 0) { offset += bytesRead; toRead -= bytesRead; } - Marc Gravell
@mmx,你在评论中所说的“系统提供的API”是什么意思? - Grace
显示剩余5条评论

44

我认为应该这样:

byte[] file = System.IO.File.ReadAllBytes(fileName);

7
请注意,当处理非常大的文件时可能会出现卡顿。 - vapcguy

37

你的代码可以被优化为这样(代替使用File.ReadAllBytes):

public byte[] ReadAllBytes(string fileName)
{
    byte[] buffer = null;
    using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
    {
        buffer = new byte[fs.Length];
        fs.Read(buffer, 0, (int)fs.Length);
    }
    return buffer;
} 

请注意Read方法设置的整数最大值-文件大小限制。换句话说,您一次只能读取2GB的数据块。
此外,请注意FileStream的最后一个参数是缓冲区大小。
我还建议阅读有关FileStreamBufferedStream的内容。
像往常一样,编写一个简单的示例程序以测试哪种方法更快将会非常有益。
此外,底层硬件将对性能产生很大影响。您是否使用带有大缓存和具有内置内存缓存的RAID卡的服务器硬盘驱动器?还是使用连接到IDE端口的标准驱动器?

硬件类型有什么区别吗?所以如果是IDE,你会使用某些.NET方法,如果是RAID,你会使用另一种方法吗? - Tony_Henrich
@Tony_Henrich - 这与您从编程语言中进行的调用无关。有不同类型的硬盘驱动器。例如,Seagate驱动器被分类为“AS”或“NS”,其中NS是基于服务器的大缓存驱动器,而“AS”驱动器是面向消费者-家庭计算机的驱动器。搜索速度和内部传输速率也会影响您从磁盘读取某些内容的速度。RAID阵列可以通过缓存大大提高读/写性能。因此,您可能能够一次性读取整个文件,但底层硬件仍然是决定因素。 - user113476
2
这段代码存在一个关键性的错误。Read 只需要返回至少 1 个字节。 - mafu
我会确保将长整型转换为整型的操作用 checked 结构进行包装,就像这样:checked((int)fs.Length)。 - tzup
我会在那个using语句中只使用 var binaryReader = new BinaryReader(fs); fileData = binaryReader.ReadBytes((int)fs.Length);。但这实际上就像OP所做的一样,只是我通过将fs.Length强制转换为int而不是获取FileInfo长度的long值并进行转换来省略了一行代码。 - vapcguy

13

我认为使用BinaryReader是可以的,但可以重构为以下形式,而不是获取缓冲区长度的所有代码行:

public byte[] FileToByteArray(string fileName)
{
    byte[] fileData = null;

    using (FileStream fs = File.OpenRead(fileName)) 
    { 
        using (BinaryReader binaryReader = new BinaryReader(fs))
        {
            fileData = binaryReader.ReadBytes((int)fs.Length); 
        }
    }
    return fileData;
}

使用.ReadAllBytes()不如使用这种方法更好,因为在顶部响应的评论中,包括.ReadAllBytes()的一个评论者遇到了文件大于600 MB的问题,因为BinaryReader适用于此类操作。同时,将其放置在using语句中可以确保FileStreamBinaryReader被关闭和处理。


对于 C#,需要使用“using (FileStream fs = File.OpenRead(fileName))”而不是上面提到的“using (FileStream fs = new File.OpenRead(fileName))”。只需在File.OpenRead()之前删除new关键字。 - Syed Mohamed
@Syed 上面的代码确实是为C#编写的,但你说得对,在那里不需要使用“new”。已删除。 - vapcguy

10
根据操作频率、文件大小和文件数量,考虑到其他性能问题。需要记住的是,每个字节数组都将在垃圾收集器的掌控下释放。如果您没有缓存任何数据,可能会创建大量垃圾并且大部分性能都会消耗在% Time in GC中。如果这些块大于85K,则将分配到大对象堆(LOH),这将需要回收所有代以释放内存(这非常昂贵,在服务器上执行此操作时会停止所有执行)。此外,如果LOH上有大量对象,则可能会导致LOH碎片化(LOH永远不会紧凑),从而导致性能下降和内存不足异常。您可以在达到一定点后重新启动进程,但我不知道是否是最佳实践。
重点是,在仅仅为了快速读取所有字节而将它们全部加载到内存之前,应该考虑应用程序的完整生命周期,否则可能会将短期性能换成整体性能。

以下是关于C#源代码的内容,用于管理“垃圾回收器”、“块”、“性能”、“事件计数器”等。 - PreguntonCojoneroCabrón

2
如果“大文件”指的是超过4GB的限制,则我的下面所写的代码逻辑是合适的。要注意的关键问题是使用了LONG数据类型来进行SEEK方法。由于LONG能够指向超过2^32个数据边界,因此可以处理大文件。
在这个例子中,代码首先按照1GB的块处理大文件,处理完整的1GB块后,剩余的 (<1GB) 字节将被处理。我将此代码与计算4GB以上文件的CRC一起使用。(在本例中使用https://crc32c.machinezoo.com/进行crc32c计算)
private uint Crc32CAlgorithmBigCrc(string fileName)
{
    uint hash = 0;
    byte[] buffer = null;
    FileInfo fileInfo = new FileInfo(fileName);
    long fileLength = fileInfo.Length;
    int blockSize = 1024000000;
    decimal div = fileLength / blockSize;
    int blocks = (int)Math.Floor(div);
    int restBytes = (int)(fileLength - (blocks * blockSize));
    long offsetFile = 0;
    uint interHash = 0;
    Crc32CAlgorithm Crc32CAlgorithm = new Crc32CAlgorithm();
    bool firstBlock = true;
    using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
    {
        buffer = new byte[blockSize];
        using (BinaryReader br = new BinaryReader(fs))
        {
            while (blocks > 0)
            {
                blocks -= 1;
                fs.Seek(offsetFile, SeekOrigin.Begin);
                buffer = br.ReadBytes(blockSize);
                if (firstBlock)
                {
                    firstBlock = false;
                    interHash = Crc32CAlgorithm.Compute(buffer);
                    hash = interHash;
                }
                else
                {
                    hash = Crc32CAlgorithm.Append(interHash, buffer);
                }
                offsetFile += blockSize;
            }
            if (restBytes > 0)
            {
                Array.Resize(ref buffer, restBytes);
                fs.Seek(offsetFile, SeekOrigin.Begin);
                buffer = br.ReadBytes(restBytes);
                hash = Crc32CAlgorithm.Append(interHash, buffer);
            }
            buffer = null;
        }
    }
    //MessageBox.Show(hash.ToString());
    //MessageBox.Show(hash.ToString("X"));
    return hash;
}

2

概述:如果您的图像作为action=嵌入资源添加,则使用GetExecutingAssembly将jpg资源检索到流中,然后将流中的二进制数据读入字节数组中。

   public byte[] GetAImage()
    {
        byte[] bytes=null;
        var assembly = Assembly.GetExecutingAssembly();
        var resourceName = "MYWebApi.Images.X_my_image.jpg";

        using (Stream stream = assembly.GetManifestResourceStream(resourceName))
        {
            bytes = new byte[stream.Length];
            stream.Read(bytes, 0, (int)stream.Length);
        }
        return bytes;

    }

0
使用这个:
 bytesRead = responseStream.ReadAsync(buffer, 0, Length).Result;

2
欢迎来到Stack Overflow!由于解释是该平台答案的重要组成部分,请解释您的代码以及它如何解决问题,为什么它可能比其他答案更好。我们的指南《如何撰写优秀答案》(How to write a good answer)可能对您有所帮助。谢谢! - David

0

当你一次性读取整个文件时,使用BufferedStream有什么意义呢? - Mehrdad Afshari
他要求最佳性能,而不是一次性读取文件。 - Todd Moses
9
性能是在操作的情境下可测量的。对于顺序读取流并一次性全部读入内存的情况,额外的缓冲区不太可能获得益处。 - Mehrdad Afshari

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