Странное замедление при использовании CreateProcess - PullRequest
1 голос
/ 09 октября 2019

Позвольте мне начать с примера кода. Я сделал минимальный тестовый пример для этого. Для воспроизведения необходимы две части:

Первый исполняемый файл, небольшое приложение, которое использует CreateProcess. Давайте назовем это Отладчик .

#include <Windows.h>
#include <string>
#include <iostream>
#include <vector>

int main()
{
    STARTUPINFO         si = {0};
    PROCESS_INFORMATION pi = {0};
    si.cb = sizeof(si);

    // Starts the 'App':
    auto exe = L"C:\\Tests\\x64\\Release\\TestProject.exe";
    std::vector<wchar_t> tmp;
    tmp.resize(1024);
    memcpy(tmp.data(), exe, (1 + wcslen(exe)) * sizeof(wchar_t));

    auto result = CreateProcess(NULL, tmp.data(), NULL, NULL, FALSE, DEBUG_PROCESS, NULL, NULL, &si, &pi);
    DEBUG_EVENT debugEvent = { 0 };
    bool continueDebugging = true;
    while (continueDebugging) 
    {
        if (WaitForDebugEvent(&debugEvent, INFINITE))
        {
            std::cout << "Event " << debugEvent.dwDebugEventCode << std::endl;
            if (debugEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT)
            {
                continueDebugging = false;
            }

            // I real life, this is more complicated... For a minimum test, this will do
            auto continueStatus = DBG_CONTINUE;
            ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, continueStatus);

        }
    }
    std::cout << "Done." << std::endl;

    std::string s;
    std::getline(std::cin, s);

    return 0;
}

Второй исполняемый файл, небольшое приложение, которое делает что-то глупое, которое тратит время. Давайте назовем это Приложение :

#include <Windows.h>
#include <iostream>
#include <string>
#include <vector>

__declspec(noinline) void CopyVector(uint64_t value, std::vector<uint8_t> data)
{
    // irrelevant.
    data.resize(10);
    *reinterpret_cast<uint64_t*>(data.data()) = value;
}

int main(int argc, const char** argv)
{
    for (int i = 0; i < 10; ++i) 
    {
        LARGE_INTEGER StartingTime, EndingTime, ElapsedMicroseconds;
        LARGE_INTEGER Frequency;

        QueryPerformanceFrequency(&Frequency);
        QueryPerformanceCounter(&StartingTime);

        // Activity to be timed
        std::vector<uint8_t> tmp;
        tmp.reserve(10'000'000 * 8);

        // The activity (*)
        uint64_t v = argc;
        for (size_t j = 0; j < 10'000'000; ++j)
        {
            v = v * 78239742 + 1278321;

            CopyVector(v, tmp);
        }

        QueryPerformanceCounter(&EndingTime);
        ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;

        // We now have the elapsed number of ticks, along with the
        // number of ticks-per-second. We use these values
        // to convert to the number of elapsed microseconds.
        // To guard against loss-of-precision, we convert
        // to microseconds *before* dividing by ticks-per-second.

        ElapsedMicroseconds.QuadPart *= 1000000;
        ElapsedMicroseconds.QuadPart /= Frequency.QuadPart;

        std::cout << "Elapsed: " << ElapsedMicroseconds.QuadPart << " microsecs" << std::endl;
    }

    std::string s;
    std::getline(std::cin, s);
}

Обратите внимание, что приложение отладчик на самом деле ничего не делает. Он просто сидит там, ожидая, пока приложение app будет готово. Я использую последнюю версию VS2019.

