std :: string与以null结尾的字符串相比有多高效?

时间:2009-03-12 08:09:11

标签: c++ stl performance

我发现std::string与旧式的以null结尾的字符串相比速度非常慢,速度非常慢,以至于它们将整个程序的速度降低了2倍。

我预计STL会变慢,我没有意识到它会慢得多。

我正在使用Visual Studio 2008,发布模式。它显示字符串的赋值比char*赋值慢100-1000倍(测试char*赋值的运行时非常困难)。我知道这不是一个公平的比较,指针分配与字符串复制,但我的程序有很多字符串分配,我不确定我可以在所有地方使用“ const引用”技巧。通过引用计数实现,我的程序可以很好,但这些实现似乎不再存在。

我真正的问题是:为什么人们不再使用引用计数实现,这是否意味着我们都需要更加谨慎地避免std :: string的常见性能缺陷?

我的完整代码如下。

#include <string>
#include <iostream>
#include <time.h>

using std::cout;

void stop()
{
}

int main(int argc, char* argv[])
{
    #define LIMIT 100000000
    clock_t start;
    std::string foo1 = "Hello there buddy";
    std::string foo2 = "Hello there buddy, yeah you too";
    std::string f;
    start = clock();
    for (int i=0; i < LIMIT; i++) {
        stop();
        f = foo1;
        foo1 = foo2;
        foo2 = f;
    }
    double stl = double(clock() - start) / CLOCKS\_PER\_SEC;

    start = clock();
    for (int i=0; i < LIMIT; i++) {
        stop();
    }
    double emptyLoop = double(clock() - start) / CLOCKS_PER_SEC;

    char* goo1 = "Hello there buddy";
    char* goo2 = "Hello there buddy, yeah you too";
    char *g;
    start = clock();
    for (int i=0; i < LIMIT; i++) {
        stop();
        g = goo1;
        goo1 = goo2;
        goo2 = g;
    }
    double charLoop = double(clock() - start) / CLOCKS_PER_SEC;
    cout << "Empty loop = " << emptyLoop << "\n";
    cout << "char* loop = " << charLoop << "\n";
    cout << "std::string = " << stl << "\n";
    cout << "slowdown = " << (stl - emptyLoop) / (charLoop - emptyLoop) << "\n";
    std::string wait;
    std::cin >> wait;
    return 0;
}

14 个答案:

答案 0 :(得分:38)

关于琴弦和其他容器的性能肯定存在已知问题。他们中的大多数都与临时和不必要的副本有关。

使用它并不太难,但它也很容易做错。例如,如果您看到代码在不需要可修改参数的情况下按值接受字符串,那么您就错了:

// you do it wrong
void setMember(string a) {
    this->a = a; // better: swap(this->a, a);
}

你最好通过const引用或内部进行交换操作,而不是另一个副本。在这种情况下,向量或列表的性能损失会增加。但是,你肯定知道存在已知的问题。例如:

// let's add a Foo into the vector
v.push_back(Foo(a, b));

我们正在创建一个临时Foo,只是为了向我们的向量添加新的Foo。在手动解决方案中,可能会直接在向量中创建Foo。如果向量达到其容量限制,则必须为其元素重新分配更大的内存缓冲区。它有什么作用?它使用复制构造函数将每个元素分别复制到新位置。如果手动解决方案之前知道元素的类型,则它可能表现得更加智能。

另一个常见问题是临时引入。看看这个

string a = b + c + e;

创建了大量临时工具,您可以在实际优化到性能的自定义解决方案中避免这些临时工作。那时候,std::string的界面被设计为写时复制友好。但是,随着线程变得越来越流行,写入字符串上的透明副本在保持其状态一致性方面存在问最近的实现倾向于避免复制写入字符串,而是在适当的地方应用其他技巧。

然而,对于下一版本的标准,大多数问题都得到了解决。例如,您可以使用push_back直接在向量中创建emplace_back来代替Foo

v.emplace_back(a, b);

而不是在上面的串联中创建副本,std::string将识别何时连接临时值并针对这些情况进行优化。重新分配也将避免复制,但会将元素移动到适当的位置。

