在C + +:为什么std::string操作不执行?

Why do std::string operations perform poorly?

为了选择服务器端应用程序的语言,我做了一个测试来比较几种语言中的字符串操作。直到我最终尝试C++,结果才是正常的,这让我很吃惊。所以我想知道我是否错过了任何优化,来这里寻求帮助。

测试主要是密集的字符串操作,包括连接和搜索。测试在Ubuntu 11.10 AMD64上进行,GCC版本为4.6.1。机器是Dell Optiplex 960,带有4G RAM和四核CPU。

在python(2.7.2)中:

1
2
3
4
5
6
7
8
9
10
def test():
    x =""
    limit = 102 * 1024
    while len(x) < limit:
        x +="X"
        if x.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) > 0:
            print("Oh my god, this is impossible!")
    print("x's length is : %d" % len(x))

test()

结果是:

1
2
3
4
5
x's length is : 104448

real    0m8.799s
user    0m8.769s
sys     0m0.008s

在Java(OpenJDK-7)中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class test {
    public static void main(String[] args) {
        int x = 0;
        int limit = 102 * 1024;
        String s="";
        for (; s.length() < limit;) {
            s +="X";
            if (s.indexOf("ABCDEFGHIJKLMNOPQRSTUVWXYZ") > 0)
            System.out.printf("Find!
"
);
        }
        System.out.printf("x's length = %d
"
, s.length());
    }
}

结果是:

1
2
3
4
5
x's length = 104448

real    0m50.436s
user    0m50.431s
sys     0m0.488s

在javascript中(nodejs 0.6.3)

1
2
3
4
5
6
7
8
9
10
11
function test()
{
    var x ="";
    var limit = 102 * 1024;
    while (x.length < limit) {
        x +="X";
        if (x.indexOf("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) > 0)
            console.log("OK");
    }
    console.log("x's length =" + x.length);
}();

结果是:

1
2
3
4
5
x's length = 104448

real    0m3.115s
user    0m3.084s
sys     0m0.048s

在C++中(G++—Ofast)

NoDEJS比Python或Java更好,这并不奇怪。但我希望libstdc++比nodejs能提供更好的性能,它的结果真的让我吃惊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
using namespace std;
void test()
{
    int x = 0;
    int limit = 102 * 1024;
    string s("");
    for (; s.size() < limit;) {
        s +="X";
        if (s.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) != string::npos)
            cout <<"Find!" << endl;
    }
    cout <<"x's length =" << s.size() << endl;
}

int main()
{
    test();
}

结果是:

1
2
3
4
5
x length = 104448

real    0m5.905s
user    0m5.900s
sys     0m0.000s

小结

好的,现在让我们看看总结:

  • nodejs上的javascript(V8):3.1
  • cpython 2.7.2上的python:8.8s
  • 用LIbSTDC++ ++:5.9s
  • 基于OpenJDK 7的Java:50.4S

真想不到!我尝试了"-O2,-O3"的C++,但没有帮助。在V8中,C++似乎只有50%的JavaScript性能,甚至比CPython还要差。如果我错过了GCC中的一些优化,有人能给我解释一下吗?还是这样?非常感谢。


并不是EDCOX1 1的执行不好(就像我不喜欢C++)一样,字符串处理对于其他语言来说是非常优化的。

您对字符串性能的比较是误导性的,如果它们的目的不仅仅是为了表示这一点,那么它们是自以为是的。

我知道python字符串对象完全是在C中实现的,实际上在python 2.7上,由于unicode字符串和字节之间缺乏分离,因此存在许多优化。如果您在python 3.x上运行这个测试,您会发现它慢得多。

Javascript有许多高度优化的实现。可以预料,这里的字符串处理非常出色。

您的Java结果可能是由于字符串处理不当或其他一些糟糕的情况造成的。我希望Java专家可以介入并通过一些更改来修复这个测试。

至于C++实例,我预计性能会略微超过Python版本。它执行相同的操作,同时减少了解释程序开销。这反映在您的结果中。在使用s.reserve(limit);进行测试之前,将消除重新分配开销。

我重复一遍,您只测试语言实现的一个方面。此测试的结果不反映总体语言速度。

我提供了一个C版本来展示这样的比赛有多么愚蠢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>

void test()
{
    int limit = 102 * 1024;
    char s[limit];
    size_t size = 0;
    while (size < limit) {
        s[size++] = 'X';
        if (memmem(s, size,"ABCDEFGHIJKLMNOPQRSTUVWXYZ", 26)) {
            fprintf(stderr,"zomg
"
);
            return;
        }
    }
    printf("x's length = %zu
"
, size);
}

int main()
{
    test();
    return 0;
}

时机:

1
2
3
4
5
6
matt@stanley:~/Desktop$ time ./smash
x's length = 104448

real    0m0.681s
user    0m0.680s
sys     0m0.000s


所以我在ideone.org上玩过。

这里是你原来的C++程序的一个稍微修改过的版本,但是在循环中添加了它,所以它只测量了对EDCOX1的调用(10)。请注意,我必须将迭代次数减少到约40%,否则ideone.org会终止进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>

int main()
{
    const std::string::size_type limit = 42 * 1024;
    unsigned int found = 0;

    //std::string s;
    std::string s(limit, 'X');
    for (std::string::size_type i = 0; i < limit; ++i) {
        //s += 'X';
        if (s.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) != std::string::npos)
            ++found;
    }

    if(found > 0)
        std::cout <<"Found" << found <<" times!
"
;
    std::cout <<"x's length =" << s.size() << '
'
;

    return 0;
}

