[英]How is TreeSize Free so fast at listing folder sizes?
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 操作。 這也可以解釋為什么操作不能很好地擴展。
即使使用 1 個內核,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++ 庫上,而不是在 kernel 上。我真的不知道為什么,因為分析器無法訪問這里使用的 libstdc++ 庫的編譯代碼。 話雖如此,這顯然似乎不合理。 事實上,對於用例,不應該需要GetFileAttributesW
。 實際上, wfindfirst64
和wfindnext64
已經提供了有關文件大小和文件名的信息。 recursive_directory_iterator
的這種實現非常低效。 但是,並非所有標准 C++ 庫實現都是如此。
可以直接使用 Win32 API 編寫基本代碼。更具體地說, FindFirstFileW
和FindNextFileW
Win32 調用:
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 秒。
當談到 TreeSize 時,大部分時間都花在刷新完成時的CreateFileW
和CloseFileW
上。 這有點令人驚訝,除非他們僅在需要時根據保存在某處的文件樹緩存更新每個文件的大小。
請注意,在所有基准測試中,操作都會重復多次,因此由於相對較快的操作系統緩存,大多數 IO 操作實際上並不與 SSD 交互。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.