要获得精彩的阅读,请考虑Andrei Alexandrescu的Move Constructors

然而,有时,比较也往往是不公平的。标准容器必须支持它们必须支持的功能。例如,如果您的容器在添加/删除地图中的元素时不保持地图元素引用有效,那么将“更快”的地图与标准地图进行比较可能会变得不公平,因为标准地图必须确保元素保持有效。当然,这仅仅是一个例子,当说“我的容器比标准容器更快!!!”时,你必须记住许多这样的情况。

答案 1 :(得分:11)

看起来你在你粘贴的代码中误用了char *。如果你有

std::string a = "this is a";
std::string b = "this is b"
a = b;

您正在执行字符串复制操作。如果对char *执行相同操作,则执行指针复制操作。

std :: string赋值操作分配足够的内存来保存a中的b的内容,然后逐个复制每个字符。在char *的情况下,它不进行任何内存分配或逐个复制单个字符,它只是说“a now指向b指向的相同内存。”

我的猜测是这就是为什么std :: string比较慢,因为它实际上是在复制字符串,这似乎是你想要的。要对char *执行复制操作,您需要使用strcpy()函数将其复制到已经适当大小的缓冲区中。那么你将有一个准确的比较。但是出于程序的目的,你几乎肯定会使用std :: string。

答案 2 :(得分:7)

使用任何实用程序类(无论是STL还是您自己的)编写C ++代码时,而不是使用例如。好的旧C null终止字符串,你需要记住一些事情。

  • 如果您在没有编译器优化的情况下进行基准测试(尤其是函数内联),则类将丢失。它们不是内置的,甚至是stl。它们是根据方法调用实现的。

  • 不要创建不必要的对象。

  • 如果可能,请勿复制对象。

  • 如果可能,将对象作为参考传递,而不是副本

  • 使用更专业的方法和功能以及更高级别的算法。例如:

    std::string a = "String a"
    std::string b = "String b"
    
    // Use
    a.swap(b);
    
    // Instead of
    std::string tmp = a;
    a = b;
    b = tmp;
    

最后一点。当您的C类C ++代码开始变得更加复杂时,您需要实现更高级的数据结构,例如自动扩展数组,字典,高效优先级队列。突然之间你意识到它的工作很多而且你的课程并不比stl那么快。更多的马车。

答案 3 :(得分:5)

你肯定做错了什么,或者至少没有比较STL和你自己的代码之间的“公平”。当然,如果没有代码可以更加具体,那就很难了解。

可能是您使用STL构造代码的方式导致更多的构造函数运行,或者不以与自己实现操作时所做的相匹配的方式重用分配的对象,等等

答案 4 :(得分:5)

此测试测试两个根本不同的东西:浅拷贝和深拷贝。理解差异以及如何避免C ++中的深层副本是至关重要的,因为默认情况下,C ++对象为其实例提供了值语义(与普通旧数据类型的情况一样),这意味着将它们分配给另一个通常会复制。

我“纠正”了你的考试并得到了这个:

char* loop = 19.921
string = 0.375
slowdown = 0.0188244

显然我们应该停止使用C风格的字符串,因为它们太慢了!实际上,我故意通过测试字符串侧的浅拷贝和strcpy来测试我的测试是否有缺陷:

#include <string>
#include <iostream>
#include <ctime>

using namespace std;

#define LIMIT 100000000

char* make_string(const char* src)
{
    return strcpy((char*)malloc(strlen(src)+1), src);
}

int main(int argc, char* argv[])
{
    clock_t start;
    string foo1 = "Hello there buddy";
    string foo2 = "Hello there buddy, yeah you too";
    start = clock();
    for (int i=0; i < LIMIT; i++)
        foo1.swap(foo2);
    double stl = double(clock() - start) / CLOCKS_PER_SEC;

    char* goo1 = make_string("Hello there buddy");
    char* goo2 = make_string("Hello there buddy, yeah you too");
    char *g;
    start = clock();
    for (int i=0; i < LIMIT; i++) {
        g = make_string(goo1);
        free(goo1);
        goo1 = make_string(goo2);
        free(goo2);
        goo2 = g;
    }
    double charLoop = double(clock() - start) / CLOCKS_PER_SEC;
    cout << "char* loop = " << charLoop << "\n";
    cout << "string = " << stl << "\n";
    cout << "slowdown = " << stl / charLoop << "\n";
    string wait;
    cin >> wait;
}

