web-dev-qa-db-ja.com

std :: string操作のパフォーマンスが低下するのはなぜですか?

サーバー側アプリケーションの言語を選択するために、いくつかの言語の文字列操作を比較するテストを行いました。最終的にC++を試してみるまで、結果は正常であるように見えました。だから私は最適化を逃したのではないかと思い、助けを求めてここに来ます。

テストは主に、連結や検索などの集中的な文字列操作です。テストは、GCCのバージョン4.6.1を搭載したUbuntu 11.10 AMD64で実行されます。マシンは、4G RAMとクアッドコアCPUを搭載したDell Optiplex 960です。

Python(2.7.2):

def test():
    x = ""
    limit = 102 * 1024
    while len(x) < limit:
        x += "X"
        if x.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) > 0:
            print("Oh my god, this is impossible!")
    print("x's length is : %d" % len(x))

test()

結果が得られます:

x's length is : 104448

real    0m8.799s
user    0m8.769s
sys     0m0.008s

Java(OpenJDK-7):

public class test {
    public static void main(String[] args) {
        int x = 0;
        int limit = 102 * 1024;
        String s="";
        for (; s.length() < limit;) {
            s += "X";
            if (s.indexOf("ABCDEFGHIJKLMNOPQRSTUVWXYZ") > 0)
            System.out.printf("Find!\n");
        }
        System.out.printf("x's length = %d\n", s.length());
    }
}

結果が得られます:

x's length = 104448

real    0m50.436s
user    0m50.431s
sys     0m0.488s

javaScript(Nodejs 0.6.3)

function test()
{
    var x = "";
    var limit = 102 * 1024;
    while (x.length < limit) {
        x += "X";
        if (x.indexOf("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) > 0)
            console.log("OK");
    }
    console.log("x's length = " + x.length);
}();

結果が得られます:

x's length = 104448

real    0m3.115s
user    0m3.084s
sys     0m0.048s

c ++(g ++ -Ofast)

NodejsのパフォーマンスがPythonまたはJavaよりも優れていることは驚くに値しません。しかし、libstdc ++はNodejsよりもはるかに優れたパフォーマンスを提供すると期待していました。その結果、本当に驚きました。

#include <iostream>
#include <string>
using namespace std;
void test()
{
    int x = 0;
    int limit = 102 * 1024;
    string s("");
    for (; s.size() < limit;) {
        s += "X";
        if (s.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) != string::npos)
            cout << "Find!" << endl;
    }
    cout << "x's length = " << s.size() << endl;
}

int main()
{
    test();
}

結果が得られます:

x length = 104448

real    0m5.905s
user    0m5.900s
sys     0m0.000s

簡単な要約

では、要約を見てみましょう。

  • nodejs(V8)のjavascript:3.1s
  • Python on CPython 2.7.2:8.8秒
  • Libstdc ++を使用したC++:5.9秒
  • OpenJDK 7上のJava:50.4s

意外と! C++で "-O2、-O3"を試しましたが、役に立ちませんでした。 C++は、V8でのjavascriptのパフォーマンスが約50%であり、CPythonよりも劣っているようです。 GCCでの最適化に失敗した場合、またはこれが本当かどうか、誰かに説明してもらえますか?大いに感謝する。

60
Wu Shu

_std::string_のパフォーマンスが悪い(C++が嫌いなほど)のではなく、文字列処理が他の言語向けに大幅に最適化されているということです。

文字列のパフォーマンスの比較は誤解を招くものであり、それらがそれ以上のものを表すことを意図している場合は大げさです。

Python文字列オブジェクトはCで完全に実装されている 、そして確かにPython 2.7、 多数最適化 は、Unicode文字列とバイトの間の分離がないために存在します。このテストをPython 3.xで実行すると、かなり遅くなることがわかります。

JavaScriptには、非常に最適化された多数の実装があります。ここでは文字列処理が優れていると予想されます。

あなたのJavaの結果は、不適切な文字列処理または他のいくつかの悪いケースが原因である可能性があります。Java専門家がこのテストを実行して修正することを期待しますいくつかの変更。

C++の例については、パフォーマンスがPythonバージョンをわずかに超えると予想されます。これは、インタープリターのオーバーヘッドが少ない同じ操作を実行します。これは結果に反映されます。テストの前にs.reserve(limit);は、再割り当てのオーバーヘッドを削除します。

言語の単一のファセットのみをテストしていることを繰り返します実装。このテストの結果は、全体的な言語速度を反映していません。

私はそのようなおしっこコンテストがいかに馬鹿げていることができるかを示すためにCバージョンを提供しました:

_#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>

void test()
{
    int limit = 102 * 1024;
    char s[limit];
    size_t size = 0;
    while (size < limit) {
        s[size++] = 'X';
        if (memmem(s, size, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 26)) {
            fprintf(stderr, "zomg\n");
            return;
        }
    }
    printf("x's length = %zu\n", size);
}

int main()
{
    test();
    return 0;
}
_

タイミング:

_matt@stanley:~/Desktop$ time ./smash 
x's length = 104448

real    0m0.681s
user    0m0.680s
sys     0m0.000s
_
71
Matt Joiner

そこで、ideone.orgでこれを試してみました。

ここでは、元のC++プログラムを少し変更したバージョンですが、ループ内の追加が削除されているため、std::string::find()への呼び出しのみが測定されます。 繰り返し回数を最大40%に削減する必要があったことに注意してください。そうしないと、ideone.orgがプロセスを強制終了します。

#include <iostream>
#include <string>

int main()
{
    const std::string::size_type limit = 42 * 1024;
    unsigned int found = 0;

    //std::string s;
    std::string s(limit, 'X');
    for (std::string::size_type i = 0; i < limit; ++i) {
        //s += 'X';
        if (s.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) != std::string::npos)
            ++found;
    }

    if(found > 0)
        std::cout << "Found " << found << " times!\n";
    std::cout << "x's length = " << s.size() << '\n';

    return 0;
}

