22

私は現在、C++11 とその優れた機能を学ぼうとしています。具体的には、効率の高い汎用性を探しています。そこで、C++11 で入力ファイルの行をソートするプログラムを喜んで作成し、私の新鮮なスキルをテストしました。C++ コンパイラのインライン化と優れた機能により、この小さな例では高いパフォーマンスが期待できました。私のプログラムがどれほど高速であったかについてのヒントを得るために、関数を使用して C でまったく同じプログラムをハッキングしましたqsort。これは生の C であり、この関数に対してインライン化は実行されず、比較関数は間接指定で呼び出され、2 つの間接指定を実行する必要があるためです。char *文字列を表すポインタにアクセスします。

事実

それでも、結果には非常に驚きました。C は C++ よりも 4 倍速いようです。8Mb ファイルでは、次の結果が得られます。

$ g++ -O3 -std=c++11 -o sort sort.C
$ time ./sort < huge > /dev/null

real    0m0.415s
user    0m0.397s
sys     0m0.013s

$ cc -O3 -Wall -o sortc sort.c
$ time ./sortc < huge  > /dev/null

real    0m0.104s
user    0m0.097s
sys     0m0.010s

$ wc -l huge
140190 huge

可能な限り公平にしようとしたことに注意してください。コンパイル オプションは同じであり、私の C プログラム (後でダンプされます) は C++ と同じように動作します。入力行のサイズに制限がなく、入力数に制限もありません。行。

mallocまた、私の C プログラムは入力行ごとにほぼ 1 回呼び出すのに対し、C++ プログラムでは入力行ごとに 10 回の割り当てがあることにも気付きました。

コード

比較に使用した 2 つのプログラムを次に示します。

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <memory>

int main () {
    typedef std::vector<std::string> svec;
    svec a;
    std::string s;

    for (;;) {
        getline(std::cin, s);
        if (std::cin.eof()) {
            if (s != "")
                a.push_back(std::move(s));
            break;
        }
        a.push_back(std::move(s));
    }
    std::sort(a.begin(), a.end());
    for (std::string &s : a) {
        std::cout << s << "\n";
    }
}

そして、私のはるかに冗長なCバージョン。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define BUFSZ 100
size_t getl(char **line, size_t len) {
        char buf[BUFSZ];
        size_t i, n;

        for (i=0; i<BUFSZ; i++) {
                int c = getchar();

                if (c == EOF || c == '\n') {
                        *line = malloc(len+i+1);
                        memcpy(&(*line)[len], buf, i);
                        (*line)[len+i] = 0;
                        return i;
                }
                buf[i] = c;
        }

        n = getl(line, len+i);
        memcpy(&(*line)[len], buf, i);
        return i+n;
}

#define ARRAYSZ 30
struct Array {
        char **lv;
        size_t li, lc;
};

void addline(struct Array *a, char *line) {
        if (a->li == a->lc) {
                a->lc *= 2;
                a->lv = realloc(a->lv, a->lc * sizeof *a->lv);
        }
        a->lv[a->li++] = line;
}

int cmp(const void *a, const void *b) {
        return strcmp(*(const char **)a, *(const char **)b);
}

int main(void) {
        char *line;
        struct Array a;
        size_t i;

        a.li = 0;
        a.lc = ARRAYSZ;
        a.lv = malloc(a.lc * sizeof *a.lv);

        for (;;) {
                getl(&line, 0);
                if (feof(stdin)) {
                        if (line[0] != 0)
                                addline(&a, line);
                        else
                                free(line);
                        break;
                }
                addline(&a, line);
        }
        qsort(a.lv, a.li, sizeof *a.lv, cmp);
        for (i=0; i<a.li; i++) {
                printf("%s\n", a.lv[i]);
                free(a.lv[i]);
        }
        free(a.lv);
        return 0;
}

質問

私の C++ プログラムのどこを (単純な C にならずに) 高速化するために変更する必要があるか教えてもらえますか? 私は非常にイディオムにとどまろうとしました.C++でハックするのは良い方法ですか、それとも高性能が必要な場合はCのようなコードを書く傾向がありますか? C++ プログラムがヒープにそれほど多くを割り当てているのはなぜですか? どうすればこれを減らすことができますか?

編集

多くの要望により、C++ プログラムのプロファイリングの結果を表示します。これは、私の C++ プログラムのプロファイラーの面白い出力です (最初の 2 行)。

Each sample counts as 0.01 seconds.
 %   cumulative   self              self     total           
time   seconds   seconds    calls  ms/call  ms/call  name    
40.03      0.02     0.02  1198484     0.00     0.00  __gnu_cxx::__normal_iterator<std::string*, std::vector<std::string, std::allocator<std::string> > >::operator--()
30.02      0.04     0.02  2206802     0.00     0.00  bool std::operator< <char, std::char_traits<char>, std::allocator<char> >(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)

読んでみると、配分だけが理由ではないようです。

4

5 に答える 5

27

原因はc++stdio同期にあります。次のコード:

int main () {
    typedef std::vector<std::string> svec;
    svec a;
    std::string s;

    // note
    std::ios_base::sync_with_stdio(false);

    for (;;) {
    getline(std::cin, s);
    if (std::cin.eof()) {
        if (s != "")
            a.push_back(std::move(s));
        break;
    }
        a.push_back(std::move(s));
    }
    std::sort(a.begin(), a.end());
    for (std::string &s : a) {
        std::cout << s << "\n";
    }
}

