使用字符串与字符数组时性能差异有多大?

Posted

技术标签:

【中文标题】使用字符串与字符数组时性能差异有多大?【英文标题】:How much performance difference when using string vs char array? 【发布时间】:2014-02-21 22:24:56 【问题描述】:

我有以下代码:

char fname[255] = 0
snprintf(fname, 255, "%s_test_no.%d.txt", baseLocation, i);

std::string fname = baseLocation + "_test_no." + std::to_string(i) + ".txt";

哪一个表现更好?第二个是否涉及临时创建?有没有更好的方法来做到这一点?

【问题讨论】:

你如何衡量发生一次且耗时为零的事情的性能? 除非您将该代码调用数百万次,否则您将很难注意到其中的差异。测量,是的,但请注意,不是那么多。也就是说,由于创建了临时对象,第二个可能会花费更长的时间,但一个好的编译器可能会优化很多。 【参考方案1】:

让我们计算一下数字:

2022 年编辑:

使用带有 GCC 10.3 的 Quick-Bench 并使用 C++20 进行编译(对常量进行了一些小的更改)表明 std::string 是 now faster,几乎是 3 倍:


原始答案(2014 年)

代码(我用PAPITimers)

main.cpp

#include <iostream>
#include <string>
#include <stdio.h>
#include "papi.h"
#include <vector>
#include <cmath>
#define TRIALS 10000000

class Clock

  public:
    typedef long_long time;
    time start;
    Clock() : start(now())
    void restart() start = now(); 
    time usec() const return now() - start; 
    time now() const return PAPI_get_real_usec(); 
;


int main()

  int eventSet = PAPI_NULL;
  PAPI_library_init(PAPI_VER_CURRENT);
  if(PAPI_create_eventset(&eventSet)!=PAPI_OK) 
  
    std::cerr << "Failed to initialize PAPI event" << std::endl;
    return 1;
  

  Clock clock;
  std::vector<long_long> usecs;

  const char* baseLocation = "baseLocation";
  //std::string baseLocation = "baseLocation";
  char fname[255] = ;
  for (int i=0;i<TRIALS;++i)
  
    clock.restart();
    snprintf(fname, 255, "%s_test_no.%d.txt", baseLocation, i);
    //std::string fname = baseLocation + "_test_no." + std::to_string(i) + ".txt";
    usecs.push_back(clock.usec());
  

  long_long sum = 0;
  for(auto vecIter = usecs.begin(); vecIter != usecs.end(); ++vecIter)
  
    sum+= *vecIter;
  

  double average = static_cast<double>(sum)/static_cast<double>(TRIALS);
  std::cout << "Average: " << average << " microseconds" << std::endl;

  //compute variance
  double variance = 0;
  for(auto vecIter = usecs.begin(); vecIter != usecs.end(); ++vecIter)
  
    variance += (*vecIter - average) * (*vecIter - average);
  

  variance /= static_cast<double>(TRIALS);
  std::cout << "Variance: " << variance << " microseconds" << std::endl;
  std::cout << "Std. deviation: " << sqrt(variance) << " microseconds" << std::endl;
  double CI = 1.96 * sqrt(variance)/sqrt(static_cast<double>(TRIALS));
  std::cout << "95% CI: " << average-CI << " usecs to " << average+CI << " usecs" << std::endl;  

与 cmets 一起玩以获得一种或另一种方式。 在我的机器上使用编译行对这两种方法进行了 1000 万次迭代:

g++ main.cpp -lpapi -DUSE_PAPI -std=c++0x -O3

使用字符数组:

Average: 0.240861 microseconds
Variance: 0.196387microseconds
Std. deviation: 0.443156 microseconds
95% CI: 0.240586 usecs to 0.241136 usecs

使用字符串方法:

Average: 0.365933 microseconds
Variance: 0.323581 microseconds
Std. deviation: 0.568842 microseconds
95% CI: 0.365581 usecs to 0.366286 usecs

所以至少在我的机器上使用我的代码和我的编译器设置,我看到在移动到字符串时速度降低了大约 50%。 使用以下公式,字符数组的速度比字符串快 34% :

((字符串时间)-(字符数组时间))/(字符串时间)

这将方法之间的时间差异作为单独字符串的时间百分比给出。我原来的百分比是正确的;我改为使用字符数组方法作为参考点,它显示在移动到字符串时会减慢 52%,但我发现它具有误导性。

我会因为我做错的事情而接受任何和所有的 cmets :)


2015 年编辑

使用 GCC 4.8.4 编译:

字符串

Average: 0.338876 microseconds
Variance: 0.853823 microseconds
Std. deviation: 0.924026 microseconds
95% CI: 0.338303 usecs to 0.339449 usecs

字符数组