我在ideone.org上的结果是time: 3.37s。(当然,这是非常可疑的,但请容我一会儿,等待另一个结果。)

现在,我们使用这段代码并交换注释行,以测试附加,而不是查找。注意,这一次,在尝试查看任何时间结果时,我增加了10倍的迭代次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <string>

int main()
{
    const std::string::size_type limit = 1020 * 1024;
    unsigned int found = 0;

    std::string s;
    //std::string s(limit, 'X');
    for (std::string::size_type i = 0; i < limit; ++i) {
        s += 'X';
        //if (s.find("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0) != std::string::npos)
        //    ++found;
    }

    if(found > 0)
        std::cout <<"Found" << found <<" times!
"
;
    std::cout <<"x's length =" << s.size() << '
'
;

    return 0;
}

尽管迭代次数增加了10倍,但我在ideone.org上的结果是time: 0s

我的结论是:在C++中,这个基准是由搜索操作高度控制的,在循环中字符的附加对结果没有影响。这真的是你的意图吗?


惯用的C++解决方案是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>
#include

int main()
{
    const int limit = 102 * 1024;
    std::string s;
    s.reserve(limit);

    const std::string pattern("ABCDEFGHIJKLMNOPQRSTUVWXYZ");

    for (int i = 0; i < limit; ++i) {
        s += 'X';
        if (std::search(s.begin(), s.end(), pattern.begin(), pattern.end()) != s.end())
            std::cout <<"Omg Wtf found!";
    }
    std::cout <<"X's length =" << s.size();
    return 0;
}

我可以通过将字符串放在堆栈上并使用memmem来大大加快速度——但似乎没有必要。在我的机器上运行,这已经是Python解决方案速度的10倍多了。

[在我的笔记本电脑上]

时间/测试X的长度=104448实际0m2.055s用户0m2.049sSsys 0M0.1001S


这是最明显的一个:请在主循环之前尝试执行s.reserve(limit);

文件在这里。

我要提到的是,在C++中直接使用标准类的方式与Java或Python中使用的标准类相同,如果您不知道书桌后面做了什么,通常会给您提供低于标准的性能。语言本身并没有神奇的表现,它只是给了你正确的工具。


这里缺少的是查找搜索固有的复杂性。

您正在执行搜索102 * 1024次(104448)。一个简单的搜索算法每次都会尝试匹配模式,从第一个字符开始,然后是第二个字符,等等。

因此,您有一个字符串,它从长度1到EDCOX1(18),并且在每个步骤中搜索该字符串的模式,这是C++中的线性操作。这就是N * (N+1) / 2 = 5 454 744 576比较。我不像你那么惊讶,这需要一些时间…

让我们通过使用搜索单个Afind的过载来验证假设:

1
2
Original: 6.94938e+06 ms
Char    : 2.10709e+06 ms

大约快3倍,所以我们在同一个数量级内。因此,使用一个完整的字符串并不是很有趣。

结论?也许可以对find进行一点优化。但这个问题不值得。

注:对于那些吹捧博伊尔·摩尔的人来说,恐怕针太小了,所以没什么用。可以减少一个数量级(26个字符),但不能再减少。


我的第一个想法是没有问题。

C++提供了第二个最佳性能,比Java快十倍。也许除了Java之外,所有的运行都接近实现该功能的最佳性能,您应该看看如何修复Java问题(提示-EDCOX1,0)。

