当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《std::string与以null结尾的字符串相比有多高效?》经验,为你挑选了5个好方法。

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

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

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

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

我的完整代码如下.

#include 
#include 
#include 

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

Johannes Sch.. 39

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

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

// 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将识别何时连接临时并优化这些情况.重新分配也将避免复制,但会将元素移动到适当的位置.

如需精彩阅读,请考虑安德烈亚历山大夫斯库的Move Constructors.

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



1> Johannes Sch..:

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

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

// 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将识别何时连接临时并优化这些情况.重新分配也将避免复制,但会将元素移动到适当的位置.

如需精彩阅读,请考虑安德烈亚历山大夫斯库的Move Constructors.

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



2> Dan Olson..:

看起来你在你粘贴的代码中误用了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.


你说的一切都是正确的,但问题的关键是"有效地使用std :: string,你经常需要使用指针/引用它们而不是值本身,在这种情况下你不得不担心生命周期价值观,但摆脱这些担忧,通常被吹捧为std :: string的主要优势."

3> Tomek Szpako..:

使用任何实用程序类(无论是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那么快.只是更多的马车.



4> unwind..:

你肯定做错了什么,或者至少没有比较STL和你自己的代码之间的"公平".当然,没有代码可以更加具体.

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



5> stinky472..:

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

我"纠正"了你的测试,得到了这个:

char* loop = 19.921
string = 0.375
slowdown = 0.0188244

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

#include 
#include 
#include 

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 ref_counted = make_shared("test");
shared_ptr shallow_copy = ref_counted; // no deep copies, just 
                                               // increase ref count

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

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


@TimCooper但是当你已经在C风格的字符串中使用指针时,引用和指针是如何复杂的?如果你有一个结构Foo并且你不想深度复制它,你可以引用它或指向它.关于编译器何时复制的知识在C中同样普遍,我们只是没有那么多的工具来构建像std :: string那样复杂的类型.
@TimCooper引用计数和写时复制字符串仅对用户不了解如何避免深拷贝的天真用途有效.但与此同时,它使我们这些使用std :: string的人实际上需要副本并且意图*更慢*.它增加了我们这些不想要这些功能的人的开销.它实际上是通过试图做一些奇特的东西来避免复制,除非字符串被修改,否则它实际上会增加str1 = str2的开销.C++掌握在知识渊博的开发人员手中,无论如何都会避免这样的副本.
推荐阅读
wangtao
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有