ideone.org での私の結果はtime: 3.37s。 (もちろん、これは非常に疑わしいですが、しばらく私を甘やかし、他の結果を待ちます。)

次に、このコードを取得してコメント行を入れ替え、検索ではなく追加をテストします。 今回は、時間結果をまったく表示しないようにするために、反復回数を10倍に増やしたことに注意してください。

#include <iostream>
#include <string>

int main()
{
    const std::string::size_type limit = 1020 * 1024;
    unsigned int found = 0;

    std::string s;
    //std::string s(limit, 'X');
    for (std::string::size_type i = 0; i < limit; ++i) {
        s += 'X';
        //if (s.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) != std::string::npos)
        //    ++found;
    }

    if(found > 0)
        std::cout << "Found " << found << " times!\n";
    std::cout << "x's length = " << s.size() << '\n';

    return 0;
}

ideone.org での私の結果は、反復が10倍に増加したにもかかわらず、time: 0s

私の結論:このベンチマークは、C++では、検索操作、ループ内の文字は結果にまったく影響しません。それは本当にあなたの意図でしたか?

34
sbi

慣用的なC++ソリューションは次のようになります。

#include <iostream>
#include <string>
#include <algorithm>

int main()
{
    const int limit = 102 * 1024;
    std::string s;
    s.reserve(limit);

    const std::string pattern("ABCDEFGHIJKLMNOPQRSTUVWXYZ");

    for (int i = 0; i < limit; ++i) {
        s += 'X';
        if (std::search(s.begin(), s.end(), pattern.begin(), pattern.end()) != s.end())
            std::cout << "Omg Wtf found!";
    }
    std::cout << "X's length = " << s.size();
    return 0;
}

文字列をスタックに入れ、memmemを使用することで、これを大幅に高速化できますが、必要がないようです。私のマシンで実行すると、これはすでにpythonソリューションの速度の10倍を超えています。

[私のラップトップで]

時間./test Xの長さ= 104448実数0m2.055sユーザー0m2.049s sys 0m0.001s

14
Heptic

これが最も明白な方法です。メインループの前にs.reserve(limit);を実行してください。

ドキュメントは here です。

JavaまたはPythonで通常使用するのと同じ方法でC++の標準クラスを直接使用すると、サブパー机の後ろで何が行われているのか分からない場合のパフォーマンス言語自体には魔法のようなパフォーマンスはなく、適切なツールを提供するだけです。

8

ここで見逃しているのは、検索の本質的な複雑さです。

検索を_102 * 1024_(104 448)回実行しています。単純な検索アルゴリズムは、毎回、最初の文字からパターンを照合し、次に2番目の文字を照合します。

したがって、長さ_1_からNまでの文字列があり、各ステップでこの文字列に対してパターンを検索します。これはC++の線形演算です。それはN * (N+1) / 2 = 5 454 744 576の比較です。これほど時間がかかることは、あなたほど驚くことではありません...

単一のfindを検索するAのオーバーロードを使用して、仮説を検証してみましょう。

_Original: 6.94938e+06 ms
Char    : 2.10709e+06 ms
_

約3倍高速であるため、同じ程度の大きさです。したがって、完全な文字列を使用することはあまり面白くありません。

結論?たぶんfindは少し最適化できるでしょう。しかし、問題はそれだけの価値はありません。

注:ボイヤームーアを売り込む人には、針が小さすぎるので、あまり役に立たないと思います。1桁(26文字)カットできますが、それ以上はできません。

6
Matthieu M.

私の最初の考えは、問題はないということです。

C++は2番目に優れたパフォーマンスを提供し、Javaの10倍近く高速です。 Java以外のすべてがその機能で達成可能な最高のパフォーマンスに近い状態で実行されている可能性があり、Javaの問題(ヒント-StringBuilder )。

