2

質問

stringC++ Primer 第 4 版で私が信じているように、C スタイルの文字列操作は、平均してライブラリ クラスの操作よりも 5 倍遅く実行されるというのは本当ですか?

何で質問する?

実際にパフォーマンス テストを行ったところ、C スタイルの文字列は特定の例 (本で使用されているもの) で約 50% 高速であることが判明したためです。


設定

私はC++ Primer, 4th Editionを読んでいます.138ページにこのコードがリストされています:

//  C-style character string implementation
const char *pc = "a very long literal string";
const size_t  len = strlen(pc +1);    //  space to allocate

//  performance test on string allocation and copy
for (size_t ix = 0; ix != 1000000; ++ix) {
    char *pc2 = new char[len + 1];  //  allocate the space
    strcpy(pc2, pc);                //  do the copy
    if (strcmp(pc2, pc))            //  use the new string
        ;    //  do nothing
    delete [] pc2;                  //  free the memory
}

//  string implementation
string str("a very long literal string");

//  performance test on string allocation and copy
for(int ix = 0; ix != 1000000; ++ix) {
    string str2 = str;  //  do the copy, automatically allocated
    if (str != str2)    //  use the new string
        ;   //  do nothing
}    //  str2 is automatically freed

ここで、2行目でそれを認識していることstrlen(pc +1)、および最初は配列forを使用size_tしていますが添字を付けていないため、そうであった可能性があることを覚えておいてintください。

このコードをテストすると (strlen(pc) + 1これは意図したものと思われます)、最初のブロックは 2 番目のブロックよりも約50% 高速に実行されるという結果になりました。これは、 C スタイルの文字列がこの特定のライブラリ文字列クラスよりも高速であるという結論につながります。

ただし、上記のコードに関連する本 (139 ページ) に書かれていることから、何かが欠けていると思います (おそらく明らかです)。

たまたま、平均して、文字列クラスの実装は C スタイルの文字列関数よりもかなり高速に実行されます。5 年以上前の PC での相対的な平均実行時間は次のとおりです。

 user    0.47  # string class 
 user    2.55  # C-style character string

それで、それはどれですか?より長い文字列リテラルを使用する必要がありましたか? 彼らは GNU C Compiler を使用し、私は Microsoft のコンパイラを使用したためでしょうか? より高速なコンピューターを使用しているからでしょうか。

それとも、この本は間違っていますか?

編集

80x86 用 Microsoft (R) 32 ビット C/C++ 最適化コンパイラ バージョン 16.00.40219.01

4

4 に答える 4

10

コンパイラとマシンを使用したこの例では、C スタイルの文字列の方が高速であるという結論は、ほぼ確実です。

  • 最適化をオンにするのを忘れて、
  • strlen呼び出しを最適化しないようにするために、文字列の長さをコンパイラに「不明」にするのを忘れました (これは注意が必要です)。
  • 安全範囲チェック(該当する場合)を忘れてオフにすると、速度が低下しstd::stringます。

テストしたコードは次のとおりです。

#include <assert.h>
#include <iostream>
#include <time.h>
#include <string>
#include <string.h>
using namespace std;

extern void doNothing( char const* );

class StopWatch
{
private:
    clock_t     start_;
    clock_t     end_;
    bool        isRunning_;
public:
    void start()
    {
        assert( !isRunning_ );
        start_ = clock();
        end_ = 0;
        isRunning_ = true;
    }

    void stop()
    {
        if( isRunning_ )
        {
            end_ = clock();
            isRunning_ = false;
        }
    }

    double seconds() const
    {
        return double( end_ - start_ )/CLOCKS_PER_SEC;
    }

    StopWatch(): start_(), end_(), isRunning_() {}
};

inline void testCStr( int const argc, char const* const argv0 )
{
    //  C-style character string implementation
    //const char *pc = "a very long literal string";
    const char *pc = (argc == 10000? argv0 : "a very long literal string");
    //const size_t  len = strlen(pc +1);    //  space to allocate
    const size_t  len = strlen(pc)+1;    //  space to allocate

    //  performance test on string allocation and copy
    for (size_t ix = 0; ix != 1000000; ++ix) {
        char *pc2 = new char[len + 1];  //  allocate the space
        strcpy(pc2, pc);                //  do the copy
        if (strcmp(pc2, pc))            //  use the new string
            //;   //  do nothing
            doNothing( pc2 );
        delete [] pc2;                  //  free the memory
    }
}

inline void testCppStr( int const argc, char const* const argv0 )
{
    //  string implementation
    //string str("a very long literal string");
    string str( argc == 10000? argv0 : "a very long literal string" );

    //  performance test on string allocation and copy
    for(int ix = 0; ix != 1000000; ++ix) {
        string str2 = str;  //  do the copy, automatically allocated
        if (str != str2)    //  use the new string
            //;   //  do nothing
            doNothing( &str2[0] );
    }    //  str2 is automatically freed
}

