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ではCreatePipeCreateProcess、Linuxではpipeforkexecという違いはありますが、Windowsの方が50倍くらい遅いです。

Windows版でも、パイプやリダイレクトを使わなければ速いのですが、私が開発中のプログラムでは、コマンドの実行結果を取得する必要があります。

  • 遅さの要因は何か?
  • 実装に問題はないか?
  • 速くする方法はないか?

以上、よろしくお願いします。