繁体   English   中英

与以null终止的字符串相比,std :: string的效率如何?

[英]How efficient is std::string compared to null-terminated strings?

我发现std::string s与老式的以null终止的字符串相比非常慢,它是如此之慢,以至于它们将我的整个程序的速度降低了2倍。

我以为STL会慢一些,但我没有意识到它会这么慢。

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

我真正的问题是:为什么人们现在不再使用引用计数实现,这是否意味着我们所有人都需要更加谨慎地避免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;
}

关于字符串和其他容器的性能,肯定存在一些已知问题。 其中大多数与临时文件和不必要的副本有关。

正确使用它并不难,但是做错它也很容易。 例如,如果您在不需要可修改参数的地方看到代码按值接受字符串,则您做错了:

// 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的接口被设计为写时复制友好的。 但是,随着线程变得越来越流行,写字符串上的透明副本在保持其状态一致方面存在问题。 最近的实现倾向于避免在写字符串上进行复制,而是在适当的地方应用其他技巧。

但是,大多数问题将在下一版标准中解决。 例如,您可以使用emplace_back代替push_back直接在向量中创建Foo

v.emplace_back(a, b);

并且,除了在上面的级联中创建副本之外, std::string会识别何时将临时级联并针对这些情况进行优化。 重新分配也将避免制作副本,但会将元素移动到适合其新位置的位置。

要获得出色的阅读,请考虑Andrei Alexandrescu的Move Constructors

但是,有时比较也往往是不公平的。 标准容器必须支持其必须支持的功能。 例如,如果您的容器在添加/删除地图元素时没有使地图元素引用保持有效,则将“更快”的地图与标准地图进行比较可能会变得不公平,因为标准地图必须确保元素保持有效。 当然,这只是一个例子,在声明“我的容器比标准容器快!!”时,有许多情况需要牢记。

好像您在粘贴的代码中误用了char *。 如果你有

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

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

std :: string赋值操作分配足够的内存以将b的内容保存在a中,然后一个一个地复制每个字符。 对于char *,它不进行任何内存分配,也不逐个复制单个字符,只是说“ a现在指向b所指向的相同内存”。

我的猜测是这就是为什么std :: string变慢的原因,因为它实际上是在复制字符串,这似乎是您想要的。 要对char *进行复制操作,您需要使用strcpy()函数复制到已经适当大小的缓冲区中。 然后,您将进行准确的比较。 但是出于程序目的,您几乎应该绝对使用std :: string代替。

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

  • 如果在没有编译器优化的情况下进行基准测试(尤其是函数内联),则类将丢失。 它们不是内置的,甚至不是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 <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 ++。

如果有迹象表明向量的最终大小,则可以在填充向量之前调用reserve()来防止大小过多。

优化的主要规则:

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

您确定已证明确实是STL慢,而不是算法慢吗?

使用STL并非总是容易获得良好的性能,但通常来说,它旨在为您提供强大的功能。 我发现Scott Meyers的“有效STL”让您大开眼界,以了解如何有效地处理STL。 读!

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

通常,针对您的特定需求而设计的任何类都将胜过针对一般情况设计的通用类。 但是,学习好好使用通用类并学习遵守80:20的规则,您将比别人自己滚动一切的效率更高。


std::string一个特定缺点是它不提供性能保证,这是有道理的。 正如蒂姆·库珀(Tim Cooper)所述,STL并未说明字符串分配是否创建了深拷贝。 这对于通用类来说是很好的,因为引用计数可能会成为高度并发应用程序中的真正杀手,尽管它通常是单线程应用程序的最佳方法。

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有2种实现:引用计数或深度复制。 引用计数会在多线程程序中引入性能问题,即使只是读取字符串也是如此,而深度复制显然较慢,如上所示。 请参阅: 为什么不对VC ++字符串进行引用计数?

如该表所示,“字符串”在某些方面优于“ char *”,而在其他方面则较差,并且“ const string&”的属性与“ char *”相似。 我个人将在许多地方继续使用'char *'。 无声地复制std :: string的大量复制,使用隐式复制构造函数和临时副本,使我对std :: string有点矛盾。

他们没有错。 一般来说,STL实施要比您的实施好。

我确信您可以针对特定情况编写更好的东西,但是系数2太大了……您确实必须做错了什么。

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

如果您在使用STL时遇到性能问题,则可能是您做错了什么。

此外,STL实现不是跨编译器的标准配置。 我知道SGI的STL和STLPort通常表现良好。

话虽如此,但我很认真地说,您可能是C ++天才,并且设计的代码比STL更复杂。 不太可能,但是谁知道,您可能是C ++的勒布朗·詹姆斯。

我想说STL实现比传统实现更好。 您还尝试过使用列表而不是向量吗,因为向量对于某些目的是有效的,而列表对于其他目的是有效的

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

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

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

但是,世界上的程序员太无知或懒惰,无法在任何地方插入锁。 例如,如果多线程程序中的工作线程需要读取std :: string命令行参数,则即使只是读取字符串也需要锁定,否则可能导致崩溃。 (2个线程在不同的CPU上同时增加参考计数(+1),但分别减少(-2),因此参考计数降至零,并释放了内存。)

因此,实现者放弃了引用计数,而是让每个std :: string始终拥有自己的字符串副本。 更多的程序可以运行,但是速度都较慢。

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

我在一个主要程序上测试了std :: string效率低下的程度,该程序与以null终止的字符串相比,整体速度降低了约100%。 我还使用以下代码测试了原始std :: string分配,该代码表示​​std :: string分配的速度慢了100-900倍。 (我无法测量char *分配的速度)。 我还调试了std :: string运算符=()函数-在击中“ 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));

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM