关于.net:在C#中将大文件读入字节数组的最佳方法?

Best way to read a large file into a byte array in C#?

我有一个Web服务器,它将把大的二进制文件(几兆字节)读取到字节数组中。服务器可以同时读取多个文件(不同的页面请求),因此我正在寻找最优化的方法来实现这一点,而不会对CPU造成太大的负担。下面的代码是否足够好?

1
2
3
4
5
6
7
8
9
10
11
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;
}


只需将整个内容替换为:

1
return File.ReadAllBytes(fileName);

但是,如果您关心内存消耗,就不应该一次将整个文件读取到内存中。你应该分块去做。


我可能会说这里的答案通常是"不"。除非您绝对需要所有数据,否则考虑使用基于Stream的API(或读卡器/迭代器的某种变体)。当您有多个并行操作(如问题所建议的)以最小化系统负载和最大化吞吐量时,这一点尤其重要。

例如,如果您正在向调用者传输数据:

1
2
3
4
5
6
7
8
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);
    }
}


我认为:

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


您的代码可以分解为以下内容(代替file.readallbytes):

1
2
3
4
5
6
7
8
9
10
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;
}

注意integer.maxvalue-read方法设置的文件大小限制。换句话说,您一次只能读取2GB块。

还要注意,filestream的最后一个参数是缓冲区大小。

我还建议阅读有关文件流和BufferedStream的内容。

像往常一样,一个简单的样本程序来分析哪个是最快的将是最有益的。

另外,底层硬件也会对性能产生很大影响。您是否使用基于服务器的具有大缓存的硬盘驱动器和具有板载内存缓存的RAID卡?或者您使用的是连接到IDE端口的标准驱动器?


根据操作频率、文件大小和查看的文件数,还需要考虑其他性能问题。要记住的一点是,每个字节数组都将在垃圾收集器的控制下被释放。如果不缓存任何数据,最终可能会造成大量的垃圾,并将大部分性能损失到GC中的%时间。如果块大于85K,您将分配给大对象堆(LOH),这将需要收集所有代来释放(这非常昂贵,并且在服务器上执行时将停止所有执行)。另外,如果在LOH上有大量的对象,那么最终可能会导致LOH碎片(LOH从未被压缩),这会导致性能差和内存不足异常。一旦你达到某一点,你就可以循环利用这个过程,但我不知道这是否是一个最佳实践。

关键是,在以最快的方式将所有字节读取到内存中之前,您应该考虑应用程序的整个生命周期,或者您可能会以短期性能换取总体性能。


我想说BinaryReader是可以的,但是可以重构为这个,而不是所有获得缓冲区长度的代码行:

1
2
3
4
5
6
7
8
9
10
11
12
13
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被关闭和处理。


如果"一个大文件"意味着超出了4GB的限制,那么我下面写的代码逻辑是适当的。需要注意的关键问题是与seek方法一起使用的长数据类型。因为long可以指向2^32以上的数据边界。在这个例子中,代码首先处理1GB块的大文件,处理完整个1GB块后,处理剩余的(<1GB)字节。我使用这段代码计算超过4GB大小的文件的CRC。(本例中使用https://crc32c.machinezoo.com/进行crc32c计算)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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;
}

使用此:

1
 bytesRead = responseStream.ReadAsync(buffer, 0, Length).Result;


使用C中的bufferedstream类来提高性能。缓冲区是内存中用于缓存数据的字节块,从而减少了对操作系统的调用次数。缓冲区提高了读写性能。

有关代码示例和附加说明,请参见以下内容:http://msdn.microsoft.com/en-us/library/system.io.bufferedstream.aspx


我建议您先尝试使用Response.TransferFile()方法,然后使用Response.Flush()Response.End()方法来提供大文件。


如果处理的文件超过2 GB,则会发现上述方法失败。

只需将流传递给MD5并允许它为您将文件分块就可以轻松得多:

1
2
3
4
5
6
7
8
9
private byte[] computeFileHash(string filename)
{
    MD5 md5 = MD5.Create();
    using (FileStream fs = new FileStream(filename, FileMode.Open))
    {
        byte[] hash = md5.ComputeHash(fs);
        return hash;
    }
}