重点是,这实际上是你最终问题的核心,你必须知道你在做什么代码。如果您使用C ++对象,则必须知道将一个对象分配给另一个将创建该对象的副本(除非禁用赋值,在这种情况下您将收到错误)。您还必须知道何时使用指向对象的引用,指针或智能指针,并且使用C ++ 11,您还应该理解移动和复制语义之间的区别。

  

我真正的问题是:为什么人们不使用引用计数   实现了,这是否意味着我们都需要做多   更加小心避免常见的性能陷阱   的std :: string?

人们确实使用引用计数实现。这是一个例子:

shared_ptr<string> ref_counted = make_shared<string>("test");
shared_ptr<string> shallow_copy = ref_counted; // no deep copies, just 
                                               // increase ref count

不同之处在于字符串不会在内部执行,因为对于那些不需要它的人来说效率低下。像写时复制这样的东西通常不会因为类似的原因而对字符串完成(加上它通常会使线程安全成为问题)。然而,如果我们希望这样做,我们就可以在这里进行所有构建块进行写操作:我们可以在没有任何深度复制的情况下交换字符串,我们可以为它们制作指针,引用或智能指针

要有效地使用C ++,你必须习惯这种涉及价值语义的思维方式。如果不这样做,您可能会享受到额外的安全性和便利性,但是代价很高(不必要的副本肯定是使编写得很差的C ++代码比C慢的重要部分)。毕竟,您的原始测试仍在处理指向字符串的指针,而不是char[]数组。如果您使用的是字符数组而不是指向它们的指针,那么您同样需要strcpy来交换它们。使用字符串,你甚至可以使用内置的交换方法来有效地完成你在测试中所做的事情,所以我的建议是花更多的时间学习C ++。

答案 5 :(得分:2)

如果您有关于矢量最终大小的指示,则可以在填写之前调用reserve()来防止过大的调整大小。

答案 6 :(得分:2)

优化的主要规则:

  • 规则1:不要这样做。
  • 规则2 :(仅限专家)不要这样做。

您确定已证明确实STL确实很慢,而不是您的算法

答案 7 :(得分:2)

STL的良好性能并非总是如此,但一般来说,它旨在为您提供动力。我发现 Scott Meyers' "Effective STL" 让人大开眼界,了解如何有效地处理STL。阅读!

正如其他人所说,您可能会遇到频繁的字符串深度副本,并将其与指针赋值/引用计数实现进行比较。

通常,任何针对您的特定需求而设计的课程都将击败为一般情况设计的通用课程。但是学会很好地使用通用课程,并学会遵守80:20的规则,并且你会比自己动手推出一切更有效率。


std::string的一个具体缺点是它没有提供性能保证,这是有道理的。正如Tim Cooper所提到的,STL没有说明字符串赋值是否会创建深层副本。这对于泛型类来说是,因为引用计数可以成为高度并发应用程序中的真正杀手,即使它通常是单线程应用程序的最佳方式。

答案 8 :(得分:0)

他们没有出错。 STL的实施通常比你的更好。

我确信你可以为一个非常特殊的情况写一些更好的东西,但是因子太多了......你真的必须做错事。

答案 9 :(得分:0)

如果使用正确,std :: string与char *一样有效,但具有额外的保护。

如果您遇到STL的性能问题,可能是您做错了。

此外,STL实现不是编译器的标准。我知道SGI的STL和STLPort表现都很好。

那就是说,而且我是完全认真的,你可能是一个C ++天才,并设计出比STL更复杂的代码。这不可能,但谁知道,你可能是C ++的勒布朗詹姆斯。

答案 10 :(得分:0)

我想说STL实现比传统实现更好。你也尝试使用列表而不是矢量,因为矢量对于某些目的是有效的,列表对于其他一些是有效的

