第二次运行应用程序时文件加载速度较慢,带有重现代码

Posted

技术标签:

【中文标题】第二次运行应用程序时文件加载速度较慢,带有重现代码【英文标题】:Files loading slower on second run of application, with repro code 【发布时间】:2015-04-19 05:25:33 【问题描述】:

应用说明

我有一个离线数据处理工具。该工具可加载数十万个文件。对于每一个,它都会执行一些计算,并在完成后写入一个索引文件。它全是 C++(所有 IO 都是通过标准库对象/函数),并且正在使用针对 amd64 的 Visual Studio 2013 进行编译。

性能

我的测试数据集有 115,757 个文件需要处理。文件总大小为 731MB,文件大小中位数为 6KB。

首次运行:12 秒 第二轮:~18 分钟

慢了 90 倍! 第二次运行是从一分钟的运行时间推断出来的。之后的所有运行,正如我迄今为止所经历的那样,都同样缓慢。

惊喜!

如果我重命名包含文件的文件夹,然后将其重命名回原来的名称,下次我运行该应用程序时它会再次快速执行!

它是相同的应用程序、机器和源数据。唯一的区别是临时重命名了一个文件夹。

到目前为止,我可以 100% 地重现这一点。

分析

下一步自然是进行剖析。我分析了快速运行和慢速运行并比较了热点。在慢速版本中,大约 86% 的应用程序花费在一个名为 NtfsFindPrefix 的函数中。快速版本在此处花费了大约 0.4% 的时间。这是调用堆栈:

Ntfs.sys!NtfsFindPrefix<itself>
Ntfs.sys!NtfsFindPrefix
Ntfs.sys!NtfsFindStartingNode
Ntfs.sys!NtfsCommonCreate
Ntfs.sys!NtfsCommonCreateCallout
ntoskrnl.exe!KySwitchKernelStackCallout
ntoskrnl.exe!KiSwitchKernelStackContinue
ntoskrnl.exe!KeExpandKernelStackAndCalloutEx
Ntfs.sys!NtfsCommonCreateOnNewStack
Ntfs.sys!NtfsFsdCreate
fltmgr.sys!FltpLegacyProcessingAfterPreCallbacksCompleted
fltmgr.sys!FltpCreate
ntoskrnl.exe!IopParseDevice
ntoskrnl.exe!ObpLookupObjectName
ntoskrnl.exe!ObOpenObjectByName
ntoskrnl.exe!NtQueryAttributesFile
ntoskrnl.exe!KiSystemServiceCopyEnd
ntdll.dll!NtQueryAttributesFile
KernelBase.dll!GetFileAttributesW
DataGenerator.exe!boost::filesystem::detail::status

有问题的提升电话是exists 电话。它将测试文件的压缩版本,找不到它,然后测试解压缩的文件并找到它。

分析还显示磁盘没有受到应用程序运行的影响,但文件 IO 预计很高。我相信这表明文件已经被分页到内存中。

文件 IO 还显示文件“创建”事件的持续时间在慢速版本中平均要长得多。 26 us vs 11704 us

机器

三星 SSD 830 系列 英特尔 i7 860 Windows 7 64 位 NTFS 文件系统。 32GB 内存

总结

在第二次运行时,调用 NtfsFindPrefix 需要更长的时间。 这是 NTFS 驱动程序中的一个函数。 磁盘在任一配置文件中均未命中,文件是从内存中的页面提供的。 重命名操作似乎足以阻止此问题在下次运行时发生。

问题

现在背景信息已经不碍事了,有没有人知道发生了什么并知道如何解决它?

似乎我可以通过自己重命名文件夹来解决它,但这似乎......很脏。另外,我不确定为什么会这样。

重命名是否会使内存中的页面无效并导致它们在下次运行之前得到更新?这是 NTFS 驱动程序中的错误吗?

感谢阅读!


更新!!

经过更多分析后,看起来执行速度较慢的部分正在测试是否存在不存在的压缩文件。如果我删除此测试,一切似乎又会变得更快。

我还设法在一个小型 C++ 应用程序中重现了这个问题,供大家查看。请注意,示例代码将在您的计算机上的当前目录中创建 100k 6KB 文件。其他人可以复制吗?

// using VS tr2 could replace with boost::filesystem
#include <filesystem>
namespace fs = std::tr2::sys;
//namespace fs = boost::filesystem;

#include <iostream>
#include <string>
#include <chrono>
#include <fstream>

void createFiles( fs::path outDir )

    // create 100k 6KB files with junk data in them. It doesn't matter that they are all the same.
    fs::create_directory( outDir );
    char buf[6144];
    for( int i = 0; i < 100000; ++i )
    
        std::ofstream fout( outDir / fs::path( std::to_string( i ) ), std::ios::binary );
        fout.write( buf, 6144 );
    

    fs::rename( outDir, fs::path( outDir.string() + "_tmp" ) );
    fs::rename( fs::path( outDir.string() + "_tmp" ), outDir );


