7

Надо сканировать огромное количество файлов и папок. Обычно для этого используется метод Directory.EnumerateFileSystemEntries. Возможно ли как-то ускорить процесс чтения файловой системы?

Stack
  • 9,452

2 Answers2

11

Можно ускорить почти в два раза, если использовать WinAPI.

// Microsoft (R) Roslyn C# Compiler version 1.1.0.51204
using System.Runtime.InteropServices;

[DllImport("kernel32.dll")] static extern int GetLastError();
[DllImport("kernel32.dll")] static extern bool FindClose(IntPtr handle);
class SafeHandle : Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid {
    private SafeHandle() : base(true) { }
    protected override bool ReleaseHandle() { return FindClose(this.handle); }
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
struct DATA {       // WIN32_FIND_DATA
    public FileAttributes FileAttributes;
    public System.Runtime.InteropServices.ComTypes.FILETIME CreationTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME LastAccessTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME LastWriteTime;
    public uint FileSizeHigh;
    public uint FileSizeLow;
    public uint Reserved0;
    public uint Reserved1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string FileName;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
    public string AlternateFileName;
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern SafeHandle FindFirstFileEx(string name, int i, 
                                         out DATA data, int so, IntPtr sf, int f);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern bool FindNextFile(SafeHandle h, out DATA data);

IEnumerable<DATA> ReadPath(string path) {  // например: "c:\" | "c:\*.png" | "c:\*"
    DATA d;
    var p = String.Concat(@"\\?\", path.TrimEnd('\\', ' '));
    using (var h = FindFirstFileEx(p, 1, out d, 0, IntPtr.Zero, 2)) {
        if (h.IsInvalid) throw new System.ComponentModel.Win32Exception(GetLastError());
        yield return d;
        while (FindNextFile(h, out d)) if (d.FileName != "..") yield return d;
        var e = GetLastError();   // e = 18 -- дошли до конца
        if (e != 18) throw new System.ComponentModel.Win32Exception(e);
    }
}

Получить файлы и папки в c:\:

foreach (var d in ReadPath(@"c:\*")) 
   Console.WriteLine("name=" + d.FileName);

Получить файлы в c:\temp\ и подпапках:

class File { public string Path; public string Name; }
IEnumerable<File> Scan(string path) {
    foreach (var d in ReadPath(path + "*").Skip(1)) {
        // сканируем только подпапки, а junction пропускаем
        if ((d.FileAttributes & FileAttributes.Directory) != 0
            && (d.FileAttributes & FileAttributes.ReparsePoint) == 0)
            foreach (var f in Scan(path + d.FileName + "\\")) 
               yield return f;
        else
            yield return new File { Path = path, Name = d.FileName };
    }
}

long ToLong(uint high, uint low) { return (long)(((UInt64)high << 0x20) | (UInt64)low); }

foreach (var f in Scan(@"c:\temp\")) 
   Console.WriteLine(
      f.Path + "\t\t\t" + f.Name + " size=" + ToLong(d.FileSizeHigh, d.FileSizeLow));
Stack
  • 9,452
  • 3
    [DllImport("kernel32.dll")] static extern int GetLastError(); - вместо этого надо использовать Marshal.GetLastWin32Error. – Qwertiy Jan 13 '16 at 18:13
  • "А за счёт чего происходит ускорение?" -- тут прямые вызовы WinAPI. а в Directory.EnumerateFileSystemEntries очень много всего. посмотрите исходник – Stack Jan 13 '16 at 18:26
  • @Qwertiy "надо использовать Marshal.GetLastWin32Error" -- выдает неправильный код если Tasks.Task и много подзадач (т.е. TaskCreationOptions.AttachedToParent). примера под рукой нет, и возможно это был какой-то побочный эффект. как-нибудь проверю и напишу. – Stack Jan 13 '16 at 18:55
  • А по ссылке утверждается, что наоборот winapi-вариант может дать неправильный код, если там сборщик мусора запустится. – Qwertiy Jan 13 '16 at 19:01
  • @Qwertiy "winapi-вариант может дать неправильный код," -- от версии CLR зависит, как там сказано: "If you compile this with .NET2, it will produce "2 / 0"; if you switch to .NET 4, it will output "2 / 2"". т.е. надо проверять. – Stack Jan 13 '16 at 19:06
  • @VladD "вы вроде бы не закрываете хендл, а надеетесь на сборщик мусора." -- где не закрываю? вы смотрели на SafeHandle и на using? – Stack Jan 13 '16 at 19:41
  • @Stack: А, точно, закрываете. Окей, вы не обрабатываете ошибки кроме no more files и file not found. (http://referencesource.microsoft.com/#mscorlib/system/io/__error.cs,142.) Ну и код в BCL идёт тоже через те же нативные функции, но делает больше — вы и вправду считаете, что всё, что они там делают, лишнее, и это можно безболезненно выкинуть? – VladD Jan 13 '16 at 19:47
  • @VladD "точно, закрываете. Окей, вы не обрабатываете ошибки" -- напомнили "а на цепочку закрыл?)". это пример-основа. кому надо тот может дописать то, что требуется. "вы и вправду считаете, что всё, что они там делают, лишнее" -- про все сказать не могу. не смотрел. главное что код работает быстрее чем .Enumerate* и делает то, что мне надо. если найдете баг, напишите. – Stack Jan 13 '16 at 20:08
  • @VladD "Это для FindNext, а не для FindFirst" -- а для FindFrst.. тоже есть, см. if (h.IsInvalid) throw ... – Stack Jan 13 '16 at 21:03
  • @VladD, валидный хендл одновременно с ошибкой? Это возможно? – Qwertiy Jan 13 '16 at 21:07
  • @VladD "валидный хендл одновременно с ошибкой?" -- если при вызове FindFirstFileEx возникнет ошибка, то h.IsInvalid = true. "Я не хочу копать ошибки, это было бы не очень этично с моей стороны." -- я не обижусь если найдете. мне самому инетересно в каких ситуациях этот код сломается. – Stack Jan 13 '16 at 21:21
  • @Stack: Если каталог пустой (или файлов по маске не найдено?), то FindFirstFileEx по идее возвращает INVALID_HANDLE_VALUE, у вас это, кажется, приведёт к исключению. – VladD Jan 13 '16 at 21:24
  • @VladD "Если каталог пустой (или файлов по маске не найдено?) ... у вас это, кажется, приведёт к исключению" -- да. потому что если garbage in, то fail-fast. – Stack Jan 13 '16 at 21:32
  • 1
    @VladD, кстати, у этого кода ещё полезный момент есть - он должен уметь обрабатывать пути длиннее 256 символов за счёт \\?\. – Qwertiy Jan 13 '16 at 21:33
  • @Stack: Э, не, пустой каталог не должен быть ошибкой. Чем количество файлов 0 хуже, чем 5? – VladD Jan 13 '16 at 21:35
  • @VladD "пустой каталог не должен быть ошибкой" -- если нужны данные по папке, то не надо ставить суффикс \* и не будет ошибки. а если ставите \* то значит намеревались получить содержимое, но его нет - значит fail fast. – Stack Jan 13 '16 at 21:37
  • По поводу обработки длинных имён — я читал у разработчиков BCL, почему они не обрабатывают длинные имена. Дело в том, что это по сути бесполезно — вы ведь передадите имя файла на дальнейшую обработку другим системным функциям, а у них снова ограничение в 260 символов. Это как-бы неправильно, ограничение должно быть общесистемным, и исправляться на общесистемном уровне, а не костылём. – VladD Jan 13 '16 at 21:37
  • Я здесь согласен с @Qwertiy — если я, например, хочу удалить или там забекапить все файлы в 50 заранее заданных каталогах, то обработать 0 файлов в каком-то из каталогов нормально. – VladD Jan 13 '16 at 21:40
  • @Qwertiy: Это потому, что он должен использоваться во всех случаях, даже когда нужна экстремальная скорость, а у нас тут как бы конкретный, в котором время обработки подавляет всё остальное. То есть с технической стороны вы, скорее всего, правы, но мне это кажется слишком мелкой монетой, чтобы за ней нагибаться. – VladD Jan 13 '16 at 21:42
  • @VladD "Должна быть пустая коллекция, а не ошибка" -- нет ошбки. видно, код не запускали. так вот в любой папке есть записи . и ... и если .. отфильтровывается, то . возвращается и для пустой папки при запросе с суффиксом \* – Stack Jan 13 '16 at 21:43
  • Хм. А вот это уже серьёзнее. В худшем случае будет затёртая память со всеми своими прелестями. – VladD Jan 13 '16 at 21:43
  • @Qwertiy: Опередили :) – VladD Jan 13 '16 at 21:45
  • @Qwertiy "с 260 там всё равно что-то не то будет, наверное:" -- эти 260 это только на имя файла, без пути. путь передается только на входе в FindFirstFileEx, а на выходе только имена – Stack Jan 13 '16 at 21:45
  • @Qwertiy "если маской будет не , а какой-нибудь .txt" -- в коде в комменте перечислены примеры. один из них как-раз "*.png" – Stack Jan 13 '16 at 21:47
  • @VladD "В худшем случае будет затёртая память со всеми своими прелестями" -- данные передаются из WinAPI наружу в DATA. так что все ок. а если нет, то значит это баг в WinAPI. но пока никто жаловался. – Stack Jan 13 '16 at 21:48
  • @Qwertiy "а в 2 раза на чём-то ведь замерялось" -- с помощью Stopwatch смотрел сколько ElapsedMilliseconds при сканировании папки с 10 тыс. файлов в EnumerateFileSystemEntries и сколько в ReadPath. запускал несколько раз. – Stack Jan 13 '16 at 21:55
  • @VladD "проблема будет только если имя файла длиннее 260 символов" -- такого имени (только имени; без пути) не будет, т.к. его просто не получится создать. а вот длина всего пути до файла может быть равна 32767 – Stack Jan 13 '16 at 21:58
  • в коде в комменте перечислены примеры. один из них как-раз "*.png" - имелось в виду, что будет в случае пустой папки при такой маске? ведь . и .. ей уже не соответствуют. – Qwertiy Jan 13 '16 at 22:02
  • @Qwertiy "а если на структуре замерить?" -- я что-то пропустил наверное. какой структуре? DATA это и так struct – Stack Jan 13 '16 at 22:04
  • class File { public string Path; public string Name; } - вот тут вместо class сделать struct. – Qwertiy Jan 13 '16 at 22:05
  • @VladD "А примонтированный сетевой диск?" -- если FindFirstFileEx может с ним работать, то все ок. – Stack Jan 13 '16 at 22:06
  • @Stack: Может, конечно. Вопрос в том, что там может быть файловая система с именами длиннее чем 260 символов. И FindFirstFileEx придётся вернуть какую-нибудь ошибку. – VladD Jan 13 '16 at 22:07
  • @Qwertiy " вместо class сделать struct." -- попробую. но получится что в Scan будет создаваться структура, и yield return наружу. при этом будет копирование структуры, а не передача ссылки, не так ли? – Stack Jan 13 '16 at 22:09
  • 1
    Да, так. Но в структуре только две ссылки, т. е. копируемый размер увеличивается незначительно, а необходимость выделения памяти и сборки мусора отпадает. – Qwertiy Jan 13 '16 at 22:10
  • @VladD "может быть файловая система с именами длиннее чем 260 символов. И FindFirstFileEx придётся вернуть какую-нибудь ошибку" -- на входе ReadPath может быть строка.length = 32767; вы видели такой путь? – Stack Jan 13 '16 at 22:12
  • Кстати, я тут подумал насчёт ускорения в 2 раза - ты не собираешь конкатенацию строк чтобы получить полный путь, в отличие от стандартной функции - на этом можно сэкономить что-то. Потому что я не думаю, что прямое обращение к api как-то экономит на взаимодействии с файловой системой - скорее окружающий код. – Qwertiy Jan 13 '16 at 22:12
  • @VladD "длинные пути, что имя файла больше 260 символов" -- да, полный путь к файлу (т.е. весь путь и имя) чтобы был под 30 тыс. символов в длину. – Stack Jan 13 '16 at 22:19
  • @Qwertiy " не собираешь конкатенацию строк чтобы получить полный путь" -- и не вызываю Path.GetFullPath, который вызывает ужасный NormalizePath(path, true) – Stack Jan 13 '16 at 22:23
  • @Qwertiy "вместо class сделать struct." -- проверил. для 9509 файлов. ElapsedMilliseconds, для struct File 34;30;28;27;28;39;27;28;27;27; для class File 30;27;26;27;28;29;28;32;29;27; – Stack Jan 13 '16 at 22:42
  • @Stack: Таких длинных нет. Но имя файла (не полный путь, а именно имя) длиннее 260 символов — да. – VladD Jan 13 '16 at 22:58
  • Как-то маловато. Я думал, ты там на несколько минут поиск запустил. А при 10К файлов даже не факт, что сборщик мусора хоть раз сработал. – Qwertiy Jan 13 '16 at 23:33
  • 2
    Комментарии не предназначены для расширенной дискуссии; разговор перемещен в чат. – Nick Volynkin Jan 14 '16 at 06:59
  • 3
    Судя по всему, вы проверяли результат на разогретом дисковом кэше - т.е. несколько раз читали одну и ту же папку, и брали среднее значение. В таких условиях у меня результат на 18к файлов - 500ms -> 300ms, т.е. почти в два раза. Это хороший результат, но стоит учитывать что при этом не происходит реального обращения к диску. В реальных условиях дисковых кэш скорее всего будет холодным, и я намерял в этом случае разницу 3700ms -> 3500ms. Можете привести конретные результаты измерений, указав тип диска (hdd/sdd) и состояние кэша перед измерениями? –  Jan 14 '16 at 11:11
  • @PashaPash "состояние кэша перед измерениями" -- как определить состояние кэша? диск hdd. 99% времени 'спит' – Stack Jan 14 '16 at 14:19
  • @Stack Я использовал утилиту RamMAP + сброс Standby Lists. Она позволяет посмотреть конкретные закэшированные файлы, но вроде бы не позволяет закэшированные папки (т.е. то, что файлами не является) После сброса - при полностью пустом кэше, ваш код у меня отрабатывает за 10s (при 300-400ms на разогретом), и во время работы слышно как диск старается. На SSD на холодном кэше - около 900ms. На горячем - 400ms (почему-то чуть медленее, чем на HDD, скорее всего просто из-за системности диска) –  Jan 14 '16 at 17:10
  • @Stack т.е. код действительно применим, но основной профит он принесет в определенных условиях - например, для частого чтения одной и той же структуры папок в поисках изменений (ведь он нахяляву еще и даты файлов выдает). –  Jan 14 '16 at 17:12
2

Нужно индексировать FS в базу данных и работать потом с базой.

cpp_user
  • 1,503
  • "индексировать FS в базу данных" -- как это сделать? – Stack Jan 13 '16 at 17:57
  • По хорошему драйвер на файловую систему навесить, по плохому тупо периодически обходить ее. – cpp_user Jan 13 '16 at 18:10
  • "По хорошему драйвер на файловую систему навесить" -- это как-то сложно. и не всегда возможно. "периодически обходить ее" -- чтобы обходить ее быстро надо использовать WinAPI. – Stack Jan 13 '16 at 18:22
  • Смысл был в том чтобы один раз обойти FS и через драйвер ловить все изменения FS. Никакое WinAPI такой уровень поддержки актуальности индекса и быстроты обеспечить не может. – cpp_user Jan 13 '16 at 18:27
  • Т. е. если ты будешь делать программу, которой надо искать что-то на диске, ты к ней в комплект дашь драйвер, который будет следить за всеми действиями в файловой системе? Ну круто. – Qwertiy Jan 13 '16 at 18:30
  • 2
    Если задача состоит в том что нужно искать очень и очень быстро, а не по полчаса - то да. – cpp_user Jan 13 '16 at 18:34
  • @cpp_user "через драйвер ловить все изменения FS." -- а как это сделать? Process Monitor отлавливает все обращения к диску, но как это делается найти не смог. – Stack Jan 13 '16 at 18:41
  • Ищи по "File System Filter Driver - called on every file system I/O operation (create, read, write, rename, remove, ...)" – cpp_user Jan 13 '16 at 18:51
  • Кстати, а что будет с обновлением данных, если изменения будут внесены из другой ОС, например? – Qwertiy Jan 13 '16 at 19:39
  • Это уже административная проблема, если у вас можно внести изменения другой осью(что мешает поставить драйвер и на эту ось?) или просто молотком по железу. – cpp_user Jan 13 '16 at 19:52