C-Style字符串与库字符串性能

时间:2012-06-22 23:03:39

标签: c++

问题

C样式的字符串操作平均执行速度比库string类操作慢5倍,C ++ Primer,4th Edition会让我相信吗?

为什么要问?

因为当我实际进行性能测试时,事实证明,对于特定示例(本书中使用的一个),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时),我的结果是第一个块比第二个块执行快50%,这导致结论 C风格的字符串比这个特定的例子的库字符串类更快

然而,我打赌我遗漏了一些东西(可能很明显),因为书中(第139页)中有关上述代码的内容:

  

实际上,平均而言,字符串类实现会执行   比C风格的字符串函数快得多。亲戚   我们超过五年的PC的平均执行时间是   如下:

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

那是哪一个?我应该使用更长的字符串文字吗?也许是因为他们使用了GNU C编译器并使用了微软的编译器?是因为我有一台更快的电脑吗?

或者这本书错了吗?

修改

Microsoft(R)32位C / C ++优化编译器版本16.00.40219.01 for 80x86

4 个答案:

答案 0 :(得分: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:\dev\test]
> g++ foo.cpp doNothing.cpp -O2

[d:\dev\test]
> a
C strings: 0.417 seconds.
C++ strings: 0.084 seconds.

[d:\dev\test]
> a
C strings: 0.398 seconds.
C++ strings: 0.082 seconds.

[d:\dev\test]
> a
C strings: 0.4 seconds.
C++ strings: 0.083 seconds.

[d:\dev\test]
> _

上述说法,C ++字符串通常不是字符串最快的实现方式。

通常,不可变字符串(引用计数)可以很好地超过C ++字符串,而且,当我了解到这一点时,一个简单地复制字符串数据的字符串实现更快,当它使用适当的快速自定义时分配器。但是,不要问我如何实施后者。我只在另一个论坛上看到了代码和测试结果,在我与STL的讨论中指出了不可变字符串的一般优越性之后,有人慷慨地提供了这个,并且存在一些分歧。 ; - )

答案 1 :(得分:7)

首先:这个问题没有明确的答案。

原因是性能取决于库实现,编译器和您使用的选项,您使用的操作系统以及您使用的CPU架构。

这本书有些陈旧(2005年,硬件和软件已经发展),它的代码已在旧编译器,旧实现和旧硬件上进行了测试。无论它对性能的评价是基于它的作者的观察结果,不同的人在尝试使用不同的编译器,库和硬件组合的代码之间肯定会有所不同。

你能做的最好的就是尝试自己。像这样的简单“基准”并不能说明C风格的字符串与真实世界中的std::string之间的性能,常见情况,除非它们提供了尽可能多的方法的广泛报道。测试并比较性能 - 这本身就是一个很大的项目。

请注意,编译器优化可能会欺骗您使用本书中所示的代码。例如,由于空if - 块,整个if - 语句及其中的表达式(在本例中为例如对strcpy的调用)可以被删除(*)。使用本书中给出的代码块可以非常很难做出有意义的,真实的适用基准测试。

另请注意,无论这些微基准测试的结果如何,仅适用于他们基准测试的操作 - 换句话说 - 仅仅因为字符串分配,复制和比较似乎是 x 使用std::string或C风格的字符串加快时间并不意味着另一个 x 的速度通常比另一个快一倍!

*:使用带有-Ofast的GCC 4.7.1测试了C风格的字符串代码,并且在编译的可执行文件中没有引用strcmp,这表明字符串比较在代码 - 确实 - 因为if - 块是空的,所以没有理由在第一个地方有整个if

添加我自己的观察结果:我将两段代码分解为不同的函数,然后对其中一个进行了100次重复调用(带有for - 循环),然后使用{{来测量运行时间1}} unix-utility。使用GCC 4.7.1和time编译。

对C-Style字符串函数的100次调用大约需要7.05秒(3次运行,7到7.1秒之间的变化),而对std :: string版本的100次调用仅需 大约1.4秒平均超过3次!实际上,这表明std :: string远远胜过C风格的字符串。

答案 2 :(得分: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 ++界面如此简化和简单,很容易忽视实际上在引擎盖下。实际情况是,如果两个代码序列都以与语言相同的方式编写,那么性能也应该大致相同。

testCStr()的版本功能上等同原始 testCppStr(),可能写成如下:

#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);
}

答案 3 :(得分:1)

同意@zxcdw说的话,我想补充一下:

std::string应该(显着)慢于C风格的字符串,这没有内在的原因。

std::string实际上可以做更多的工作,因为它可以检查每个元素访问的边界(你应该能够通过编译时选项关闭),等等。它实际上可以少做工作,因为它知道字符串的长度(因此结束),并且当你追加某些东西时不必搜索它。 所以你永远不知道(没有测量)。

另一方面,了解更多意味着更大的内存占用(两个指针而不是一个),当你在短时间内处理大量不同的字符串对象时,这也会影响性能(由于更多的缓存未命中) 。但我不认为这就是你的情况。