int main( int argc, const char* argv[] )

    fs::path outDir = "out";

    if( !fs::exists( outDir ) )
        createFiles( outDir );

    auto start = std::chrono::high_resolution_clock::now();

    int counter = 0;
    for( fs::recursive_directory_iterator i( outDir ), iEnd; i != iEnd; ++i )
    
        // test the non existent one, then the other
        if( !fs::exists( fs::path( i->path().string() + "z" ) ) && fs::exists( i->path() ) )
            counter += 1;

        if( counter % 100 == 0 )
            std::cout << counter << std::endl;
    
    std::cout << counter << std::endl;

    auto end = std::chrono::high_resolution_clock::now();
    std::chrono::duration< double, std::milli > s( end - start );

    std::cout << "Time Passed: " << s.count() << "ms" << std::endl;

    return 0;


更新 2

我记录了 MS here 的问题。希望他们能帮助阐明这个问题。

【问题讨论】:

程序中读取文件数据的代码导致文件系统缓存丢失目录条目。数据太多,它会推出旧信息。第一次很快,缓存仍然有你之前所做的任何事情。. 之后很慢,现在磁盘读取器头必须通过 MFT 来查找文件。您需要更多 RAM 或更快的磁盘。数据库应位于列表顶部。 更新:慢版本中所有额外的时间都花在寻找不存在的文件的压缩版本上。两个版本都做这个检查。 我现在有复制代码了!重现此问题只需约 50 行。 考虑上传 ETW 跟踪,以便人们无需运行重现代码即可进行调查。这也可以作为其行为方式的存档,并将包括许多相关的详细信息,例如内存量、磁盘类型、操作系统版本等。 这似乎很可能是 ntfs.sys 中的性能错误。缓存所有数据的运行不应该更慢,期间。您可以将其报告给 Microsoft,并提供跟踪和重现,然后就这样离开。如果您想深入挖掘:查看慢速数据,其中 CPU 使用率(采样)列排列为进程、线程、模块、函数、地址、橙色条、计数。然后钻入 Ntfs.sys!NtfsFindPrefix,然后按地址排序。您现在在函数中有一个样本图。使用本地内核调试来获取该函数的汇编并将它们关联起来。 【参考方案1】:

我最好的猜测是,Windows 索引器在被修改后立即创建文件锁定,正如 Damon 所说

NtfsFindPrefix 确实做了相当多的 FCB 获取和释放,但除非代码不正确(在这种情况下它应该失败),为什么第二次运行它会变慢?在您修改该文件夹中的文件后,Windows 资源管理器或索引服务会执行某种“智能操作”,例如扫描所有文件以调整专用文件夹类型,并保持锁定这样做可能吗?您是否测试过如果在第二次运行之前等待几分钟什么都不做,或者如果您杀死 Explorer 和索引器会发生什么?

我建议创建一个专门用于保存您编写的任何程序的数据的文件夹:

例子

C:/Users/%yourusername%/DataAnalysis

使用此文件夹保存原始软件访问的所有数据文件。创建文件夹后,您必须禁用索引。

我相信在文件夹属性菜单中有一个选项可以禁用文件夹的索引。如果没有,请按照说明 here 禁用对该文件夹的索引并避免出现问题。

该页面将告诉您访问控制面板并访问索引选项。在此处选择修改并浏览文件系统并取消选择刚刚创建的文件夹以避免对其内容编制索引。

让我知道它是否有效!

【讨论】:

它没有用。我还完全禁用了我的防病毒实时文件系统保护。这提高了第一次和第二次运行的性能,但两者之间的巨大差异仍然存在。 另外,仅供参考,我让我的电脑整晚都开着,给它足够的时间来完成索引,但今天早上我的下一次运行同样慢。【参考方案2】:

如果瓶颈在于存在调用,那么如果您将文件列表读入内存数据结构并针对您的数据结构测试文件是否存在,您可能会获得更好的性能。

使用 FindFirstFileEx/FindNextFile 获取工作文件夹中所有文件的名称。将结果加载到 std::vector (或您选择的容器)中。种类。使用 std::binary_search 检查特定文件是否存在。

我已经编写了很多工具来处理单个文件夹中的大量文件,根据我的经验,FindFirstFileEx/FindNextFile 是这些场景的最佳选择。

【讨论】:

以上是关于第二次运行应用程序时文件加载速度较慢,带有重现代码的主要内容,如果未能解决你的问题,请参考以下文章

在 Visual Studio 之外启动时程序运行速度较慢

jQuery Mobile 面板在页面完全加载之前在初始加载时显示,速度较慢的设备

关闭块不会在第二次 swift3 上重新加载表

AdvancedFilter 宏在 AutoFilter 关闭时运行速度较慢

第二次从相机拍摄时无法加载图像

为啥当我使用变量存储数值结果而不是重新计算时,C++ 程序运行速度较慢?