2

问题

C 风格的字符串操作是否真的比库string类操作慢 5 倍,正如 C++ Primer,第 4 版让我相信的那样?

为什么这么问?

因为当我实际进行性能测试时,事实证明对于特定示例(书中使用的示例),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 用于 80x86

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::string中的 s之间的性能,常见情况,除非它们提供尽可能多的方法来测试和比较性能的广泛覆盖——这将是一个相当大的项目本身。

请注意,编译器优化可能会使用本书中显示的代码来欺骗您。例如,由于空的if-blocks,整个if-statement 和其中的表达式(在这种情况下例如调用 strcpy)可以被删除(*)。使用书中给出的代码块来做有意义的、现实世界适用的基准测试是非常困难的。

另请注意,无论这些微基准测试的结果如何,仅适用于它们进行基准测试的操作 - 换句话说 - 只是因为字符串分配、复制和比较似乎使用任一或 C 样式字符串快x倍,std::string不代表对方一般比对方快x倍!

*:使用 GCC 4.7.1 测试了 C 风格的字符串代码,-Ofast并且在编译的可执行文件中没有引用strcmp,这表明字符串比较在代码中被消除为不必要的 - 它确实- 因为if-block 是空的所以一开始就没有理由把整个都放在if那里!

添加我自己的观察:我将两段代码分解为不同的函数,然后对其中一个进行 100 次重复调用(使用for-loop),然后使用timeunix-utility 测量运行时间。使用 GCC 4.7.1 和-Ofast.

100 次调用 C-Style 字符串函数大约需要 7.05 秒(3 次运行,变化在 7 和 7.1 秒之间),而 100 次调用 std::string 版本平均只需要大约 1.4 秒超过 3 次运行!事实上,这表明 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实际上可能会做更多的工作,因为它可能会检查每个元素访问的边界(您应该能够使用编译时选项将其关闭)等等。实际上它可能做的工作更少,因为它知道长度(因此是字符串的结尾),并且在附加内容时不必搜索它。所以你永远不知道(没有测量)。

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

于 2012-06-22T23:41:05.767 回答