【问题标题】:Why do std::string operations perform poorly?为什么 std::string 操作表现不佳?
【发布时间】:2012-01-08 18:00:07
【问题描述】:

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

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

在 Python (2.7.2) 中:

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()

给出结果:

x's length is : 104448

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

在 Java (OpenJDK-7) 中:

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!\n");
        }
        System.out.printf("x's length = %d\n", s.length());
    }
}

给出结果:

x's length = 104448

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

在 Javascript (Nodejs 0.6.3) 中

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

给出结果:

x's length = 104448

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

在 C++ 中 (g++ -Ofast)

Nodejs 的性能优于 Python 或 Java 并不奇怪。但我预计 libstdc++ 会比 Nodejs 提供更好的性能,结果让我很惊讶。

#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();
}

给出结果:

x length = 104448

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

简要总结

好的,现在让我们看看摘要:

  • Nodejs(V8) 上的javascript:3.1s
  • CPython 2.7.2 上的 Python:8.8s
  • 带有 libstdc++ 的 C++:5.9 秒
  • OpenJDK 7 上的 Java:50.4 秒

令人惊讶!我在 C++ 中尝试了“-O2,-O3”,但注意到有帮助。在 V8 中,C++ 的性能似乎只有 javascript 的 50%,甚至比 CPython 还要差。如果我错过了 GCC 中的一些优化,谁能向我解释一下,或者只是这种情况?非常感谢。

【问题讨论】:

  • 您正在测试混合操作,您可能应该尝试将测试划分为执行不同性能检查的不同测试,例如:增长字符串,或查找,或...目前您不能知道时间花在了哪里。顺便说一句,这对于决定一种语言可能是一个非常无用的测试......
  • 在循环之前尝试s.reserve(limit);
  • @AshBurlaczenko 可能是因为 Java 中的字符串是不可变的。我想s += "X" 是那里的性能杀手。这就是StringBuilder 存在的原因。
  • @AshBurlaczenko:在 Java 中,字符串是不可变的并且是池化的,因此非常慢。通常,您使用 stringbuilders 来组装字符串。无论如何,这整件事都是在比较苹果和橘子。
  • 您的结果中还包括每种语言的运行时启动和终止。

标签: c++ python performance node.js stl


【解决方案1】:

似乎在 nodejs 中有更好的子字符串搜索算法。您可以自己实现并尝试一下。

