使用 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 并没有真正做任何事情)。然而,现实却大不相同:
- 运行 App(Windows 资源管理器/Ctrl-F5)。在我的电脑上每次迭代大约需要 1 秒。
- 运行 App 在 Visual Studio 调试器 (F5) 中。同样,每次迭代大约 1 秒。我所期望的。
- 运行 调试器 Visual Studio 调试器 (F5)。同样,每次迭代大约 1 秒。再一次,我所期望的。
- 运行 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_CHECK
、FLG_HEAP_ENABLE_FREE_CHECK
、FLG_HEAP_VALIDATE_PARAMETERS
)所有这些检查并填充所有具有特殊模式的分配块(baadf00d
和 abababab
在块的末尾)使所有堆 alloc/free 变慢(比较没有这种情况)
从另一方面看,您的程序大部分时间都使用堆中的 allocate/free 内存。
配置文件也显示了这个 - RtlAllocateHeap
,memset
- 当分配的块充满魔术图案时,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
。结果使用了调试堆并且 运行 更慢。
让我从一些示例代码开始。我为此做了一个最小的测试用例。要重现,需要两件:
第一个可执行文件,一个使用 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 并没有真正做任何事情)。然而,现实却大不相同:
- 运行 App(Windows 资源管理器/Ctrl-F5)。在我的电脑上每次迭代大约需要 1 秒。
- 运行 App 在 Visual Studio 调试器 (F5) 中。同样,每次迭代大约 1 秒。我所期望的。
- 运行 调试器 Visual Studio 调试器 (F5)。同样,每次迭代大约 1 秒。再一次,我所期望的。
- 运行 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_CHECK
、FLG_HEAP_ENABLE_FREE_CHECK
、FLG_HEAP_VALIDATE_PARAMETERS
)所有这些检查并填充所有具有特殊模式的分配块(baadf00d
和 abababab
在块的末尾)使所有堆 alloc/free 变慢(比较没有这种情况)
从另一方面看,您的程序大部分时间都使用堆中的 allocate/free 内存。
配置文件也显示了这个 - RtlAllocateHeap
,memset
- 当分配的块充满魔术图案时,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
。结果使用了调试堆并且 运行 更慢。