TreeSize Free如何在列出文件夹大小时如此快速?



程序TreeSize Free可以列出目录内的文件夹,并根据文件大小对它们进行降序排序,以找到最大的文件夹/文件来清理您的硬盘。我在想他们怎么这么快就搞定了。假设我想在c++中有效地计算文件夹大小,我将使用以下现代c++代码:

size_t calculate_directory_size(const std::filesystem::path& directory, const size_t maximum_size)
{
size_t size = 0;
for (std::filesystem::recursive_directory_iterator directory_iterator(directory);
directory_iterator != std::filesystem::recursive_directory_iterator(); ++directory_iterator)
{
if (!is_directory(*directory_iterator))
{
size += file_size(*directory_iterator);
}
if (size > maximum_size)
{
// Save time
return size;
}
}
return size;
}

然而,即使在优化的构建中运行此代码也比TreeSize所能做的要慢得多(例如,至少慢3 - 4倍)。有没有比我的实现更快的迭代和总结文件大小的技巧?我不认为"聪明的多线程"将提供如此巨大的好处,因为磁盘访问不能真正多线程地获得巨大的性能增益。

多线程

磁盘访问不能真的是多线程的,以获得巨大的性能增益

这是一般不成立. 这主要取决于硬件和操作系统(OS)堆栈(文件系统、驱动程序、实际操作系统等)。

这对于硬盘驱动器(HDD)来说通常是正确的。事实上,它们本质上是顺序的,主要是由于磁头和旋转磁盘。然而,一个好的操作系统堆栈可以根据头部的实时位置和要获取的块的位置巧妙地对操作进行优先级排序。然而,HDD的速度很大程度上受到巨大的寻道时间的限制,而且在大多数现代文件系统上,被搜索文件的层次结构几乎从来不是连续的(尽管有一个缓存来避免多次读取)。

对于固态硬盘(SSD),这更复杂:获取块的时间要小得多,但它仍然有明显的延迟。异步请求多个文件可以更快然后做一个同步循环,等待每个块被接收,然后请求一个新的块。现代NVMe SSD每秒可以实现数十万个IO请求,因此异步操作至关重要。使用多线程是一种使事情更加异步的方法,尽管它通常不是很有效。

TreeSize使用多线程使得计算更快在我的机器上安装了一块NVMe SSD(三星970 EVO Plus)和一个i5-9600KF处理器。以下是C:Windows目录的(近似)计时:

1 core: 11.0 s
2 core:  9.0 s
6 core:  7.5 s

计时是通过将线程的亲和性调优到固定数量的内核来产生的。使用多线程并不是灵丹妙药,但仍然比在某些平台上对TreeSize代码进行串行操作要好得多。

注意,分析信息显示,在目录扫描期间,实际上只有3个TreeSize线程同时处于活动状态。其中一个显然不太活跃,它似乎管理所有(GUI)事件,而其他两个则执行IO操作。这也可以解释为什么操作不能很好地扩展。


c++标准库性能

即使使用一个核心,在TreeSize和你的c++代码之间也有很大的性能差距。实际上,在我的机器上,使用GNU c++编译器,前者需要11秒,而后者需要46秒。

低级剖析分析显示c++代码的大部分时间花在了7个函数上:

Time | Function name
--------------------------------------------------------------------------
28% | std::filesystem::status
25% | std::filesystem::__cxx11::recursive_directory_iterator::operator++
20% | std::filesystem::file_size
11% | GetFileAttributesW
5% | wfindfirst64
3% | wfindnext64
2% | findclose
... | ...

根据分析信息,大约75%的时间花在stdlibc++库中,而不是内核中。我真的不知道为什么,因为分析器没有访问这里使用的libstdc++库的编译代码。话虽如此,这显然是不合理的。事实上,对于这个用例,不应该需要GetFileAttributesW。实际上,wfindfirst64wfindnext64已经提供了有关文件大小和文件名的信息。这个recursive_directory_iterator的实现效率很低. 然而,并非所有标准c++库实现都是如此。


Windows的快速实现

可以直接使用Win32 API编写基本代码。更具体地说,FindFirstFileWFindNextFileWWin32调用:

size_t calculate_directory_size_win32(const fs::path& directory, const size_t maximum_size)
{
size_t size = 0;
WIN32_FIND_DATAW infos;
std::vector<std::wstring> paths_to_scan;
paths_to_scan.push_back(directory.wstring());
while(not paths_to_scan.empty())
{
std::wstring current_path = std::move(paths_to_scan.back());
paths_to_scan.pop_back();
HANDLE hFind = FindFirstFileW((current_path + L"\*").c_str(), &infos);
if(hFind != INVALID_HANDLE_VALUE)
{
do
{
if (infos.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
if(wcscmp(infos.cFileName, L".") != 0 && wcscmp(infos.cFileName, L"..") != 0)
paths_to_scan.push_back(current_path + L'\' + infos.cFileName);
}
else
{
size += (size_t(infos.nFileSizeHigh) << 32) | infos.nFileSizeLow;
}
if (size > maximum_size)
return size;
}
while(FindNextFileW(hFind, &infos) != 0);
FindClose(hFind);
}
}
return size;
}
上面的代码支持基本目录(可能需要额外的检查特殊实体,如符号链接),并且要快得多在我的机器上:只需要8秒。

说到树大小,刷新完成后,大部分时间都花在CreateFileWCloseFileW上。这有点令人惊讶,除非他们只是在需要的时候更新每个文件的大小,基于保存在某处的文件树缓存。

请注意,在所有基准测试中,操作都会重复多次,因此由于相对较快的操作系统缓存,大多数IO操作实际上不会与SSD交互。

最新更新