31944


WindowsのプログラムをC++で作っています。CreateProcessで子プロセスを起動します。その子プロセスの実行結果を取得するために、パイプを使って標準出力を親プロセスに取り込んでいます。

[code]

    #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);
    }
    
[/code]

これと同じことをLinuxでもやってみました。

[code]

    #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;
    }
    
[/code]

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

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

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


CC BY-SA 3.0
2017-01-16T04:02:25.870
31879
2017-01-23T23:06:12.237
3337
question
c++ linux windows c
Windowsで標準出力をパイプでリダイレクトする子プロセス実行が遅い


挙げられているコードをプロファイル測定してみましたが、95%が`ReadFile()`でした。プロセス間通信に使用しているパイプが遅いということですからどうしようもないと思います。  
一応、`CreatePipe`で作る匿名パイプよりも`CreateNamedPipe`で作る名前付きパイプの方がオプションが多く非同期にも対応できパフォーマンスを向上させることもできますが、大規模データに対する遅延が原因でない以上、名前付きパイプでもさしたる改善は見込めないと思います。

> Linuxと比べて50倍も差が‌​あることに納得できない

一般論でなく`git --version`つまりGit for
Windowsに限定すると、`GIT/cmd/git.exe`が`GIT/mingw64/bin/git.exe`を呼び出すラッパーとなっているため、それだけでもオーバーヘッドになっています。尚且つMinGW64のオーバーヘッドもあります。  
Visual
C++にて単純に`printf()`だけを実行するプログラムと比較したところ３倍ほど高速化しました。ただし、これを差し引いても15倍以上の差があるのでこれらの点については言及しませんでした。

個人的な感想ですが、UNIX系OSはパイプによるプロセス間通信が多数回・頻繁に行われるために、それを前提としたOS設計がなされているのに対し、Windowsではほとんどパイプが使われていない、もしくは使われていてもプロセスを頻繁に停止させることがないため、それほど重要視されていないのかもしれません。

* * *

もし`git`コマンド実行結果を得たいのであれば[`libgit2`](https://libgit2.github.com/)を使われてはどうでしょうか？


CC BY-SA 3.0
2017-01-18T09:16:59.010
31944
2017-01-23T23:06:12.237
2017-01-23T23:06:12.237
4236
4236
31879
answer