C++の場合、パフォーマンスを少し改善しようとすることがあります。特に...

  • _s += 'X';_ではなく_s += "X";_
  • ループの外でstring searchpattern ("ABCDEFGHIJKLMNOPQRSTUVWXYZ");を宣言し、これをfind呼び出しに渡します。 _std::string_インスタンスはそれ自体の長さを認識していますが、C文字列はそれを決定するために線形時間チェックを必要とし、これは_std::string::find_パフォーマンスに関連する場合とそうでない場合があります。
  • JavaでStringBuilderを使用する必要があるのと同じ理由で、_std::stringstream_を使用してみてください。ただし、stringへの変換を繰り返すと、さらに問題が発生する可能性があります。

全体として、結果はそれほど驚くべきことではありません。 JavaScriptは、優れたJITコンパイラーを備えているため、この場合にC++静的コンパイルが許可されているよりも少しだけ最適化できる可能性があります。

十分な作業を行うことで、常にJavaScriptよりもC++を最適化できるはずですが、それが自然に発生するのではなく、それを実現するためにかなりの知識と努力が必要になる場合が常にあります。

5
Steve314

C++の場合、「ABCDEFGHIJKLMNOPQRSTUVWXYZ」に_std::string_を使用してみてください。私の実装では、string::find(const charT* s, size_type pos = 0) constは文字列引数の長さを計算します。

4
dimba

C/C++言語は簡単ではなく、何年もかけて高速なプログラムを作成します。

strncmp(3)バージョンがcバージョンから変更された場合:

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>

void test()
{
    int limit = 102 * 1024;
    char s[limit];
    size_t size = 0;
    while (size < limit) {
        s[size++] = 'X';
        if (!strncmp(s, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 26)) {
            fprintf(stderr, "zomg\n");
            return;
        }
    }
    printf("x's length = %zu\n", size);
}

int main()
{
    test();
    return 0;
}
4
Gabrielix

私はC++の例を自分でテストしました。 std::sting::findの呼び出しを削除すると、プログラムはすぐに終了します。したがって、文字列連結中の割り当てはここでは問題ありません。

変数sdt::string abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"を追加し、std::string::findの呼び出しで "ABC ... XYZ"の出現箇所を置き換えると、プログラムは元の例とほぼ同じ時間で終了します。これは、割り当てと文字列の長さの計算がランタイムに多くを追加しないことを再び示しています。

したがって、libstdc ++で使用される文字列検索アルゴリズムは、javascriptまたはpythonの検索アルゴリズムほど高速ではないようです。目的に合った独自の文字列検索アルゴリズムを使用してC++を再試行したい場合があります。

3
swegi

テストコードは、過剰な文字列連結の病理学的シナリオをチェックしています。 (テストの文字列検索部分はおそらく省略された可能性があります。それは最終結果にほとんど貢献しないと思います。)過度の文字列連結は、ほとんどの言語が非常に強く警告し、非常によく知られた代替手段を提供する落とし穴です。 (つまり、StringBuilder)、ここで基本的にテストしているのは、これらの言語が完全に予期される失敗のシナリオでどれほどひどく失敗するかです。それは無意味です。

同様に無意味なテストの例は、タイトなループで例外をスローおよびキャッチするときのさまざまな言語のパフォーマンスを比較することです。すべての言語は、例外のスローとキャッチが非常に遅いことを警告しています。彼らはどれほど遅いかを指定しません、彼らは何も期待しないようにあなたに警告するだけです。したがって、先に進んでそれを正確にテストすることは、無意味です。

したがって、文字列の連結を回避するために、マインドレスな文字列連結部分(s + = "X")をこれらの言語のそれぞれが提供する構成で正確に置き換えてテストを繰り返すことは、はるかに理にかなっています。 (StringBuilderクラスなど)。

1
Mike Nakis

Nodejsでは検索部分文字列のアルゴリズムが優れているようです。あなたは自分自身を実装してそれを試すことができます。

1
xandox

Sbiで述べたように、テストケースは検索操作によって支配されます。 C++とJavascriptの間でテキスト割り当てがどれだけ速く比較されるのか知りたくてたまりませんでした。

システム:Raspberry Pi 2、g ++ 4.6.3、ノードv0.12.0、g ++ -std = c ++ 0x -O2 perf.cpp

C++:770ミリ秒

予約なしのC++:1196ミリ秒

Javascript:2310ms

C++

#include <iostream>
#include <string>
#include <chrono>
using namespace std;
using namespace std::chrono;

void test()
{
    high_resolution_clock::time_point t1 = high_resolution_clock::now();
    int x = 0;
    int limit = 1024 * 1024 * 100;
    string s("");
    s.reserve(1024 * 1024 * 101);
    for(int i=0; s.size()< limit; i++){
        s += "SUPER Nice TEST TEXT";
    }

    high_resolution_clock::time_point t2 = high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( t2 - t1 ).count();
    cout << duration << endl;
}

int main()
{
    test();
}

JavaScript

function test()
{
    var time = process.hrtime();
    var x = "";
    var limit = 1024 * 1024 * 100;
    for(var i=0; x.length < limit; i++){
        x += "SUPER Nice TEST TEXT";
    }

    var diff = process.hrtime(time);
    console.log('benchmark took %d ms', diff[0] * 1e3 + diff[1] / 1e6 );
}

test();
0
Pascalius