Самый быстрый способ создания файлов в С#

Я запускаю программу, чтобы проверить, как быстро найти и повторить все файлы в папке с большим количеством файлов. Самая медленная часть процесса - создание 1 миллиона файлов плюс. Я использую довольно наивный метод для создания файлов на данный момент:

Console.Write("Creating {0:N0} file(s) of size {1:N0} bytes... ", 
    options.FileCount, options.FileSize);
var createTimer = Stopwatch.StartNew();
var fileNames = new List<string>();
for (long i = 0; i < options.FileCount; i++)
{
    var filename = Path.Combine(options.Directory.FullName, 
                        CreateFilename(i, options.FileCount));
    using (var file = new FileStream(filename, FileMode.CreateNew, 
                        FileAccess.Write, FileShare.None, 4096, 
                        FileOptions.WriteThrough))
    {
        // I have an option to write some data to files, but it not being used. 
        // That why there a using here.
    }
    fileNames.Add(filename);
}
createTimer.Stop();
Console.WriteLine("Done.");

// Other code appears here.....

Console.WriteLine("Time to  CreateFiles: {0:N3}sec ({1:N2} files/sec, 1 in {2:N4}ms)"
       , createTimer.Elapsed.TotalSeconds
       , (double)total / createTimer.Elapsed.TotalSeconds
       , createTimer.Elapsed.TotalMilliseconds / (double)options.FileCount);

Вывод:

Creating 1,000,000 file(s) of size 0 bytes... Done.
Time to  CreateFiles: 9,182.283sec (1,089.05 files/sec, 1 in 9.1823ms)

Если что-то явно лучше этого? Я хочу проверить на несколько порядков больше 1 миллиона, и для создания файлов требуется день!

Я не пробовал какой-либо parallelism, пытаясь оптимизировать любые параметры файловой системы или изменять порядок создания файла.

Для полноты здесь содержание CreateFilename():

public static string CreateFilename(long i, long totalFiles)
{
    if (totalFiles < 0)
        throw new ArgumentOutOfRangeException("totalFiles", 
            totalFiles, "totalFiles must be positive");

    // This tries to keep filenames to the 8.3 format as much as possible.
    if (totalFiles < 99999999)
        // No extension.
        return String.Format("{0:00000000}", i);
    else if (totalFiles >= 100000000 && totalFiles < 9999999999)
    {
        // Extend numbers into extension.
        long rem = 0;
        long div = Math.DivRem(i, 1000, out rem);
        return String.Format("{0:00000000}", div) + "." + 
            String.Format("{0:000}", rem);
    }
    else
        // Doesn't fit in 8.3, so just tostring the long.
        return i.ToString();
}

UPDATE

Пытался распараллеливаться в соответствии с предложением StriplingWarrior, используя Parallel.For(). Результаты: около 30 потоков разбивают мой диск и сеть замедляется!

        var fileNames = new ConcurrentBag<string>();
        var opts = new ParallelOptions();
        opts.MaxDegreeOfParallelism = 1;       // 1 thread turns out to be fastest.
        Parallel.For(0L, options.FileCount, opts,
            () => new { Files = new List<string>() },   
            (i, parState, state) =>
            {
                var filename = Path.Combine(options.Directory.FullName, 
                                   CreateFilename(i, options.FileCount));
                using (var file = new FileStream(filename, FileMode.CreateNew
                                  , FileAccess.Write, FileShare.None
                                  , 4096, FileOptions.WriteThrough))
                {
                }
                fileNames.Add(filename);
                return state;
            },
            state => 
            {
                foreach (var f in state.Files)
                {
                    fileNames.Add(f);
                }
            });
        createTimer.Stop();
        Console.WriteLine("Done.");

Обнаружено, что изменение FileOptions в FileStream улучшило perf на ~ 50%. Кажется, я отключил кеш-запись.

new FileStream(filename, FileMode.CreateNew, 
                 FileAccess.Write, FileShare.None, 
                 4096, FileOptions.None)

Результаты:

Creating 10,000 file(s) of size 0 bytes... Done.
Time to  CreateFiles: 12.390sec (8,071.05 files/sec, 1 in 1.2390ms)

