【问题标题】:How to create fast and efficient filestream writes on large sparse files如何在大型稀疏文件上创建快速高效的文件流写入
【发布时间】:2013-07-23 18:05:53
【问题描述】:

我有一个应用程序可以在多个段中写入大文件。我使用 FileStream.Seek 来定位每个文件。看来,当我在稀疏文件中的较深位置调用 FileStream.Write 时,写入会触发对所有先前字节的“回填”操作(写入 0),这很慢。

有没有更有效的方法来处理这种情况?

下面的代码演示了这个问题。在我的机器上,初始写入大约需要 370 毫秒。

    public void WriteToStream()
    {
        DateTime dt;
        using (FileStream fs = File.Create("C:\\testfile.file"))
        {   
            fs.SetLength(1024 * 1024 * 100);
            fs.Seek(-1, SeekOrigin.End);
            dt = DateTime.Now;
            fs.WriteByte(255);              
        }

        Console.WriteLine(@"WRITE MS: " + DateTime.Now.Subtract(dt).TotalMilliseconds.ToString());
    }

【问题讨论】:

    标签: c# file-io filestream sparse-matrix


    【解决方案1】:

    NTFS 确实支持Sparse Files,但是如果不调用一些本机方法,在 .net 中就无法做到这一点。

    将文件标记为稀疏文件并不难,只要知道一旦文件被标记为稀疏文件,它就永远无法转换回非稀疏文件,除非将整个文件复制到一个新的非稀疏文件中文件。

    用法示例

    class Program
    {
        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern bool DeviceIoControl(
            SafeFileHandle hDevice,
            int dwIoControlCode,
            IntPtr InBuffer,
            int nInBufferSize,
            IntPtr OutBuffer,
            int nOutBufferSize,
            ref int pBytesReturned,
            [In] ref NativeOverlapped lpOverlapped
        );
    
        static void MarkAsSparseFile(SafeFileHandle fileHandle)
        {
            int bytesReturned = 0;
            NativeOverlapped lpOverlapped = new NativeOverlapped();
            bool result =
                DeviceIoControl(
                    fileHandle,
                    590020, //FSCTL_SET_SPARSE,
                    IntPtr.Zero,
                    0,
                    IntPtr.Zero,
                    0,
                    ref bytesReturned,
                    ref lpOverlapped);
            if(result == false)
                throw new Win32Exception();
        }
    
        static void Main()
        {
            //Use stopwatch when benchmarking, not DateTime
            Stopwatch stopwatch = new Stopwatch();
    
            stopwatch.Start();
            using (FileStream fs = File.Create(@"e:\Test\test.dat"))
            {
                MarkAsSparseFile(fs.SafeFileHandle);
    
                fs.SetLength(1024 * 1024 * 100);
                fs.Seek(-1, SeekOrigin.End);
                fs.WriteByte(255);
            }
            stopwatch.Stop();
    
            //Returns 2 for sparse files and 1127 for non sparse
            Console.WriteLine(@"WRITE MS: " + stopwatch.ElapsedMilliseconds); 
        }
    }
    

    一旦一个文件被标记为稀疏,它现在的行为就像你一样,除了它在 cmets 中的行为。您无需写入字节即可将文件标记为设定的大小。

    static void Main()
    {
        string filename = @"e:\Test\test.dat";
    
        using (FileStream fs = new FileStream(filename, FileMode.Create))
        {
            MarkAsSparseFile(fs.SafeFileHandle);
    
            fs.SetLength(1024 * 1024 * 25);
        }
    }
    

    【讨论】:

    • 感谢您的回复,这很有趣。我的印象是 FileStream.SetLength() 创建了一个稀疏文件。我错了吗?我试图避免在大搜索点写入稀疏文件时产生的性能损失。我不完全清楚这将如何避免这个问题。
    • 内部 SetLength 正在调用 SetEndOfFile 。我不知道这是否创建了一个稀疏文件。
    • 刚刚运行了一个快速测试,它没有。
    • @user1385998 这比我想象的要容易得多!查看我更新的帖子。
    • 知道了!非常感谢所有详细信息和信息(更不用说 SparseFile 代码中的错误捕获)。添加的细节是,一旦您将文件创建为稀疏文件,它在后续打开时仍然保持稀疏,这正是我所需要的。
    【解决方案2】:

    下面是一些使用稀疏文件的代码:

    using System;
    using System.ComponentModel;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading;
    
    using Microsoft.Win32.SafeHandles;
    
    public static class SparseFiles
    {
        private const int FILE_SUPPORTS_SPARSE_FILES = 64;
    
        private const int FSCTL_SET_SPARSE = 0x000900c4;
    
        private const int FSCTL_SET_ZERO_DATA = 0x000980c8;
    
        public static void MakeSparse(this FileStream fileStream)
        {
            var bytesReturned = 0;
            var lpOverlapped = new NativeOverlapped();
            var result = DeviceIoControl(
                fileStream.SafeFileHandle, 
                FSCTL_SET_SPARSE, 
                IntPtr.Zero, 
                0, 
                IntPtr.Zero, 
                0, 
                ref bytesReturned, 
                ref lpOverlapped);
    
            if (!result)
            {
                throw new Win32Exception();
            }
        }
    
        public static void SetSparseRange(this FileStream fileStream, long fileOffset, long length)
        {
            var fzd = new FILE_ZERO_DATA_INFORMATION();
            fzd.FileOffset = fileOffset;
            fzd.BeyondFinalZero = fileOffset + length;
            var lpOverlapped = new NativeOverlapped();
            var dwTemp = 0;
    
            var result = DeviceIoControl(
                fileStream.SafeFileHandle, 
                FSCTL_SET_ZERO_DATA, 
                ref fzd, 
                Marshal.SizeOf(typeof(FILE_ZERO_DATA_INFORMATION)), 
                IntPtr.Zero, 
                0, 
                ref dwTemp, 
                ref lpOverlapped);
            if (!result)
            {
                throw new Win32Exception();
            }
        }
    
        public static bool SupportedOnVolume(string filename)
        {
            var targetVolume = Path.GetPathRoot(filename);
            var fileSystemName = new StringBuilder(300);
            var volumeName = new StringBuilder(300);
            uint lpFileSystemFlags;
            uint lpVolumeSerialNumber;
            uint lpMaxComponentLength;
    
            var result = GetVolumeInformationW(
                targetVolume, 
                volumeName, 
                (uint)volumeName.Capacity, 
                out lpVolumeSerialNumber, 
                out lpMaxComponentLength, 
                out lpFileSystemFlags, 
                fileSystemName, 
                (uint)fileSystemName.Capacity);
            if (!result)
            {
                throw new Win32Exception();
            }
    
            return (lpFileSystemFlags & FILE_SUPPORTS_SPARSE_FILES) == FILE_SUPPORTS_SPARSE_FILES;
        }
    
        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool DeviceIoControl(
            SafeFileHandle hDevice, 
            int dwIoControlCode, 
            IntPtr InBuffer, 
            int nInBufferSize, 
            IntPtr OutBuffer, 
            int nOutBufferSize, 
            ref int pBytesReturned, 
            [In] ref NativeOverlapped lpOverlapped);
    
        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool DeviceIoControl(
            SafeFileHandle hDevice, 
            int dwIoControlCode, 
            ref FILE_ZERO_DATA_INFORMATION InBuffer, 
            int nInBufferSize, 
            IntPtr OutBuffer, 
            int nOutBufferSize, 
            ref int pBytesReturned, 
            [In] ref NativeOverlapped lpOverlapped);
    
        [DllImport("kernel32.dll", EntryPoint = "GetVolumeInformationW")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool GetVolumeInformationW(
            [In] [MarshalAs(UnmanagedType.LPWStr)] string lpRootPathName, 
            [Out] [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpVolumeNameBuffer, 
            uint nVolumeNameSize, 
            out uint lpVolumeSerialNumber, 
            out uint lpMaximumComponentLength, 
            out uint lpFileSystemFlags, 
            [Out] [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpFileSystemNameBuffer, 
            uint nFileSystemNameSize);
    
        [StructLayout(LayoutKind.Sequential)]
        private struct FILE_ZERO_DATA_INFORMATION
        {
            public long FileOffset;
    
            public long BeyondFinalZero;
        }
    }
    

    以及测试上述类的示例代码。

    class Program
    {
        static void Main(string[] args)
        {
            using (var fileStream = new FileStream("test", FileMode.Create, FileAccess.ReadWrite, FileShare.None))
            {
                fileStream.SetLength(1024 * 1024 * 128);
                fileStream.MakeSparse();
                fileStream.SetSparseRange(0, fileStream.Length);
            }
        }
    }
    

    希望对你有帮助

    【讨论】:

    • 注意:Windows 资源管理器不知道如何复制稀疏文件。它将所有 0 字节数据复制为实际的 0 字节数据。因此,如果您想保持稀疏性,您可能应该在文件中保留一些关于稀疏区域的元数据,以便在管理员(用户)复制文件后恢复它们。
    猜你喜欢
    • 2011-04-30
    • 2014-03-28
    • 2014-10-03
    • 2018-02-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多