我发现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.
然而,有时候,比较也往往是不公平的.标准容器必须支持它们必须支持的功能.例如,如果您的容器在添加/删除地图中的元素时不保持地图元素引用有效,那么将"更快"的地图与标准地图进行比较可能会变得不公平,因为标准地图必须确保元素保持有效.当然,这仅仅是一个例子,当说"我的容器比标准容器更快!!!"时,你必须记住许多这样的情况.
关于琴弦和其他容器的性能肯定存在已知的问题.他们中的大多数都与临时和不必要的副本有关.
使用它并不太难,但它也很容易做错.例如,如果您看到代码在不需要可修改参数的情况下按值接受字符串,那么您就错了:
// 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.
然而,有时候,比较也往往是不公平的.标准容器必须支持它们必须支持的功能.例如,如果您的容器在添加/删除地图中的元素时不保持地图元素引用有效,那么将"更快"的地图与标准地图进行比较可能会变得不公平,因为标准地图必须确保元素保持有效.当然,这仅仅是一个例子,当说"我的容器比标准容器更快!!!"时,你必须记住许多这样的情况.
看起来你在你粘贴的代码中误用了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.
使用任何实用程序类(无论是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那么快.只是更多的马车.
你肯定做错了什么,或者至少没有比较STL和你自己的代码之间的"公平".当然,没有代码可以更加具体.
可能是您使用STL构造代码的方式导致更多的构造函数运行,或者不以与自己实现操作时所做的相匹配的方式重用分配的对象,等等.
这个测试测试两个根本不同的东西:浅拷贝和深拷贝.理解差异以及如何避免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_ptrref_counted = make_shared ("test"); shared_ptr shallow_copy = ref_counted; // no deep copies, just // increase ref count
不同之处在于字符串不会在内部执行,因为对于那些不需要它的人来说效率低下.像写时复制这样的东西通常不会因为类似的原因而对字符串完成(加上它通常会使线程安全成为问题).然而,如果我们希望这样做,我们就可以在这里进行所有构建块进行写操作:我们可以在没有任何深度复制的情况下交换字符串,我们可以为它们制作指针,引用或智能指针.
要有效地使用C++,您必须习惯这种涉及值语义的思维方式.如果不这样做,您可能会享受到额外的安全性和便利性,但是代价很高(不必要的副本肯定是使编写得很差的C++代码比C慢的重要部分).毕竟,你的原始测试仍在处理指向字符串的指针,而不是char[]
数组.如果您使用的是字符数组而不是指向它们的指针,那么您同样需要strcpy
交换它们.使用字符串,您甚至可以使用内置的交换方法来有效地完成测试中的操作,因此我的建议是花更多时间学习C++.