Описание приложения
У меня есть автономный инструмент для обработки данных. Этот инструмент загружает сотни тысяч файлов. Для каждого из них он выполняет некоторые вычисления, а когда записывает один индексный файл. Это все С++ (все IO - через стандартные объекты/функции библиотеки) и компилируется с таргетингом на Visual Studio 2013 amd64.
Производительность
В моем тестовом наборе данных содержится 115 757 файлов, которые необходимо обработать. Размер файлов составляет 731 МБ, а средний размер файла - 6 КБ.
- Первый запуск: 12 секунд
- Второй запуск: ~ 18 минут
Thats 90x slower! Второй запуск экстраполируется с одной минуты времени выполнения. Все работает после этого, как я уже догадался, одинаково медленны.
Сюрприз!
Если я переименую папку с файлами в ней, а затем переименую ее обратно в то, что она изначально была, в следующий раз, когда я запустим приложение, она снова будет работать быстро!
Свое же приложение, машина и исходные данные. Единственное отличие состоит в том, что одна папка была временно переименована.
До сих пор я могу воспроизвести этот 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
. Он проверит для zipped-версии файла, не найдет его, а затем проверит для распакованного и найдет его.
Профилирование также показало, что диск не попал ни в один из запусков приложения, однако файл IO был ожидаемым. Я считаю, что это указывает на то, что файлы уже были выгружены в память.
Файл IO также показал, что продолжительность файлов "Создание" событий была в среднем МНОГО выше в медленной версии. 26 us vs 11704 us.
машина
- Samsung SSD 830 Series
- Intel i7 860
- Windows 7 64 бит
- Файловая система NTFS.
- 32GB Ram
Резюме
- Во втором прогоне вызовы в
NtfsFindPrefix
занимают гораздо больше времени. - Это функция в драйвере NTFS.
- Диск не попал ни в один из профилей, файлы были поданы со страниц в памяти.
- Кажется, что операции переименования достаточно, чтобы остановить эту проблему в следующем прогоне.
Вопрос
Теперь, когда фоновая информация не работает, кто-нибудь узнает, что происходит и как ее исправить?
Кажется, я мог обойти это, переименовав папку сам, но это кажется... грязным. плюс им не уверен, что это даже работает.
Является ли переименование недействительными страниц в памяти и вызывает их обновление до следующего запуска? Это ошибка в драйвере NTFS?
Спасибо за чтение!
Обновление!!
После некоторого дополнительного профилирования, похоже, что часть, выполняющая медленнее, тестирует, чтобы увидеть, существует ли несуществующий zipped файл. Если я удалю этот тест, все, кажется, станет быстрее.
Мне также удалось воспроизвести эту проблему в небольшом приложении на С++, которое все тоже видят. Обратите внимание, что пример кода создаст 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 argv, const char* argc[] )
{
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 здесь. Надеюсь, они могут помочь пролить свет на проблему.