Windowsで標準出力をパイプでリダイレクトする子プロセス実行が遅い
WindowsのプログラムをC++で作っています。CreateProcessで子プロセスを起動します。その子プロセスの実行結果を取得するために、パイプを使って標準出力を親プロセスに取り込んでいます。
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>
#include <io.h>
#include <string>
std::string run(std::string const &command)
{
std::string ret;
try {
HANDLE hOutputRead = INVALID_HANDLE_VALUE;
HANDLE hInputWrite = INVALID_HANDLE_VALUE;
HANDLE hErrorWrite = INVALID_HANDLE_VALUE;
HANDLE hOutputReadTmp = INVALID_HANDLE_VALUE;
HANDLE hOutputWrite = INVALID_HANDLE_VALUE;
HANDLE hInputWriteTmp = INVALID_HANDLE_VALUE;
HANDLE hInputRead = INVALID_HANDLE_VALUE;
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = 0;
sa.bInheritHandle = TRUE;
HANDLE currproc = GetCurrentProcess();
// パイプを作成
if (!CreatePipe(&hOutputReadTmp, &hOutputWrite, &sa, 0))
throw std::string("Failed to CreatePipe");
// 子プロセスのエラー出力
if (!DuplicateHandle(currproc, hOutputWrite, currproc, &hErrorWrite, 0, TRUE, DUPLICATE_SAME_ACCESS))
throw std::string("Failed to DuplicateHandle");
// パイプを作成
if (!CreatePipe(&hInputRead, &hInputWriteTmp, &sa, 0))
throw std::string("Failed to CreatePipe");
// 子プロセスの標準出力
if (!DuplicateHandle(currproc, hOutputReadTmp, currproc, &hOutputRead, 0, FALSE, DUPLICATE_SAME_ACCESS))
throw std::string("Failed to DupliateHandle");
// 子プロセスの標準入力
if (!DuplicateHandle(currproc, hInputWriteTmp, currproc, &hInputWrite, 0, FALSE, DUPLICATE_SAME_ACCESS))
throw std::string("Failed to DupliateHandle");
// 不要なハンドルを閉じる
CloseHandle(hOutputReadTmp);
CloseHandle(hInputWriteTmp);
// プロセス起動
PROCESS_INFORMATION pi;
STARTUPINFOA si;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdInput = hInputRead; // 標準入力ハンドル
si.hStdOutput = hOutputWrite; // 標準出力ハンドル
si.hStdError = hErrorWrite; // エラー出力ハンドル
char *tmp = (char *)alloca(command.size() + 1);
strcpy(tmp, command.c_str());
if (!CreateProcessA(0, tmp, 0, 0, TRUE, CREATE_NO_WINDOW, 0, 0, &si, &pi)) {
throw std::string("Failed to CreateProcess");
}
// 不要なハンドルを閉じる
CloseHandle(hOutputWrite);
CloseHandle(hInputRead);
CloseHandle(hErrorWrite);
// 入力を閉じる
CloseHandle(hInputWrite);
while (1) {
char buf[256];
DWORD len = 0;
if (!ReadFile(hOutputRead, buf, sizeof(buf), &len, nullptr)) break;
if (len < 1) break;
ret += std::string(buf, len);
}
CloseHandle(hOutputRead);
// 終了
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
} catch (std::string const &e) { // 例外
OutputDebugStringA(e.c_str());
}
return ret;
}
int main()
{
DWORD t = GetTickCount();
std::string ret;
for (int i = 0; i < 100; i++) {
ret = run("git --version");
}
puts(ret.c_str());
t = GetTickCount() - t;
printf("%ums\n", t);
}
これと同じことをLinuxでもやってみました。
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/time.h>
#include <string>
std::string run(char const *file, char *const *argv)
{
std::string ret;
try {
const int R = 0;
const int W = 1;
int fd_r;
int fd_w;
int child_to_parent[2], parent_to_child[2];
int pid;
if (pipe(child_to_parent) < 0) {
throw std::string("failed: pipe");
}
if (pipe(parent_to_child) < 0) {
close(child_to_parent[R]);
close(child_to_parent[W]);
throw std::string("failed: pipe");
}
pid = fork();
if (pid < 0) {
close(child_to_parent[R]);
close(child_to_parent[W]);
close(parent_to_child[R]);
close(parent_to_child[W]);
throw std::string("failed: fork");
}
if (pid == 0) { // child
close(parent_to_child[W]);
close(child_to_parent[R]);
dup2(parent_to_child[R], R);
dup2(child_to_parent[W], W);
close(parent_to_child[R]);
close(child_to_parent[W]);
if (execvp(file, argv) < 0) {
close(parent_to_child[R]);
close(child_to_parent[W]);
fprintf(stderr, "failed: exec\n");
exit(1);
}
}
close(parent_to_child[R]);
close(child_to_parent[W]);
fd_w = parent_to_child[W];
fd_r = child_to_parent[R];
//
close(fd_w);
while (1) {
char buf[256];
int n = read(fd_r, buf, sizeof(buf));
if (n < 1) break;
ret += std::string(buf, n);
}
close(fd_r);
} catch (std::string const &e) {
fprintf(stderr, "%s\n", e.c_str());
exit(1);
}
return ret;
}
unsigned int get_tick_count()
{
struct timeval tv;
if (gettimeofday(&tv, nullptr) != 0) return 0;
return (tv.tv_sec * 1000) + (tv.tv_usec / 1000);
}
int main()
{
static char *argv[] = {
"git",
"--version",
nullptr
};
unsigned int t = get_tick_count();
std::string ret;
for (int i = 0; i < 10000; i++) {
ret = run(argv[0], argv);
}
puts(ret.c_str());
t = get_tick_count() - t;
printf("%ums\n", t);
return 0;
}
プログラムの内容は、git --version
コマンド繰り返しを実行するというものです。
Windows版は100回ループして、約5秒
Linux版は10000回ループして、約9秒
逆算すると、1回の実行に、
Windows版では、0.05秒
Linux版では、0.0009秒
かかっています。
※テスト環境は Core i7 2.9GHz VMware Hypervisor 上の仮想マシン
WindowsではCreatePipe
とCreateProcess
、Linuxではpipe
とfork
とexec
という違いはありますが、Windowsの方が50倍くらい遅いです。
Windows版でも、パイプやリダイレクトを使わなければ速いのですが、私が開発中のプログラムでは、コマンドの実行結果を取得する必要があります。
- 遅さの要因は何か?
- 実装に問題はないか?
- 速くする方法はないか?
以上、よろしくお願いします。