在C++案例中,有一些事情试图提高性能一点。特别地。。。

  • s += 'X';而不是s +="X";
  • 在循环外声明string searchpattern ("ABCDEFGHIJKLMNOPQRSTUVWXYZ");,并将其传递给find调用。一个std::string实例知道它自己的长度,而一个c字符串需要一个线性时间检查来确定它的长度,这可能(或不可能)与std::string::find的性能有关。
  • 尝试使用EDCOX1,7,因为类似的原因,为什么你应该使用EDCOX1,0,Java,虽然最有可能的重复转换回EDCOX1,9,将产生更多的问题。

总的来说,结果并不令人惊讶。JavaScript,具有良好的JIT编译器,在这种情况下,可以比C++静态优化稍微好一些。

有足够的工作,你应该总是能够优化C++比JavaScript更好,但是总会有这样的情况,这不仅仅是自然发生的,而且可能需要相当多的知识和努力来实现它。


C/C++语言是不容易的,需要数年才能制作出快速的程序。

使用从C版本修改的strncmp(3)版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>

void test()
{
    int limit = 102 * 1024;
    char s[limit];
    size_t size = 0;
    while (size < limit) {
        s[size++] = 'X';
        if (!strncmp(s,"ABCDEFGHIJKLMNOPQRSTUVWXYZ", 26)) {
            fprintf(stderr,"zomg
"
);
            return;
        }
    }
    printf("x's length = %zu
"
, size);
}

int main()
{
    test();
    return 0;
}

对于C++,尝试使用EDCOX1对"ABCDFEGHKLMNOPPQRSTUVWXYZ"1来实现——在我的实现EDCOX1中,4计算字符串参数的长度。


我自己刚刚测试了C++示例。如果我取消对std::sting::find的调用,程序将立即终止。因此,字符串连接期间的分配在这里是没有问题的。

如果我添加一个变量sdt::string abc ="ABCDEFGHIJKLMNOPQRSTUVWXYZ",并替换std::string::find调用中出现的"abc…xyz",程序需要几乎与原始示例相同的时间来完成。这再次表明,分配和计算字符串的长度不会给运行时增加太多内容。

因此,对于您的示例来说,libstdc++使用的字符串搜索算法似乎不如javascript或python的搜索算法快。也许你想用你自己的字符串搜索算法再次尝试C++,它更适合你的目的。


您的测试代码正在检查过多字符串串联的病理情况。(测试中的字符串搜索部分可能被省略了,我敢打赌,它对最终结果几乎没有任何作用。)过多的字符串连接是一个陷阱,大多数语言都强烈警告不要这样做,并为(即,stringbuilder)提供非常著名的替代方法,所以您在这里测试的本质是在完全预期失败的情况下,ESE语言会失败。那是毫无意义的。

类似的无意义测试的一个例子是,比较各种语言在紧密循环中抛出和捕获异常时的性能。所有语言都警告说异常的抛出和捕获速度非常慢。它们没有指定速度有多慢,只是警告您不要期待任何事情。因此,继续进行精确的测试是毫无意义的。

因此,重复测试会更有意义,用这些语言中的每一种提供的任何构造替换无意识的字符串连接部分(s+="x"),以避免字符串连接。(例如,类StringBuilder。)


在nodejs中,搜索子串的算法似乎更好。你可以实现自我并尝试它。


正如SBI所提到的,测试用例由搜索操作控制。我想知道C++和JavaScript之间的文本分配速度有多快。

系统:树莓PI 2,G+4.4.3,节点V0.12.0,G++-STD= C++0X-O2 Pr.CPP

C++:770MS

无保留的C++:1196MS

javascript:2310毫秒

C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>
#include <chrono>
using namespace std;
using namespace std::chrono;

void test()
{
    high_resolution_clock::time_point t1 = high_resolution_clock::now();
    int x = 0;
    int limit = 1024 * 1024 * 100;
    string s("");
    s.reserve(1024 * 1024 * 101);
    for(int i=0; s.size()< limit; i++){
        s +="SUPER NICE TEST TEXT";
    }

    high_resolution_clock::time_point t2 = high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>( t2 - t1 ).count();
    cout << duration << endl;
}

int main()
{
    test();
}

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test()
{
    var time = process.hrtime();
    var x ="";
    var limit = 1024 * 1024 * 100;
    for(var i=0; x.length < limit; i++){
        x +="SUPER NICE TEST TEXT";
    }

    var diff = process.hrtime(time);
    console.log('benchmark took %d ms', diff[0] * 1e3 + diff[1] / 1e6 );
}

test();