Сейчас я протестировал четыре сценария. Для каждого сценария я рассчитал время, необходимое для одной итерации (с переменной i). Я ожидал бы, что при запуске App (1) и Debugger (4) будет примерно одинаковая скорость (потому что Debugger isn 'действительно ничего не делаю). Однако реальность совсем иная:

  1. Запуск Приложение (проводник Windows / Ctrl-F5). Это займет примерно 1 секунду на каждую итерацию на моем компьютере.
  2. Запустите Приложение в отладчике Visual Studio (F5). Опять же, около 1 секунды на итерацию. Что бы я ожидал.
  3. Запустите Отладчик в отладчике Visual Studio (F5). Опять же, около 1 секунды на итерацию. Опять же, что я и ожидал.
  4. Запустить Отладчик (просто из Windows Explorer или ctrl-F5). На этот раз нам придется подождать ок. 4 секунды (!) На итерацию. Не то, что я ожидал!

Я сузил проблему до аргумента vector<uint8_t> data, который передается по значению (вызывая копию c'tor).

Мне бы очень хотелось узнать, что здесь происходит ... Почему работает отладчик в 4 раза медленнее, пока ничего не делает?

- обновить-

Я добавил некоторые возможности трассировки и профилирования стека в мою маленькую программу отладчика, используя собственную библиотеку ... для сравнения случаев (3) и (4) друг с другом. Я в основном посчитал, как часто указатель в моей трассировке стека происходит.

Эти методы можно найти заметно в результатах случая (4), но они незначительны в случае (3). Число в начале - это простой счетчик:

352       - inside memset (address: 0x7ffa727349d5)
284       - inside RtlpNtMakeTemporaryKey (address: 0x7ffa727848b2)
283       - inside RtlAllocateHeap (address: 0x7ffa726bbaba)
261       - inside memset (address: 0x7ffa727356af)
180       - inside RtlFreeHeap (address: 0x7ffa726bfc10)
167       - inside RtlpNtMakeTemporaryKey (address: 0x7ffa72785408)
161       - inside RtlGetCurrentServiceSessionId (address: 0x7ffa726c080f)

Особенно RtlpNtMakeTevenKey, кажется, часто появляется. К сожалению, я понятия не имею, что это значит, и Google, похоже, не помогает ...

1 Ответ

4 голосов
/ 13 октября 2019

отличается в куче отладки. read Куча Windows медленна при запуске из отладчика

и Ускорение выполнения отладки, часть 1: _NO_DEBUG_HEAP

при инициализации процесса (ntdll)) check - присутствует ли отладчик, если check - переменная окружения _NO_DEBUG_HEAP существует и имеет ненулевое значение. если нет - установите NtGlobalFlag (в PEB) для отладки использования кучи (FLG_HEAP_ENABLE_TAIL_CHECK, FLG_HEAP_ENABLE_FREE_CHECK, FLG_HEAP_VALIDATE_PARAMETERS) всех этих проверок и заполнения всех выделенных блоков специальным шаблоном (baadf00d иabababab в конце блока) замедляет выделение / освобождение всей кучи (сравните без этого случая)

с другой стороны ваша программа чаще всего использует для выделения / освобождения памяти из кучи.

профиль также показывает это - RtlAllocateHeap, memset - конечно, когда выделенный блок заполнен магическим шаблоном, RtlpNtMakeTemporaryKey - эта «функция» состоит из одной инструкции - jmp ZwDeleteKey - так что вы действительно не внутри этой функции, а «рядом»"это, внутри другой функции, связанной с кучей.


, как отмечено Саймон Мурье - почему дела (2) и (3) работают так же быстро, как (1) (когда нет отладчика)) но только в случае (4) медленнее?

из Улучшения отладки в C ++ в Visual Studio "14"

Таким образом, чтобы повысить производительность при запуске приложений C ++ сотладчик Visual Studio, в Visual Studio 2015 отключаемкуча отладки операционной системы.

это делается с помощью набора _NO_DEBUG_HEAP=1 в среде отлаженного процесса. сравните Ускорение отладочных прогонов, часть 1: _NO_DEBUG_HEAP (статья устарела) - теперь это по умолчанию.

мы можем проверить это по следующему коду в приложении:

WCHAR _no_debug_heap[32];
if (GetEnvironmentVariable(L"_NO_DEBUG_HEAP", _no_debug_heap, _countof(_no_debug_heap)))
{
    DbgPrint("_NO_DEBUG_HEAP=%S\n", _no_debug_heap);
}
else
{
    DbgPrint("error=%u\n", GetLastError());
}

поэтому, когда мы запускаем приложение под отладчиком - нет кучи отладки, потому что VS отладчик добавляет _NO_DEBUG_HEAP=1. когда вы запускаете ваш отладчик в режиме отладчика, а приложение - из CreateProcessW функция

lpEnvironment

Указатель на блок среды для нового процесса. Если этот параметр имеет значение NULL, новый процесс использует среду вызывающего процесса.

, так как вы передаете здесь 0 - поэтому приложение использует ту же среду, что и отладчик - наследует _NO_DEBUG_HEAP=1

но в случае (4) - вы не установите _NO_DEBUG_HEAP=1 самостоятельно. в результате использовалась отладочная куча и работала медленнее.

...