int main( int argc, char* argv[] )
{
    StopWatch   timer;

    timer.start();  testCStr( argc, argv[0] );  timer.stop();
    cout << "C strings: " << timer.seconds() << " seconds." << endl;

    timer.start();  testCppStr( argc, argv[0] );  timer.stop();
    cout << "C++ strings: " << timer.seconds() << " seconds." << endl;
}

典型的な結果:

[d:\開発\テスト]
> g++ foo.cpp doNothing.cpp -O2

[d:\開発\テスト]
>
C ストリング: 0.417 秒。
C++ 文字列: 0.084 秒。

[d:\開発\テスト]
>
C ストリング: 0.398 秒。
C++ 文字列: 0.082 秒。

[d:\開発\テスト]
>
C弦:0.4秒。
C++ 文字列: 0.083 秒。

[d:\開発\テスト]
> _

とはいえ、C++ 文字列は、一般に可能な限り高速な文字列の実装ではありません。

一般に、不変の文字列 (参照カウント) は C++ の文字列よりもかなりの差があります。それを知ったときは驚きましたが、文字列データを単純にコピーする文字列の実装は、適切で高速なカスタム アロケーターを使用すると、さらに高速です。ただし、後者を実装する方法を私に尋ねないでください。別のフォーラムでコードとテスト結果を見ただけで、STL との議論で不変文字列の一般的な優位性を指摘した後、誰かが親切に提供してくれましたが、意見の相違がありました。;-)

于 2012-06-23T00:26:50.860 に答える
7

まず第一に、この質問に対する決定的な答えはありません。

その理由は、パフォーマンスがライブラリの実装、使用するコンパイラとオプション、使用するオペレーティング システム、および使用する CPU アーキテクチャに依存するためです。

この本はやや古く (2005 年、ハードウェアとソフトウェアは進化しています)、コードは古いコンパイラ、古い実装、古いハードウェアでテストされています。パフォーマンスについての記述は、作成者による観察に基づいており、さまざまなコンパイラ、ライブラリ、およびハードウェアの組み合わせでコードを試してみると、さまざまな人々によって確実に異なります。

あなたができる最善のことは、自分自身を試すことです。これらのような単純な「ベンチマーク」は、パフォーマンスをテストおよび比較するための可能な限り多くの可能な方法を広範囲にカバーしない限り、現実世界のC スタイルの文字列とstd::strings の間のパフォーマンスについて多くを語ることはありません。それ自体がかなり大きなプロジェクト。

コンパイラの最適化は、本に示されているようなコードであなたを欺く可能性があることに注意してください。たとえば、ブロックが空であるため、ifステートメント全体ifとその中の式 (この場合は strcpy の呼び出しなど) を削除できます (*)。この本に記載されているコード ブロックを使用して、意味のある現実世界に適用可能なベンチマークを実行することは非常に困難です。

また、これらのマイクロベンチマークの結果がどうであれ、それらがベンチマークする操作にのみ適用されることに注意してください。つまり、文字列の割り当て、コピー、および比較は、または C スタイルの文字列でx倍高速に見えるためです。一般に、他のものよりもx倍速いstd::stringという意味ではありません。

*: GCC 4.7.1 を使用して C スタイルの文字列コードをテストし、コンパイル済みの実行可能ファイルに-Ofastは参照がありません。これは、-block が空であるstrcmpため、文字列比較がコードで不要であるとして削除されたことを示唆しています。ですから、そもそもそこに全体を持っている理由さえありません!ifif

私自身の観察を追加するには: 2 つのコードを個別の関数に分割し、そのうちの 1 つを (ループを使用して) 100 回呼び出してから、 unix ユーティリティforで実行時間を測定しました。timeGCC 4.7.1 および-Ofast.

C スタイルの文字列関数への 100 回の呼び出しには約 7.05 秒かかりました (3 回の実行、7 ~ 7.1 秒の変動) が、std::string バージョンへの 100 回の呼び出しには3 回の実行で平均約 1.4 秒しかかかりませんでした! 実際、これは std::string が C スタイルの文字列よりもはるかに優れていることを示唆しています。

于 2012-06-22T23:27:06.943 に答える
4

これは公正な比較ではありません。std::stringはcopy on writeなどの手法を使用する場合があります。あなたのタイミングの結果を考えると、 str2はコピーをまったく作成していないと思います。むしろ、 strへの参照であり、割り当てとコピーを節約するだけでなく、比較もnopにする可能性があります。また、strcpy()の使用は、ターミネータをチェックする必要があるため、最適ではありません。より正直な比較のために、次の改訂をお勧めします。