答案 11 :(得分:0)

                        string  const string&   char*   Java string
---------------------------------------------------------------------------------------------------
Efficient               no **       yes         yes     yes
assignment                          

Thread-safe             yes         yes         yes     yes

memory management       yes         no          no      yes
done for you

**有两个std :: string实现:引用计数或深度复制。引用计数引入了多线程程序中的性能问题,偶数读取字符串甚至是深度复制,如上所示。看到:  Why VC++ Strings are not reference counted?

如下表所示,'string'在某些方面优于'char *',在其他方面更差,'const string&amp;'属性与'char *'相似。我个人会在很多地方继续使用'char *'。静态发生的std :: string的大量复制,使用隐式复制构造函数和临时函数使我对std :: string有些矛盾。

答案 12 :(得分:-1)

std::string 总是比C字符串慢。 C字符串只是一个线性内存数组。您不能仅仅作为数据结构获得更高效率。您使用的算法(如strcat()strcpy())通常等同于STL对应物。相对而言,类实例化和方法调用将明显慢于C字符串操作(如果实现使用虚拟,则更糟糕)。获得同等性能的唯一方法是编译器进行优化。

答案 13 :(得分:-5)

很大一部分原因可能是在STL的现代实现中不再使用引用计数。

这是故事(有人纠正我,如果我错了):在开始时,STL实现使用引用计数,并且速度快但不是线程安全的 - 实现者期望应用程序员在更高级别插入自己的锁定机制,为了使它们成为线程安全的,因为如果锁定是在2级进行的,那么这会使速度降低两倍。

然而,世界各地的程序员都无知或懒得到处插入锁。例如,如果多线程程序中的工作线程需要读取std :: string命令行参数,那么即使只是为了读取字符串也需要锁定,否则可能会发生崩溃。 (2个线程在不同的CPU(+1)上同时递增引用计数,但是单独递减它(-2),因此引用计数降为零,并释放内存。)

所以实现者抛弃了引用计数,而是每个std :: string总是拥有自己的字符串副本。更多的计划有效,但它们都很慢。

所以现在,即使将一个std :: string简单地分配给另一个,(或等效地,将std :: string作为参数传递给函数),也需要大约400个机器代码指令而不是它需要的2个分配一个char *,减速200次。

我在一个主程序上测试了std :: string效率低的程度,与空终止字符串相比,整体减速率约为100%。我还使用以下代码测试了原始std :: string赋值,该代码表示​​std :: string赋值速度慢100-900倍。 (我无法测量char *赋值的速度)。我还调试了std :: string operator =()函数 - 在达到'memcpy()'之前,我在堆栈深处,大约7层深。

我不确定是否有任何解决方案。也许如果你需要快速的程序,使用普通的旧C ++,如果你更关心自己的生产力,你应该使用Java。

#define LIMIT 800000000
clock_t start;
std::string foo1 = "Hello there buddy";
std::string foo2 = "Hello there buddy, yeah you too";
std::string f;

start = clock();
for (int i=0; i < LIMIT; i++) {
    stop();
    f    = foo1;
    foo1 = foo2;
    foo2 = f;
}
double stl = double(clock() - start) / CLOCKS_PER_SEC;

start = clock();
for (int i=0; i < LIMIT; i++) {
    stop();
}
double emptyLoop = double(clock() - start) / CLOCKS_PER_SEC;

char* goo1 = "Hello there buddy";
char* goo2 = "Hello there buddy, yeah you too";
char *g;

start = clock();
for (int i=0; i < LIMIT; i++) {
    stop();
    g = goo1;
    goo1 = goo2;
    goo2 = g;
}
double charLoop = double(clock() - start) / CLOCKS_PER_SEC;

TfcMessage("done", 'i', "Empty loop = %1.3f s\n"
                        "char* loop = %1.3f s\n"
                        "std::string loop = %1.3f s\n\n"
                        "slowdown = %f", 
                        emptyLoop, charLoop, stl, 
                        (stl - emptyLoop) / (charLoop - emptyLoop));