Average: 0.239083 microseconds
Variance: 0.193538 microseconds
Std. deviation: 0.439929 microseconds
95% CI: 0.238811 usecs to 0.239356 usecs

因此,字符数组方法的速度仍然明显更快,尽管速度较慢。在这些测试中,它的速度提高了大约 29%。

【讨论】:

干杯,我想我已经解释了你观察到的行为,看看我的回答 :-) +1 实际性能测试。 将该基址设为 80 个字符串,同时在循环内声明 char fname[255]=。然后进行第三次测试并尝试在循环外声明的 std::string 并在内部使用 append 或 operator+= 。我相信那个人会赢。 @ÖöTiib:我已经在我的帖子中提到了你们的 cmets。我按照我最初的方式执行计时,因为我觉得它更纯粹地捕捉到了 OP 正在做的事情。此外,我已经用我在 Ubuntu 上拥有的最新版本的 GCC 更新了时间。 (虽然不是最新的整体。截至撰写本文时,5.2 已经发布) 对不起,但它看起来不像我建议的那样。我的建议是使用 80 个字符串作为 baselocation 文本本身。其他建议是不要使用 operator+ 或 operator=(string&&) 而是使用 operator+= 或 string::append 。 string::reserve 只有在插入或附加到字符串时才有意义。当您构建几兆字节的文本时,可能会出现这种情况,而这种微秒的分数很重要。在移动分配之前做保留是没有意义的。 @ÖöTiib:抱歉耽搁了。谢谢你的澄清。我想我明白你的建议了。也许它会更快,但是我不怀疑字符串和字符数组方法之间的差异会发生很大变化。【参考方案2】:

snprintf() 版本几乎肯定会快很多。为什么?仅仅是因为没有发生内存分配。 new 运算符非常昂贵,在我的系统上大约需要 250ns - snprintf() 在此期间将完成相当多的工作。

这并不是说您应该使用snprintf() 方法:您付出的代价是安全。您提供给snprintf() 的固定缓冲区大小很容易出错,并且您绝对需要为缓冲区不够大的情况提供代码。因此,只有在您确定这部分代码对性能非常关键时才考虑使用snprintf()

如果你有一个 POSIX-2008 兼容的系统,你也可以考虑尝试asprintf() 而不是snprintf(),它会为你提供malloc() 的内存,给你几乎和C++一样的舒适string s。至少在我的系统上,malloc() 比内置的 new-operator 快很多(不过不要问我为什么)。


编辑: 刚刚看到,您在示例中使用了文件名。如果您关心文件名,请忘记字符串操作的性能!您的代码几乎不会花时间在其中。除非您有大约 100000 次这样的字符串操作每秒,否则它们与您的性能无关。

【讨论】:

正确。我什至尝试在我的答案中预先声明并分配字符串中的空间,但这实际上只会导致事情变慢。【参考方案3】:

如果它真的很重要,请衡量这两种解决方案。如果不是,您认为从您拥有的数据、公司/私人编码风格标准等中最有意义的那一个。确保使用优化的构建 [与您将在实际生产构建中使用的优化相同,而不是 -O3因为这是最高的,如果您的生产版本使用 -O1]

如果你只做一些,我希望两者都会非常接近。如果你有几百万,可能会有差异。哪个更快?我猜是第二个 [1],但这取决于谁编写了 snprintf 的实现以及谁编写了 std::string 的实现。两者肯定有可能比你想象的要花更长的时间来了解函数的工作原理(并且可能运行得比你想象的要快)

[1] 因为我用过printf,而且它不是一个简单的函数,所以花了很多时间来搞乱格式字符串的各种摸索。它不是很有效(我也看过 glibc 等中的那些,但它们并没有明显更好)。 另一方面,std::string 函数通常是内联的,因为它们是模板实现,从而提高了效率。包中的小丑是std::string 的内存分配是否可能发生。当然,如果不知何故 baselocation 变得相当大,您可能无论如何都不想将它存储为固定大小的本地数组,这样在这种情况下就可以平衡了。

【讨论】:

你似乎比我了解更多。想对我的帖子发表评论,看看我怎样才能让时间更公平? @Mats Petersson .. 都是真的,但您可能已经知道有一些优化的 printf 可作为开源使用,其中牺牲了深奥的格式以提高性能。角落里的粉红色大象是 std::allocator 设计......那个部门没有改进,在我知道的 6 年后......不要让我透露我的来源:)【参考方案4】:

我建议在这种情况下使用 strcat。这是迄今为止最快的方法:

【讨论】:

以上是关于使用字符串与字符数组时性能差异有多大?的主要内容,如果未能解决你的问题,请参考以下文章

如何将字符串数组转化为一个字符串

长阵列性能问题

用户代理字符串可以有多大?

关于kmp算法

BCM5356与BCM5357区别究竟有多大?

差异方法初始化char数组