Другие идеи по-прежнему приветствуются.

Ответ 1

Самый быстрый способ, который я нашел, был простой цикл вокруг File.Create():

IEnumerable filenames = GetFilenames();
foreach (var filename in filenames)
{
    File.Create(filename);
}

Что эквивалентно (что я фактически использую в коде):

IEnumerable filenames= GetFilenames();
foreach (var filename in filenames)
{
    new FileStream(filename, FileMode.CreateNew, 
             FileAccess.Write, FileShare.None, 
             4096, FileOptions.None)
}

И если вы действительно хотите что-то записать в файл:

IEnumerable filenames= GetFilenames();
foreach (var filename in filenames)
{
    using (var fs = new FileStream(filename, FileMode.CreateNew, 
             FileAccess.Write, FileShare.None, 
             4096, FileOptions.None))
    {
        // Write something to your file.
    }
}

Вещи, которые, похоже, не помогают:

  • Parallelism в форме Parallel.ForEach() или Parallel.For(). Это приводит к спаду в сети, который ухудшается по мере увеличения количества потоков.
  • Согласно StriplingWarrior, SSD. Я еще не тестировал себя (пока), но я предполагаю, что это может быть потому, что так много мелких записей.

Ответ 2

Ваше самое большое узкое место здесь, без сомнения, является вашим жестким диском. В некоторых быстрых тестах я смог увидеть некоторые существенные улучшения производительности (но не по порядку величины), воспользовавшись parallelism:

Parallel.For(1, 10000,
    i => File.Create(Path.Combine(path, i.ToString())));

Интересно, что на моей машине, по крайней мере, SSD, похоже, не имеет большого значения для этой операции.

  • На моем жестком диске приведенный выше код создает 100 000 файлов примерно через 31 секунду.
  • В моем SDD приведенный выше код создает 100 000 файлов примерно через 33 секунды.

Ответ 3

Очень поздний ответ.. но я сам столкнулся с этой проблемой.

Выполнение создания было ключевым вопросом в моем случае.

С помощью инструмента fsutil мы могли бы создавать файлы намного быстрее. Но запуск процесса для каждого файла был снова медленнее. Таким образом, мы объединили команды и передали их cmd.exe. Максимальный размер файла Cmd.exe составляет 8000 символов. Таким образом, процесс cmd был вызван на 8000 символов.. и еще раз в конце.

Мы сравнили эту проблему с простым foreach:

For Each path In filenames3
    Using File.Create(path)
    End Using
Next

Unit тест дал такой результат:

Files to generate per folder: 45900 files
Files to generate: 688500 files
Let really generate: 4981 files (random distinct for a shorter test time)
fsutil took: 10359 ms
delete took: 1654 ms
File create took: 28633 ms
delete took: 24998

Итак: 10359 мс против 28633 мс. Если вам нужно только создать файлы, это очень хороший выигрыш времени. Также обратите внимание, что очистка этих созданных файлов происходит НАМНОГО быстрее, поэтому убедитесь, что вы понимаете, что делает fsutil, прежде чем использовать его.

!! ВНИМАНИЕ: Административные привилегии обязательны!

Я закончил с этим кодом:

Private Function CreateFiles(input As IEnumerable(Of String)) As String
    Dim sb As New StringBuilder("/c ", 8000)
    Dim ret As New StringBuilder


    For Each path In input
        Dim newline = "fsutil file createNew """ & path & """ 0 & "
        If sb.Length + newline.Length > 8000 Then
            ret.AppendLine(CallFSUtil(sb.ToString))
            sb.Clear()
            sb.Append("/c ")
        End If

        sb.Append(newline)
    Next
    ret.AppendLine(CallFSUtil(sb.ToString))
    Return ret.ToString
End Function

Private Function CallFSUtil(command As String) As String
    Dim pi As New ProcessStartInfo("cmd", command) With {
        .RedirectStandardOutput = True,
        .RedirectStandardError = True,
        .UseShellExecute = False,
        .CreateNoWindow = True
    }
    Dim p As New Process With {
        .StartInfo = pi
    }
    p.Start()
    Return p.StandardOutput.ReadToEnd
End Function