【问题标题】:What is the fastest way to get only directory list仅获取目录列表的最快方法是什么
【发布时间】:2017-06-20 16:20:26
【问题描述】:

我需要为给定的根/父路径递归地构建 only 目录的树结构。 类似于“浏览文件夹”对话框。

Delphi 的 FindFirst (FindFirstFile API) 不适用于 faDirectoryFindNext 将获取所有文件(它使用 faAnyFile 而不管指定的 faDirectory)不仅是目录。这使得构建树的过程非常缓慢。

有没有不使用FindFirst/FindNext的快速获取目录列表(树)的方法?

【问题讨论】:

  • 如果你不想写低级驱动,也许你可以看看.NET是如何在Directory.GetDirectories方法中完成这个任务的。但我敢打赌他们在那里使用FindFirstFile 功能。
  • 更快速的方式使用FindFirstFileEx,但是最快的使用NtQueryDirectoryFileFileDirectoryInformation。无论如何你都有文件和文件夹,但你可以通过dwFileAttributes轻松过滤它
  • @Victoria - 这是绝对错误的。这既是用户模式又是内核模式 api。如果您对此一无所知 - 不需要说
  • @DaveNottage,它是如何实现的?并且“fine”是非常相对的:) FindFirstFile/Ex 也可以“fine”工作,但它也枚举文件,而不仅仅是目录。
  • 您是否尝试分析以找到重的部分?

标签: delphi winapi


【解决方案1】:

绝对最快的方法,使用NtQueryDirectoryFile api。有了这个,我们可以一次查询多个文件而不是单个文件。还选择将返回什么信息(信息更小 - 速度更快)。示例(完全递归)

// int nLevel, PSTR prefix for debug only
void ntTraverse(POBJECT_ATTRIBUTES poa, int nLevel, PSTR prefix)
{
    enum { ALLOCSIZE = 0x10000 };//64kb

    if (nLevel > MAXUCHAR)
    {
        DbgPrint("nLevel > MAXUCHAR\n");
        return ;
    }

    NTSTATUS status;
    IO_STATUS_BLOCK iosb;
    UNICODE_STRING ObjectName;
    OBJECT_ATTRIBUTES oa = { sizeof(oa), 0, &ObjectName };

    DbgPrint("%s[<%wZ>]\n", prefix, poa->ObjectName);

    if (0 <= (status = NtOpenFile(&oa.RootDirectory, FILE_GENERIC_READ, poa, &iosb, FILE_SHARE_VALID_FLAGS, 
        FILE_SYNCHRONOUS_IO_NONALERT|FILE_OPEN_REPARSE_POINT|FILE_OPEN_FOR_BACKUP_INTENT)))
    {
        if (PVOID buffer = new UCHAR[ALLOCSIZE])
        {
            union {
                PVOID pv;
                PBYTE pb;
                PFILE_DIRECTORY_INFORMATION DirInfo;
            };

            while (0 <= (status = NtQueryDirectoryFile(oa.RootDirectory, NULL, NULL, NULL, &iosb, 
                pv = buffer, ALLOCSIZE, FileDirectoryInformation, 0, NULL, FALSE)))
            {

                ULONG NextEntryOffset = 0;

                do 
                {
                    pb += NextEntryOffset;

                    ObjectName.Buffer = DirInfo->FileName;

                    switch (ObjectName.Length = (USHORT)DirInfo->FileNameLength)
                    {
                    case 2*sizeof(WCHAR):
                        if (ObjectName.Buffer[1] != '.') break;
                    case sizeof(WCHAR):
                        if (ObjectName.Buffer[0] == '.') continue;
                    }

                    ObjectName.MaximumLength = ObjectName.Length;

                    if (DirInfo->FileAttributes & FILE_ATTRIBUTE_DIRECTORY)
                    {
                        ntTraverse(&oa, nLevel + 1, prefix - 1);
                    }

                } while (NextEntryOffset = DirInfo->NextEntryOffset);    
            }

            delete [] buffer;

            if (status == STATUS_NO_MORE_FILES)
            {
                status = STATUS_SUCCESS;
            }
        }

        NtClose(oa.RootDirectory);
    }

    if (0 > status)
    {
        DbgPrint("---- %x %wZ\n", status, poa->ObjectName);
    }
}
   


void ntTraverse()
{
    BOOLEAN b;
    RtlAdjustPrivilege(SE_BACKUP_PRIVILEGE, TRUE, FALSE, &b);

    char prefix[MAXUCHAR + 1];
    memset(prefix, '\t', MAXUCHAR);
    prefix[MAXUCHAR] = 0;

    STATIC_OBJECT_ATTRIBUTES(oa, "\\systemroot");
    ntTraverse(&oa, 0, prefix + MAXUCHAR);
}

但是如果您使用交互式树 - 您不需要一次展开所有树,而只需要展开顶层,处理 TVN_ITEMEXPANDINGTVE_EXPANDTVN_ITEMEXPANDEDTVE_COLLAPSE 以在用户单击和设置时展开/折叠节点cChildren

如果将FindFirstFileExWFIND_FIRST_EX_LARGE_FETCHFindExInfoBasic 一起使用,这将获得接近NtQueryDirectoryFile 的性能,但更小:

WIN32_FIND_DATA fd;
HANDLE hFindFile = FindFirstFileExW(L"..\\*", FindExInfoBasic, &fd, FindExSearchLimitToDirectories, 0, FIND_FIRST_EX_LARGE_FETCH);
if (hFindFile != INVALID_HANDLE_VALUE)
{
    do 
    {
        if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
        {
            if (fd.cFileName[0] == '.')
            {
                switch (fd.cFileName[1])
                {
                case 0:
                    continue;
                case '.':
                    if (fd.cFileName[2] == 0) continue;
                    break;
                }
            }
            DbgPrint("%S\n", fd.cFileName);
        }
    } while (FindNextFile(hFindFile, &fd));

    FindClose(hFindFile);
}