【讨论】:

    【解决方案2】:

    并不是std::string 表现不佳(尽管我不喜欢 C++),而是字符串处理针对那些其他语言进行了高度优化。

    您对字符串性能的比较具有误导性,并且如果它们旨在代表的不仅仅是这些,也是自以为是的。

    我知道 Python string objects are completely implemented in C 以及在 Python 2.7 上确实存在 numerous optimizations 是因为 unicode 字符串和字节之间缺乏分隔。如果您在 Python 3.x 上运行此测试,您会发现它的速度要慢得多。

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

    您的 Java 结果可能是由于不正确的字符串处理或其他一些不良情况。我希望 Java 专家可以介入并通过一些更改来修复此测试。

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

    我再说一遍,您只是在测试语言实现的一个方面。此测试的结果不反映整体语言速度。

    我提供了一个 C 版本来展示这种小便比赛是多么愚蠢:

    #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\n");
                return;
            }
        }
        printf("x's length = %zu\n", size);
    }
    
    int main()
    {
        test();
        return 0;
    }
    

    时间:

    matt@stanley:~/Desktop$ time ./smash 
    x's length = 104448
    
    real    0m0.681s
    user    0m0.680s
    sys     0m0.000s
    

    【讨论】:

    • FWIW Python 2.7 和 3.2 之间的差异不到 10%。 PEP 393 可能会消除 Python 3.3 中的这种差异。另外值得一提的是,在 Python 中搜索字符串使用了一种 Boyer-Moore 形式,因此当字符串变长时,它应该比进行简单搜索的语言更有优势。
    • @Matt:嗯,C 程序太极端了……我并没有试图在语言之间进行战斗或较量,因为每种语言都有不同的优化方式。我只是想找到一种可以非常高效地处理字符串的语言。该程序刚刚描述了一个程序从输入(控制台或套接字)读取,然后可能对其进行解密,并在字符串中搜索指定模式的情况。我的测试程序简化了程序,当然只是一个演示。结果只是提醒我 C++ 并不总是最锋利的刀。无论如何,谢谢:)
    • @Wu Shu:如果要搜索的特定模式是固定的和预先确定的,您可以构造一个自动机来搜索该模式。这将比重复调用std::string::find 快得多。
    • @WuShu:实际上,C 和 C++ 可能是最锋利的刀。只是 Python 和 Node.js 可能是电锯。它很繁重,有时甚至有点矫枉过正,但是当您在 C++ 中感到疲倦时,您会欣赏他们在 Python 中采用的“包含电池”的方法。
    • 在 java 中,使用 StringBuilder 而不是 String 可以将它(在我的机器上)加速大约 4 倍,其余的正在搜索。在java中,字符串是不可变的,所以他正在做的是在java中严重错误的字符串操作。然后是时间 VM 启动而不是时间有用操作的问题(这是 VM 上所有语言的问题,而不仅仅是 java)
    【解决方案3】:

    正如 sbi 所说,测试用例以搜索操作为主。 我很好奇 C++ 和 Javascript 之间的文本分配速度有多快。

    系统:树莓派 2,g++ 4.6.3,节点 v0.12.0,g++ -std=c++0x -O2 perf.cpp

    C++ : 770 毫秒

    C++ 无保留:1196ms

    Javascript:2310 毫秒

    C++

    #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

    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();
    

    【讨论】:

      【解决方案4】:

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

      从 c 版本修改的 strncmp(3) 版本:

      #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\n");
                  return;
              }
          }
          printf("x's length = %zu\n", size);
      }
      
      int main()
      {
          test();
          return 0;
      }
      

      【讨论】:

        【解决方案5】:

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

        #include <iostream>
        #include <string>
        #include <algorithm>
        
        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 倍以上。

        [在我的笔记本电脑上]

        时间 ./test X 的长度 = 104448 实际0m2.055s 用户 0m2.049s 系统 0m0.001s

        【讨论】:

        • 已确认。 g++ 4.4.3。在我的测试中搜索 5 秒,查找 12.5 秒(都在同一个 exe 中;我的测试时间更长,因为我使用 std::string s(limit,'X'); 预先创建了字符串,即搜索和查找还有更多工作要做。)结论:stdlib find( ) 在 g++ 上具有很大的优化潜力!
        • 哇;添加了一个 memmem() 版本,它是 0.75s(使用相同的字符串,通过 c_str() 访问)。 (实际上,它是 0;整个循环似乎被优化了;所以我在循环中添加了一些小的计算来阻止它。) 新结论:find() 和 search() 正在做一些奇怪的事情,即使 -O3 也不能优化,或者 memmem 正在使用一些特殊的 CPU 功能。令人着迷!
        • std::search 比 std::string::search 快的原因是因为(按照惯例?)std::search 是在头文件中实现的,这为编译器提供了更多优化空间.另一方面,std::string::search 不是。 (而且因为这是多次调用该函数,所以会产生很大的不同)
        • @Heptic:嗯。 std::string 只是 std::basic_string&lt;char&gt; 的 typedef,它是一个模板,因此完全在 headers 中实现。
        【解决方案6】:

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

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

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

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

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

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

        结论?也许find 可以稍微优化一下。但是这个问题不值得。

        注意:对于吹捧Boyer Moore 的人来说,恐怕针太小了,所以用处不大。可能会减少一个数量级(26 个字符),但不会更多。

        【讨论】:

        • 大海捞针中没有A,所以它应该只检查字符串中没有找到的每个字符,而不是查看模式的其他字符。您似乎在描述find_any_of 方法,它应该再次在这里很快找到'X'
        • @UncleBens:完全不是,我说的是find,即使对于字符串模式,如果它不匹配,它也应该在模式的第一个字符处停止,然后在大海捞针中继续前进。查找单个字符A(模式的第一个字符)仅快 3 倍这一事实证实了我的怀疑,不是模式搜索慢,而只是在这么长的字符串中查找模式这么多次本身就很慢。
        【解决方案7】:

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

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

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

        【讨论】:

        • 好吧,如果你删除 string::find,这只是字符串连接,这在为字符串优化的语言/运行时之间没有太大区别:C++ 中的字符串也比 C 中的优化得多(字符串作为字符数组)。 string::find 不仅是对搜索算法的测试,也是对字符串遍历的测试。我再做一次测试。
        【解决方案8】:

        所以我去 ideone.org 上玩了一下。

        这里是原始 C++ 程序的略微修改版本,但循环中的附加被删除了,所以它只测量对 std::string::find() 的调用。 请注意,我必须将迭代次数减少到 ~40%,否则 ideone.org 会终止该进程。

        #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!\n";
            std::cout << "x's length = " << s.size() << '\n';
        
            return 0;
        }
        

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

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

        #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!\n";
            std::cout << "x's length = " << s.size() << '\n';
        
            return 0;
        }
        

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

        我的结论:这个基准在 C++ 中,高度受搜索操作支配,循环中的字符附加对结果完全没有影响.这真的是你的意图吗?

        【讨论】:

        • @sbi: 那就是当一个笔记比 C++ find 是 O(N),而在 Python indexOf 使用 Boyer-Moore (正如 Duncan 在评论中指出的那样) .再次,“包括电池”。
        • @Matthieu M.:Boyer-Moore 在这里没有任何收获,因为在搜索字符串中根本找不到搜索字符串的第一个字符。相反,它可能会增加一些开销,在循环的每次迭代中不必要地处理搜索字符串。
        • 我们确定 string::find(const char*) 不只是根据 string::find(const string&) 实现的吗?如果是这样,这里的内存分配可能会很昂贵。
        • @Kylotan:我都测试了。没有明显的区别。
        • @MikeNakis:确实,我对其进行了测试,甚至手动执行循环不变代码运动(将模式分析移出循环),boyer-moore 搜索仍然较慢。因此我怀疑他们使用了更复杂的东西,可能更接近memmem
        【解决方案9】:

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

        C++ 提供第二好的性能,几乎是 Java 的十倍。也许除了 Java 之外的所有设备都在接近该功能可实现的最佳性能,您应该研究如何解决 Java 问题(提示 - StringBuilder)。

        在 C++ 的情况下,有一些事情可以尝试提高性能。特别是……

        • s += 'X'; 而不是 s += "X";
        • 在循环外声明string searchpattern ("ABCDEFGHIJKLMNOPQRSTUVWXYZ");,并将其传递给find 调用。 std::string 实例知道它自己的长度,而 C 字符串需要线性时间检查才能确定,这可能(或可能不)与 std::string::find 性能相关。
        • 尝试使用std::stringstream,原因与您应该在Java 中使用StringBuilder 的原因类似,但很可能重复转换回string 会产生更多问题。

        总的来说,结果并不太令人惊讶。在这种情况下,具有良好 JIT 编译器的 JavaScript 可能能够比 C++ 静态编译更好地优化。

        通过足够的工作,您应该始终能够比 JavaScript 更好地优化 C++,但总会有这样的情况不会自然发生,并且可能需要相当多的知识和努力才能实现。

        【讨论】:

        • 性能受限于find 调用,而不是分配。例如,测试第 2 点,(根本)没有区别。
        • @Matthieu - 好吧,我没有说我的任何想法肯定会有所作为。然而,第二点是所有关于find 调用。关键是使用find 的不同重载,它将搜索模式作为std::string 而不是C 字符串,因此(可能但不是绝对)避免strlenfind 调用中调用。另一个想法是,由于搜索模式是恒定的,编译模式方法可能工作得更快(例如,Boyer-Moore 字符串搜索),但这是作弊 - 除非例如JavaScript 优化器比我想象的要聪明得多。
        • 我测试了一个幼稚的 Boyer-Moore(在每个步骤中构建表格),但它的表现更差。与干草堆的大小(104448 个字符)相比,针非常小(26 个字符),因此额外的复杂性平衡了预期的加速。我想在外面建桌子可能会有所帮助……但可能没有预期的那么多。
        • Stringstream 不会在此处提供任何性能改进。 std::string 已经是可变的并且可以在恒定的摊销时间内插入。
        【解决方案10】:

        对于 C++,尝试将 std::string 用于“ABCDEFGHIJKLMNOPQRSTUVWXYZ” - 在我的实现中,string::find(const charT* s, size_type pos = 0) const 计算字符串参数的长度。

        【讨论】:

          【解决方案11】:

          您的测试代码正在检查过度字符串连接的病态场景。 (测试的字符串搜索部分可能被省略了,我敢打赌它对最终结果几乎没有任何贡献。)过多的字符串连接是大多数语言强烈警告的一个陷阱,并提供了众所周知的替代方案, (即 StringBuilder,)所以你在这里基本上测试的是这些语言在完全预期失败的情况下失败的严重程度。那是没有意义的。

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

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

          【讨论】:

          • 我自己刚刚查看了示例代码,结果发现几乎所有的运行时间都花在了字符串搜索上。
          • o_O -- 好的,然后发生了一些非常奇怪的事情。在发布我的答案之前,我检查了上述所有语言中所有 find() 和 indexOf() 方法的文档,以确保它们都执行直接的非正则表达式、区分大小写的搜索。因此,如果尽管任务微不足道,搜索仍然是个问题,我不知道该说什么。
          • 好吧,我只检查了 C++ 示例,我认为您对 Java 示例的性能非常差是正确的。
          • @swegi 您检查了哪些语言?我预计它们之间可能会有所不同。使用 Python 2.7 编写的代码在我的系统上需要 13.1 秒,删除 find 调用需要 0.019 秒。所以字符串连接(至少在 Python 上)是测试的无关部分。这可能只是因为 C 版本的 Python 使用引用计数并且可以在检测到字符串只有一个引用时就地进行连接。
          • std::string::operator+= 由 C++ 提供的构造,用于避免在 Java 中导致字符串连接变慢的事情。 std::string 是一个可变类,与StringBuilder 相同。 TBH 我认为“为什么 C++ 慢?”这个问题有点令人困惑,但其中包含 waaay 慢的 Java 结果,这促使各种人解释为什么 Java 结果很慢。这与问题无关;-)
          【解决方案12】:

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

          文档是here

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

          【讨论】:

          • 在我的机器上,在循环之前添加s.reserve(limit) 对性能没有明显影响。
          • 我同意你所说的一般,但你测试过吗?使用 gcc 4.6 我在使用 string::reserve 时没有得到任何加速。您能否展示如何利用类如何在后台工作的知识快速进行连接?
          • 这真的是个问题吗?每个string::operator++ 只附加一个字符,因此内存重新分配和复制不应该是一个很大的消耗。
          • 好吧,在实践中检查了这一点。将 s += "X" 替换为字符串 s(102*1024, 'X');极大地提高了速度(我的 VBox 中真正的 0m0.003s )。 std::string::reserve 无济于事,尽管我已经说过(尽管我认为应该具有相同的效果)。需要再调查一下。编辑:大声笑,现在才注意到 for 循环的表述方式:) 好的,回滚所有内容
          • 当然,构建字符串可以极大地提高速度。然后您完全绕过循环...如果您首先分配字符串,则需要更改循环条件以迭代 i = 0 变量,然后您会注意到搜索是真正的问题。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2021-11-01
          • 1970-01-01
          • 1970-01-01
          • 2018-12-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多