Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create fast and efficient filestream writes on large sparse files

I have an application that writes large files in multiple segments. I use FileStream.Seek to position each wirte. It appears that when I call FileStream.Write at a deep position in a sparse file the write triggers a "backfill" operation (writeing 0s) on all preceding bytes which is slow.

Is there a more efficient way of handling this situation?

The below code demonstrates the problem. The initial write takes about 370 MS on my machine.

    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());
    }
like image 973
revoxover Avatar asked Jul 23 '13 18:07

revoxover


People also ask

Does NTFS support sparse files?

Support for sparse files is introduced in the NTFS file system as another way to make disk space usage more efficient. When sparse file functionality is enabled, the system does not allocate hard disk drive space to a file except in regions where it contains nonzero data.

What are sparse files used for?

Sparse files are commonly used for disk images, database snapshots, log files and in scientific applications.

What is a sparse file record?

A file format that saves storage space by recording only actual data. Whereas regular files record empty fields as blank data or runs of nulls, a sparse file includes meta-data that describe where the runs of non-data are located. The reported file size is always the size of the entire file.


2 Answers

NTFS does support Sparse Files, however there is no way to do it in .net without p/invoking some native methods.

It is not very hard to mark a file as sparse, just know once a file is marked as a sparse file it can never be converted back in to a non sparse file except by coping the entire file in to a new non sparse file.

Example useage

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); 
    }
}

Once a file has been marked as sparse it now behaves like you excepted it to behave in the comments too. You don't need to write a byte to mark a file to a set size.

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);
    }
}

enter image description here

like image 60
Scott Chamberlain Avatar answered Oct 30 '22 12:10

Scott Chamberlain


Here is some code to use sparse files:

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;
    }
}

And sample code to test the above class.

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);
        }
    }
}

Hope this helps

like image 38
Paul Avatar answered Oct 30 '22 10:10

Paul