使用 CreateProcess 时奇怪的减速

Weird slowdown when using CreateProcess

让我从一些示例代码开始。我为此做了一个最小的测试用例。要重现,需要两件:

第一个可执行文件,一个使用 CreateProcess 的小应用程序。我们称它为 Debugger.

#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;
}

第二个可执行文件,一个小应用程序,它会做一些愚蠢的事情来消耗时间。我们称这个为 App:

#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 并没有真正做任何事情)。然而,现实却大不相同:

  1. 运行 App(Windows 资源管理器/Ctrl-F5)。在我的电脑上每次迭代大约需要 1 秒。
  2. 运行 App 在 Visual Studio 调试器 (F5) 中。同样,每次迭代大约 1 秒。我所期望的。
  3. 运行 调试器 Visual Studio 调试器 (F5)。同样,每次迭代大约 1 秒。再一次,我所期望的。
  4. 运行 Debugger(仅来自 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)

尤其是 RtlpNtMakeTemporaryKey 好像出现了很多。不幸的是,我不知道这意味着什么,而且 Google 似乎没有帮助...

调试堆不同。阅读 The Windows Heap Is Slow When Launched from the Debugger

Accelerating Debug Runs, Part 1: _NO_DEBUG_HEAP

当进程初始化系统 (ntdll) 检查是否存在调试器时,如果检查是否存在环境变量 _NO_DEBUG_HEAP 并且设置为非零。如果否 - 设置 NtGlobalFlag(在 PEB 中)以调试堆使用(FLG_HEAP_ENABLE_TAIL_CHECKFLG_HEAP_ENABLE_FREE_CHECKFLG_HEAP_VALIDATE_PARAMETERS)所有这些检查并填充所有具有特殊模式的分配块(baadf00dabababab 在块的末尾)使所有堆 alloc/free 变慢(比较没有这种情况)

从另一方面看,您的程序大部分时间都使用堆中的 allocate/free 内存。

配置文件也显示了这个 - RtlAllocateHeapmemset - 当分配的块充满魔术图案时,RtlpNtMakeTemporaryKey - 这个 "function" 由单个指令组成 - jmp ZwDeleteKey - 所以你真的不在这个函数中,而是 "near" 它,在另一个与堆相关的函数中。


如前所述 Simon Mourier - 为什么情况 (2) 和 (3) 运行 与 (1) 一样快(当没有调试器时)但只有情况 (4 ) 更慢 ?

来自 C++ Debugging Improvements in Visual Studio "14"

So to improve performance when launching C++ applications with the Visual Studio debugger, in Visual Studio 2015 we disable the operating system’s debug heap.

这是在调试进程环境中通过设置_NO_DEBUG_HEAP=1完成的。所以比较 Accelerating Debug Runs, Part 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。当您在 debugger 下启动 debugger 和 app from your debugger - from CreateProcessW function

lpEnvironment

A pointer to the environment block for the new process. If this parameter is NULL, the new process uses the environment of the calling process.

因为你在这里传递了 0 - 所以应用程序使用与调试器相同的环境 - 继承 _NO_DEBUG_HEAP=1

但在情况 (4) 中 - 您没有自行设置 _NO_DEBUG_HEAP=1。结果使用了调试堆并且 运行 更慢。