TreeSize Free为何能快速计算文件夹大小?C++实现优化探讨
TreeSize Free是一款可列出目录内文件夹、按文件大小降序排序的工具,能帮用户找出占用空间最大的文件/文件夹以清理硬盘。我好奇它为何速度如此之快,自己用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 Free慢3-4倍。有没有比当前实现更高效的迭代和文件大小求和技巧?另外我觉得多线程不会带来这么大的性能提升,毕竟磁盘访问很难靠多线程大幅提速。
TreeSize Free之所以快,核心是它没有依赖C++标准库的通用实现,而是直接调用了Windows原生API(比如FindFirstFileExW、GetFileSizeEx,甚至更底层的NTFS相关接口),避开了标准库的额外开销。针对你的代码,有几个关键优化点:
1. 跳过标准库的冗余操作
std::filesystem的迭代器每次迭代都会做很多兼容性检查和抽象层处理,比如跨平台路径解析、权限验证等。直接用Windows原生API遍历目录,可以减少这些额外开销:
- 用
FindFirstFileExW开启FINDEX_INFO_LEVELS::FindExInfoBasic模式,只获取必要的文件信息,避免读取冗余属性; - 遍历过程中直接从
WIN32_FIND_DATAW结构体里提取文件大小,不用单独调用file_size(这会额外发起一次系统调用)。
2. 利用NTFS的特性加速
如果目标磁盘是NTFS格式,可以直接读取MFT(主文件表)相关信息,不用逐个遍历文件:
- 调用
DeviceIoControl配合FSCTL_GET_NTFS_VOLUME_DATA获取卷信息; - 用
FSCTL_GET_RETRIEVAL_POINTERS直接读取文件的簇信息,计算实际占用空间(注意:文件大小和实际占用磁盘空间可能不同,TreeSize会区分这两个值)。
3. 批量处理与缓存优化
- 一次性读取多个目录项,减少系统调用次数;
- 利用操作系统的文件系统缓存,优先遍历内存中已缓存的目录,避免重复磁盘IO;
- 你的代码里每次判断
size > maximum_size就返回,但可以提前预判:如果当前目录的已统计大小加上该目录的预估大小(比如从目录元数据获取)已经超过阈值,直接跳过整个子目录。
4. 多线程的正确用法
你说多线程没用其实不准确——机械硬盘确实很难靠多线程提速,但SSD可以并行处理多个IO请求。TreeSize会在遍历不同子目录时用多线程并行处理,因为不同子目录的文件分布在磁盘的不同位置,SSD可以同时读取这些数据,大幅提升速度。
优化后的示例思路(伪代码)
#include <windows.h> ULONGLONG calculate_directory_size(const wchar_t* directory, ULONGLONG maximum_size) { ULONGLONG total_size = 0; WIN32_FIND_DATAW find_data; HANDLE find_handle = FindFirstFileExW(directory, FindExInfoBasic, &find_data, FindExSearchNameMatch, nullptr, 0); if (find_handle == INVALID_HANDLE_VALUE) return 0; do { if (wcscmp(find_data.cFileName, L".") == 0 || wcscmp(find_data.cFileName, L"..") == 0) continue; if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { wchar_t sub_dir[MAX_PATH]; swprintf_s(sub_dir, L"%s\\%s", directory, find_data.cFileName); total_size += calculate_directory_size(sub_dir, maximum_size - total_size); if (total_size > maximum_size) break; } else { ULONGLONG file_size = (ULONGLONG)find_data.nFileSizeHigh << 32 | find_data.nFileSizeLow; total_size += file_size; if (total_size > maximum_size) break; } } while (FindNextFileW(find_handle, &find_data) != FALSE); FindClose(find_handle); return total_size; }
这段伪代码直接调用Windows原生API,避免了std::filesystem的抽象开销,同时在遍历过程中直接从WIN32_FIND_DATAW获取文件大小,减少了一次系统调用,速度会比你的原代码快很多。
内容的提问来源于stack exchange,提问作者BullyWiiPlaza