与える

 real   0m0.106s
 user   0m0.104s
 sys    0m0.004s

Cバージョンはこれを提供します:

 real   0m0.167s
 user   0m0.164s
 sys    0m0.000s

編集:もちろん、RiaDsync_with_stdioは静的関数であるため、すべてのstdioストリームに対して関数を1回呼び出すだけで十分です。

于 2012-08-16T11:32:24.207 に答える
11

また、2 つの異なる I/O ライブラリを使用しています。C と C++ の I/O ライブラリは大きく異なるため、これによりタイミング情報が完全に台無しになります。IOstream は、速度を考慮して設計されていない単純なものです。

さらに、I/O は完全に時間調整できません。I/O データのソースがたまたま 1 回だけ遅くなった場合、たとえば、OS がある実行ではたまたまそれをキャッシュに持っていたが、別の実行ではキャッシュに持っていなかった場合、1 つのプログラムはソート時間に関係なく遅く見えるでしょう。 .

たとえば、既存の をソートするのにかかる時間を純粋に計る必要がありstd::vector<std::string>ます。

そうそう、あなたgetlはメモリリークでいっぱいです。

于 2012-08-16T11:26:11.257 に答える
7

私の推測では、ソート速度ではなく、メモリの再割り当てを測定していると思います。一度に 1 つの要素を実行する代わりにa.push_back、C プログラムで行ったように、事前にベクトル メモリを割り当ててみてください。

a.reserve(num_lines);

1.5コンパイラが(VC++) または2(g++)の拡張係数で再割り当てを使用するかどうかに応じて、ファイル内の行を使用し2917再割り当てを行い140,190ます (つまりlog(total lines) / log(expansion factor))。

R. Martinho Fernandes によるコメントも的を射ています。タイミングの違いを得るには、両方のプログラムでステートメントを使用std::chrono::high_resolution_clock::now()します。sortこれにより、メモリと IO の違いから分離されます。

于 2012-08-16T11:14:06.330 に答える
5

他の人は、あなたが測定しているもののほとんどが I/O ライブラリの速度であると指摘しています。ただし、ここで述べたいくつかの記述とは対照的に、C++ iostream は C を使用する I/O と完全に競合する可能性があることに注意してくださいFILE *。したがって、あなたが測定しているのは、「gcc の iostream はどれくらいひどいものですか?」ということであり、「一般的な iostream はどれくらいひどいものですか?」ということではありません。

たとえば、1 つのディレクトリにあるすべての .txt ファイルを連結して、かなり大きなテキスト ファイルを作成することから始めました。次に、C コードを VC++ 10 でコンパイルし、それを使用してそのファイルを並べ替えました (出力を別のファイルに書き込みました)。3.2秒かかりました。

また、同じタスクを実行するために、かなり慣用的な C++ と考えるものも書きました。

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <iterator>

class line { 
    std::string data;
public:
    friend std::istream &operator>>(std::istream &is, line &l) { 
        return std::getline(is, l.data);
    }
    operator std::string() const { return data; }
};

int main() { 
    std::vector<std::string> vec(
        (std::istream_iterator<line>(std::cin)),
        std::istream_iterator<line>());

    std::sort(vec.begin(), vec.end());
    std::copy(vec.begin(), vec.end(), 
        std::ostream_iterator<std::string>(std::cout, "\n"));

    return 0;
}

これはあなたの C++ コードとほぼ同じ長さですが、(私が主張するように) どちらのプログラムよりも単純です。あなたが実際に気にかけていれば、それをもう少し短くするのはかなり簡単でしょう. 最適化を特に試みることはまったくありません。VC++ 10 (C コードで使用したものと同じ最適化フラグを使用-O2b2 -GL) でコンパイルすると、2.8 秒で実行され、C コードよりも約 10% 高速になります。

これを Linux で実行すると、C コードよりも遅いことがわかると思います。2 つの呼び出しを追加するsync_with_stdio(false);と、C++ コードで行ったように、おそらく修正されます。呼び出しはsync_with_stdio(false);一般に Linux ではかなり大きな違いをもたらしますが、Windows でそれらを使用した場合の改善を測定することはできませんでした (最近試した VC++ と MinGW、そしてずっと前の Intel、Comeau 、CygWin、および Borland も同様)。

于 2012-08-16T15:21:39.330 に答える
2

I/O の問題はさておき (これらが本物ではないというわけではありません)、2 つの種類は 2 つの異なることを行います。C++ バージョンはstringオブジェクトを移動し、C バージョンはポインターを移動します。後者は通常 (以下を参照) 高速です。オブジェクトの代わりにポインターを移動するように C++ コードを書き直します。つまり、 a を使用し、各オブジェクトstd::vector<std::string*>を明示的に new します。stringはい、慣用的ではありませんが、より公正な速度比較です。

std::sortが move-aware である場合、文字列の移動ははるかに高速になり、それほど影響をstd::vector<std::string>受けません。ただし、移動セマンティクスは C++11 で新しく追加されたものであり、C++11 コンパイラを使用している場合でも、移動セマンティクスが に移行されていない可能性がありますstd::sort

于 2012-08-16T14:15:00.800 に答える