对于标题感到抱歉,它可能有点令人困惑,但我不知道如何更好地解释它。
有两个扩展名为.cat(目录文件)和.dat的文件。 .cat文件包含.dat文件中二进制文件的信息。 此信息是文件的名称、大小、在.dat文件中的偏移量以及md5哈希。
例如.cat文件;
assets/textures/environments/asteroids/ast_crystal_blue_diff-small.gz 22387 1546955265 85a67a982194e4141e08fac4bf062c8f
assets/textures/environments/asteroids/ast_crystal_blue_diff.gz 83859 1546955265 86c7e940de82c2c2573a822c9efc9b6b
assets/textures/environments/asteroids/ast_crystal_diff-small.gz 22693 1546955265 cff6956c94b59e946b78419d9c90f972
assets/textures/environments/asteroids/ast_crystal_diff.gz 85531 1546955265 57d5a24dd4da673a42cbf0a3e8e08398
assets/textures/environments/asteroids/ast_crystal_green_diff-small.gz 22312 1546955265 857fea639e1af42282b015e8decb02db
assets/textures/environments/asteroids/ast_crystal_green_diff.gz 115569 1546955265 ee6f60b0a8211ec048172caa762d8a1a
assets/textures/environments/asteroids/ast_crystal_purple_diff-small.gz 14179 1546955265 632317951273252d516d36b80de7dfcd
assets/textures/environments/asteroids/ast_crystal_purple_diff.gz 53781 1546955265 c057acc06a4953ce6ea3c6588bbad743
assets/textures/environments/asteroids/ast_crystal_yellow_diff-small.gz 21966 1546955265 a893c12e696f9e5fb188409630b8d10b
assets/textures/environments/asteroids/ast_crystal_yellow_diff.gz 82471 1546955265 c50a5e59093fe9c6abb64f0f47a26e57
assets/textures/environments/asteroids/xen_crystal_diff-small.gz 14161 1546955265 23b34bdd1900a7e61a94751ae798e934
assets/textures/environments/asteroids/xen_crystal_diff.gz 53748 1546955265 dcb7c8294ef72137e7bca8dd8ea2525f
assets/textures/lensflares/lens_rays3_small_diff.gz 14107 1546955265 a656d1fad4198b0662a783919feb91a5
我相对容易地解析了那些文件,我使用了Span<T>
,并经过一些BenchmarkDotNet
的基准测试后,我认为我已经尽可能优化了这种类型文件的读取。
但是.dat文件就不同了。一个典型的.dat文件大小为GB级别。
我首先尝试了我能想到的最简单的方法。
(我删除了空值检查和验证代码,以使代码更易读。)
public async Task ExportAssetsAsync(CatalogFile catalogFile, string destDirectory, CancellationToken ct = default)
{
IFileInfo catalogFileInfo = _fs.FileInfo.FromFileName(catalogFile.FilePath);
string catalogFileName = _fs.Path.GetFileNameWithoutExtension(catalogFileInfo.Name);
string datFilePath = _fs.Path.Combine(catalogFileInfo.DirectoryName, $"{catalogFileName}.dat");
IFileInfo datFileInfo = _fs.FileInfo.FromFileName(datFilePath);
await using Stream stream = datFileInfo.OpenRead();
foreach (CatalogEntry catalogEntry in catalogFile.CatalogEntries)
{
string destFilePath = _fs.Path.Combine(destDirectory, catalogEntry.AssetPath);
IFileInfo destFile = _fs.FileInfo.FromFileName(destFilePath);
if (!destFile.Directory.Exists)
{
destFile.Directory.Create();
}
stream.Seek(catalogEntry.ByteOffset, SeekOrigin.Begin);
var newFileData = new byte[catalogEntry.AssetSize];
int read = await stream.ReadAsync(newFileData, 0, catalogEntry.AssetSize, ct);
if (read != catalogEntry.AssetSize)
{
_logger?.LogError("Could not read asset data from dat file: {DatFile}", datFilePath);
throw new DatFileReadException("Could not read asset data from dat file", datFilePath);
}
await using Stream destStream = _fs.File.Open(destFile.FullName, FileMode.Create);
destStream.Write(newFileData);
destStream.Close();
}
}
可以猜到,这种方法既慢又会在堆上分配大量内存,从而使垃圾回收器忙碌。
我对上述方法进行了一些修改,尝试使用缓冲区进行读取,然后使用 stackalloc 和 Span 代替使用 new byte[catalogEntry.AssetSize]
进行分配。虽然在缓冲读取方面没有取得太多的进展,但当文件大小超过堆栈大小时,使用 stackalloc 很自然会导致 StackOverflow 异常。
然后经过一些研究,我决定可以使用 .NET Core 2.1 中引入的 System.IO.Pipelines
。并将上述方法更改如下。
public async Task ExportAssetsPipe(CatalogFile catalogFile, string destDirectory, CancellationToken ct = default)
{
IFileInfo catalogFileInfo = _fs.FileInfo.FromFileName(catalogFile.FilePath);
string catalogFileName = _fs.Path.GetFileNameWithoutExtension(catalogFileInfo.Name);
string datFilePath = _fs.Path.Combine(catalogFileInfo.DirectoryName, $"{catalogFileName}.dat");
IFileInfo datFileInfo = _fs.FileInfo.FromFileName(datFilePath);
await using Stream stream = datFileInfo.OpenRead();
foreach (CatalogEntry catalogEntry in catalogFile.CatalogEntries)
{
string destFilePath = _fs.Path.Combine(destDirectory, catalogEntry.AssetPath);
IFileInfo destFile = _fs.FileInfo.FromFileName(destFilePath);
if (!destFile.Directory.Exists)
{
destFile.Directory.Create();
}
stream.Position = catalogEntry.ByteOffset;
var reader = PipeReader.Create(stream);
while (true)
{
ReadResult readResult = await reader.ReadAsync(ct);
ReadOnlySequence<byte> buffer = readResult.Buffer;
if (buffer.Length >= catalogEntry.AssetSize)
{
ReadOnlySequence<byte> entry = buffer.Slice(0, catalogEntry.AssetSize);
await using Stream destStream = File.Open(destFile.FullName, FileMode.Create);
foreach (ReadOnlyMemory<byte> mem in entry)
{
await destStream.WriteAsync(mem, ct);
}
destStream.Close();
break;
}
reader.AdvanceTo(buffer.Start, buffer.End);
}
}
}
根据BenchmarkDotnet的数据显示,第二种方法的性能和内存分配比第一种方法更差。这可能是因为我在使用System.IO.Pipelines时使用方式不正确或不当导致的。由于我以前没有处理如此大型文件的输入/输出操作经验,所以对此并不熟悉。请问如何在最小的内存分配和最大的性能下实现我的目标?非常感谢您提前的帮助和正确的指导。