#include <stdlib.h>
inline void testCStr(const int argc, const char* argv)
{
    const char* str = (argc == 10000) ? argv : "a very long literal string";
    size_t len = strlen(str);

    int i;
    for ( i = 0; i < 1000000; i++ )
     {
        char* dup = (char*) malloc(len + 1);
        memcpy(dup, str, len + 1);
        dup[0] = str[0]; /* keep things even. */
        if (strcmp(str, dup))
            doNothing(dup);
        free(dup);
     }
}

inline void testCppStr(const int argc, const char* argv)
{
    string str = (argc == 10000) ? argv : "a very long literal string";

    for ( int i = 0; i < 1000000; i++ )
     {
        string dup = str;
        dup[0] = str[0]; // force copy (defeats copy on write).
        if (str != dup)
           doNothing(dup.c_str());
     }
}

C++ インターフェースは非常に合理化されシンプルであるため、内部で実際に行われていることが見落とされがちです。実際には、両方のコード シーケンスが言語に関して同等の方法で記述されている場合、パフォーマンスもほぼ同等になるはずです。

元のtestCppStr( )と機能的に同等なバージョンのtestCStr()は、次のように記述できます。

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

/* utility. */
static int strcmpx(const char* x, size_t xsize, const char* y, size_t ysize)
{
    int cmp = memcmp(x, y, xsize);
    if (cmp != 0)
        return cmp;
    return -(xsize < ysize);
}

/* mystring. */
typedef struct mystring {
    long* refcnt;
    char* cstr;
    size_t size;
} mystring;

/* mystring private. */
static inline int mystring_unique(const mystring* x)
{   return *x->refcnt == 1;
}

static inline void mystring_add_ref(const mystring* x)
{   ++*x->refcnt;
}

static void mystring_del_ref(mystring* x)
{
    int unique = mystring_unique(x);
    --*x->refcnt;

    if (unique)
        free(x->refcnt);
}

static void mystring_make_unique(mystring* x, const char* str, size_t size)
{
    void* base = malloc(size + 1 + sizeof (long));
    x->refcnt = (long*) base;
    *x->refcnt = 1;

    x->cstr = (char*) base + sizeof (long);
    memcpy(x->cstr, str, size + 1);
    x->size = size;
}

/* mystring public. */
void mystring_construct(mystring* x, const char* str)
{   mystring_make_unique(x, str, strlen(str));
}

void mystring_construct_copy(mystring* x, const mystring* src)
{
    mystring_add_ref(src);
    *x = *src;
}

void mystring_destroy(mystring* x)
{   mystring_del_ref(x);
}

int mystring_cmp(const mystring* x, const mystring* y)
{   return strcmpx(x->cstr, x->size, y->cstr, y->size);
}

const char* mystring_cstr(const mystring* x)
{   return x->cstr;
}

const char* mystring_at_const(const mystring* x, long i)
{   return x->cstr + i;
}

char* mystring_at(mystring* x, long i)
{
    if (!mystring_unique(x))
     {
        mystring save = *x;
        mystring_make_unique(x, x->cstr, x->size);
        mystring_del_ref(&save);
     }
    return x->cstr + i;
}

/* test case. */
void testCStr(const int argc, const char* argv)
{
#define ITERATIONS 1000000
    const char* temp = (argc == 10000) ? argv : "a very long literal string";
    mystring str;
    mystring_construct(&str, temp);

    int i;
    for ( i = 0; i < ITERATIONS; i++ )
     {
        mystring dup;
        mystring_construct_copy(&dup, &str);
#ifdef FORCE_COPY
        *mystring_at(&dup, 0) = *mystring_at_const(&str, 0);
#endif
        if (mystring_cmp(&str, &dup))
            doNothing(mystring_cstr(&dup));
        mystring_destroy(&dup);
     }
    mystring_destroy(&str);
}
于 2013-07-11T14:57:15.717 に答える
1

@zxcdw の発言に同意したので、追加したいと思います。

std::stringライブラリが C スタイルの文字列よりも (大幅に) 遅くなる固有の理由はありません。

std::string各要素アクセスの境界をチェックする可能性があるため(コンパイル時のオプションでオフにできるはずです)、実際にはより多くの作業を行う可能性があります。したがって、文字列の末尾) であり、何かを追加するときにそれを検索する必要はありません。だからあなたは決して知りません(測定せずに)。

一方、より多くのことを知ることは、より大きなメモリ フットプリント (1 つではなく 2 つのポインター) を意味し、短時間で多くの異なる文字列オブジェクトを処理する場合、パフォーマンスに影響を与える可能性があります (キャッシュ ミスが増えるため)。しかし、これがあなたの場合に起こっていることだとは思いません。

于 2012-06-22T23:41:05.767 に答える