很遗憾FindExSearchLimitToDirectories 目前没有实现

【讨论】:

  • 我完全放弃了这个想法。我需要重新考虑返回 整个 目录树。对于任何方法,我都没有发现任何显着的改进。也许直接访问文件系统元数据就可以了,但我不是为此而生的……谢谢。
  • @zig - 你是一次尝试枚举所有目录树 - 还是根据用户请求(如果这是 gui 应用程序 - 树视图) - 如果你只扩展 1 级深度 - 一切都必须很好。为什么 Nt 或 Ex 方法更快 - 使用 FindFirstFile - 如果文件夹包含 N 个文件,则需要 N+1 调用 fs 驱动程序 - 每次调用都需要切换上下文用户/内核/用户,分配 Irp,FS 驱动程序需要大量同步在访问文件夹之前返回给我们..单个文件。这是非常昂贵的。当我们使用足够大的缓冲区时——我们只能调用一次——缓冲区将被 N 个文件填充。这一定有效果
  • 我还没有完全测试NtQueryDirectoryFile(今天才开始翻译 - Delphi7 中缺少大多数 NT 头文件)但我相信你的话。当我下周有更多时间时,我将尝试实际分析它与FindFirstFileEx
  • @zig - I_CHILDRENCALLBACK 在像"\windows\WinSxS" 这样的文件夹中具有巨大优势,其中子文件夹数量巨大 - 您不需要枚举所有,而只需要枚举接收mask &amp; TVIF_CHILDREN 的文件夹(仅当前可见)。我在注册表树上实现了这个(这里有点容易 - 完全存在信息 - 键有子键)。如果您正确执行下一个:&lt;TVN_GETDISPINFO&gt;&lt;TVN_ITEMEXPANDING, TVE_EXPAND&gt;(展开节点)&lt;TVN_ITEMEXPANDED, TVE_COLLAPSE&gt;(折叠节点)&lt;TVN_DELETEITEM&gt; - 一切都必须正常工作(赢 Nt*)
  • @RbMm:我不知道。有些图书馆的作者确实知道。
【解决方案2】:

Find(First|Next)/File() 是一个可行的解决方案,尤其是在 Delphi 7 中。只需过滤掉不需要的结果,例如:

if FindFirst(Root, faDirectory, sr) = 0 then
try
  repeat
    if (sr.Attr and faDirectory <> 0) and (sr.Name <> '.') and (sr.Name <> '..') then
    begin
      // ... 
    end;
  until FindNext(sr) <> 0;
finally
  FindClose(sr);
end;

如果这对您来说还不够快,那么其他选项包括:

  1. 在 Win7+ 上,将 FindFirstFileEx()FindExInfoBasicFIND_FIRST_EX_LARGE_FETCH 一起使用。这将提供比FindFirstFile() 更高的速度。

  2. 直接访问文件系统元数据。在 NTFS 上,您可以使用DeviceIoControl() 直接枚举Master File Table

【讨论】:

  • 我已经这样做了,而这正是我不想做的。 FindFirstfaDirectory 不仅会找到目录,还会找到任何文件。所以即使你过滤它也需要永远。 Windows 资源管理器很快就会显示目录树。
  • which has some speed improvements over FindFirstFile() - 对大文件夹的速度确实提升不小 - 因为FindFirstFile 每次调用只返回一个文件。所以文件夹中的 n 个文件 - n 个调用。 FindFirstFileEx() - 可以一次返回一组文件 - 有多少文件适合缓冲(现在如果不是错误的 64kb),但无论如何它至少可以调用 2 次内核。可能与 Nt 版本拥有和单个调用拥有并使用大缓冲区 - 所有这些都非常严重地加快了性能
  • @zig - Windows explorer sees to show the directory tree in no time - 因为资源管理器仅扩展当前级别。并点击下一级。不要尝试一次枚举。可能是您尝试只构建所有树,而不是仅在用户操作时枚举新节点
  • FindFirstFileEx() 的参数 fSearchOpFindExSearchLimitToDirectories 怎么样? Doc 声明这是一个“建议”标志,但值得一试。
  • @zett42:阅读FindExSearchLimitToDirectories 的文档。它只是一个建议性标志,不能保证在所有系统上都受支持,因此您仍然需要准备好手动过滤掉非目录条目。
【解决方案3】:

如果您有 Delphi XE2 或更新版本,最快的方法是使用在System.IOUtils 中定义的TDirectory.GetDirectories

示例代码:

procedure TVideoCamera.GetInterfaceNameList(
  const AInterfaceNameList: TInterfaceNameList);
const
  SEARCH_OPTION = TSearchOption.soTopDirectoryOnly;
  PREDICATE = nil;
var
  interfaceList: TStringDynArray;
  idxInterface: Integer;
  interfaceName: String;
begin
  interfaceList := TDirectory.GetDirectories(GetCameraDirectory, SEARCH_OPTION,
      PREDICATE);

  AInterfaceNameList.Clear;
  for idxInterface := Low(interfaceList) to High(interfaceList) do
  begin
    interfaceName := ExtractFileName(InterfaceList[idxInterface]);
    AInterfaceNameList.Add(interfaceName);
  end;
end;

【讨论】:

  • 看看这个函数的实现,我严重怀疑这是最快的方法。你知道这个 Q 中的“最快”是关于运行时性能吗?
  • 好吧,我不知道。 TDirectory 正在使用 FindFirst/FindNext 方法 - 此处并未将其视为慢速 - 并且完全支持此处要求的功能。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-25
相关资源
最近更新 更多