【问题标题】:Char* vs String Speed in C++C++ 中的 Char* 与字符串速度
【发布时间】:2011-04-28 16:41:14
【问题描述】:

我有一个 C++ 程序,它将从二进制文件中读取数据,最初我将数据存储在 std::vector<char*> data 中。我已经更改了我的代码,所以我现在使用字符串而不是 char*,所以 std::vector<std::string> data。例如,我必须做的一些更改是从 strcmp 更改为 compare

但是我发现我的执行时间急剧增加。对于示例文件,当我使用 char* 时需要 0.38 秒,在我的 Linux 机器上转换为字符串后需要 1.72 秒。我在我的 Windows 机器上观察到一个类似的问题,执行时间从 0.59 秒增加到 1.05 秒。

我相信这个功能会导致速度变慢。它是转换器类的一部分,注意在变量名末尾用_ 指定的私有变量。我显然在这里遇到了内存问题,并且卡在了 C 和 C++ 代码之间。 我希望这是 C++ 代码,所以我更新了底部的代码。

我在另一个函数中也多次访问ids_names_,所以访问速度非常重要。 通过使用创建map 而不是两个单独的向量,我能够以更稳定的 C++ 代码实现更快的速度。谢谢大家!

NewList.Txt 示例

2515    ABC 23.5    32  -99 1875.7  1  
1676    XYZ 12.5    31  -97 530.82  2  
279  FOO 45.5    31  -96  530.8  3  

旧代码:

void converter::updateNewList(){
    FILE* NewList;
    char lineBuffer[100];
    char* id = 0;
    char* name = 0;

    int l = 0;
    int n;

    NewList = fopen("NewList.txt","r");
    if (NewList == NULL){
        std::cerr << "Error in reading NewList.txt\n";
        exit(EXIT_FAILURE);
    } 

    while(!feof(NewList)){
        fgets (lineBuffer , 100 , NewList); // Read line    
        l = 0;
        while (!isspace(lineBuffer[l])){
            l = l + 1;
        }

        id = new char[l];
        switch (l){
            case 1: 
                n = sprintf (id, "%c", lineBuffer[0]);
                break;
            case 2:
                n = sprintf (id, "%c%c", lineBuffer[0], lineBuffer[1]);
                break;
            case 3:
                n = sprintf (id, "%c%c%c", lineBuffer[0], lineBuffer[1], lineBuffer[2]);        
                break;
            case 4:
                n = sprintf (id, "%c%c%c%c", lineBuffer[0], lineBuffer[1], lineBuffer[2],lineBuffer[3]);
                break;
            default:
                n = -1;
                break;
        }
        if (n < 0){
            std::cerr << "Error in processing ids from NewList.txt\n";
            exit(EXIT_FAILURE);
        }

        l = l + 1;
        int s = l;
        while (!isspace(lineBuffer[l])){
            l = l + 1;
        }
        name = new char[l-s];
        switch (l-s){
            case 2:
                n = sprintf (name, "%c%c", lineBuffer[s+0], lineBuffer[s+1]);
                break;
            case 3:
                n = sprintf (name, "%c%c%c", lineBuffer[s+0], lineBuffer[s+1], lineBuffer[s+2]);
                break;
            case 4:
                n = sprintf (name, "%c%c%c%c", lineBuffer[s+0], lineBuffer[s+1], lineBuffer[s+2],lineBuffer[s+3]);
                break;
            default:
                n = -1;
                break;
        }
        if (n < 0){
            std::cerr << "Error in processing short name from NewList.txt\n";
            exit(EXIT_FAILURE);
        }


        ids_.push_back ( std::string(id) );
        names_.push_back(std::string(name));
    }

    bool isFound = false;
    for (unsigned int i = 0; i < siteNames_.size(); i ++) {
        isFound = false;
        for (unsigned int j = 0; j < names_.size(); j ++) {
            if (siteNames_[i].compare(names_[j]) == 0){
                isFound = true;
            }
        }
    }

    fclose(NewList);
    delete [] id;
    delete [] name;
}

C++ 代码

void converter::updateNewList(){
    std::ifstream NewList ("NewList.txt");

    while(NewList.good()){
        unsigned int id (0);
        std::string name;

        // get the ID and name
        NewList >> id >> name;

        // ignore the rest of the line
        NewList.ignore( std::numeric_limits<std::streamsize>::max(), '\n');

        info_.insert(std::pair<std::string, unsigned int>(name,id));

    }

    NewList.close();
}

更新:后续问题:Bottleneck from comparing strings 并感谢您提供的非常有用的帮助!我以后不会再犯这些错误了!

【问题讨论】:

  • 嗯,分析您的代码并查看热点在哪里?然后在这里问关于他们的具体问题?
  • 您是否配置调试二进制文件?无论你应该给我们实际的代码差异来寻找瓶颈。
  • 代码很丑。这是 C 和 C++ 的混合体。如果你用 C++ 编写,你应该使用 C++ 的东西。
  • 仅供参考,您的内存泄漏严重。您只是释放 id 和 name 的最后一个实例。在将数据放在 std::string 向量中的情况下,您可以在每次循环迭代结束时释放内存。对于 char* 向量,您需要迭代向量并删除每个条目。
  • std::fstream 代替 FILE*,std::stringstream 代替 sprintf。

标签: c++ string performance char


【解决方案1】:

我猜它应该与向量的性能相关

关于向量

std::vector 与内部连续数组一起工作,这意味着一旦数组已满,它需要创建另一个更大的数组,并将字符串一一复制,这意味着复制构造和破坏字符串内容相同,适得其反……

要轻松确认这一点,请使用std::vector&lt;std::string *&gt; 并查看性能是否存在差异。

如果是这种情况,他们可以做以下四件事之一:

  1. 如果您知道(或有一个好主意)向量的最终大小,请使用其方法 reserve() 在内部数组中保留足够的空间,以避免无用的重新分配。
  2. 使用std::deque,其工作方式几乎类似于矢量
  3. 使用std::list(不会让您随机访问其项目)
  4. 使用 std::vector

关于字符串

注意:我假设您的 strings\char * 被创建一次,并且没有被修改(通过 realloc、append 等)。

如果上面的想法还不够,那么……

字符串对象内部缓冲区的分配类似于 char * 的 malloc,因此您应该看到两者之间几乎没有差异。

现在,如果您的 char * 确实是 char[SOME_CONSTANT_SIZE],那么您避免使用 malloc(因此,它会比 std::string 更快)。

编辑

看了更新后的代码,看到以下问题。

  1. 如果 ids_ 和 names_ 是向量,如果你对行数有一点概念,那么你应该在 ids_ 和和 names_ 上使用reserve()
  2. 考虑制作 ids_ 和 names_ 双端队列或列表。
  3. faaNames_ 应该是一个 std::map,甚至是一个 std::unordered_map(或者你的编译器上的任何 hash_map)。您目前的搜索是两个 for 循环,这非常昂贵且效率低下。
  4. 在比较其内容之前,请考虑比较字符串的长度。在 C++ 中,字符串的长度(即 std::string::length())是零成本操作)
  5. 现在,我不知道你在用 isFound 变量做什么,但是如果你只需要找到一个真正的相等,那么我想你应该研究算法(我不知道是否已经一,见http://www.cplusplus.com/reference/algorithm/),但我相信只要考虑一下,这个搜索就会变得更有效率。

其他cmets:

  1. 忘记在 STL 中使用 int 来表示大小和长度。至少,使用size_t。在 64 位中, size_t 将变为 64 位,而 int 将保持 32 位,因此您的代码尚未准备好 64 位(另一方面,我看到很少有传入 8 个 Go 字符串的情况......但仍然,最好是正确的...)

编辑 2

这两个(所谓的 C 和 C++)代码是不同的。 “C 代码”需要长度小于 5 的 id 和名称,或者程序存在错误。 “C++ 代码”没有这样的限制。不过,如果您确认名称和 ID 始终少于 5 个字符,则此限制仍是大规模优化的基础。

【讨论】:

  • 假设这个问题与旧的 C++ 标准有关,而不是 C++0x 和已经实现移动语义的编译器。
  • +1 用于回答问题。是的,额外的 malloc/free 可能是他减速的原因。扔进初始化/复制字符串所需的 memcpy 以获得良好的度量。
  • Let_Me_Be:仍然需要创建要放入向量中的初始字符串。
  • @Torlack:初始字符串不需要任何成本:在 VC++ 2008 STL 实现中,任何小于 16(或 15?)个字符的字符串都直接嵌入到字符串类中,而不是用 @ 分配987654337@。这意味着这样的字符串(以及大多数“微不足道”的字符串都在这个大小范围内)的成本与堆栈 char[15] 一样多。
  • @Torlack:事实是“C 初始 char *”创建和放入向量的成本与“C++0x 初始 std::string”相同。尽管如此,我还是看不到 memcpy 在这里有什么帮助,因为 C++ 对象通常不是 memcpy-able (这是复制构造函数的范畴,实际上可能不太有效),但正如 Let_Me_Be 所写,C ++0x' std::move 是 C 的 memmove 的 C++ 等价物,但处理 C++ 对象并在代码中使用可以产生奇迹。
【解决方案2】:

在修复某些东西之前,请确保它是瓶颈。否则你就是在浪费时间。加上这种优化是微优化。如果您在 C++ 中进行微优化,请考虑使用裸 C。

【讨论】:

  • 如果您决定使用裸 C,stackoverflow.com/questions/2371292/… 可能会有所帮助。
  • 无需“全 C”。拥有std::vector&lt;char *&gt; 是一个很好的优化,如果这真的是瓶颈的话。
  • @paercebal 我真的怀疑它可能是瓶颈
  • 我的观点更多的是关于“全 C”的方式。如果您的闪亮汽车的悬架不够高效,则无需更换汽车。只需更改悬架。在 C++ 中,只需找到成为瓶颈的代码,用安全的接口包装它,然后隐藏在不太安全但更高效的代码中。例如,我编写了一个组件,其一个结构是瓶颈。该结构有一个 std::string 和一个 int,它被复制了很多。我把它变成了一个隐藏在 char[32] 和 int 中的类,并且复制方法使用了 memcpy。但对于组件的其余部分,它仍然是一个完整、安全的 C++ 对象。
【解决方案3】:

在开始填充之前将矢量调整为足够大的大小。或者,使用指向字符串的指针而不是字符串。

问题是每次自动调整向量大小时都会复制字符串。对于像指针这样的小对象,这几乎没有任何成本,但对于字符串,整个字符串会被完整复制。

并且id和name应该是string而不是char*,并且像这样初始化(假设你仍然使用string而不是string*):

id = string(lineBuffer, lineBuffer + l);
...
name = string(lineBuffer + s, lineBuffer + s + l);
...
ids_.push_back(id);
names_.push_back(name);

【讨论】:

  • 向量在添加字符串之前就在那里。他问为什么弦更慢。使用指向字符串的指针也不是那么好,因为字符串使用浅拷贝。
  • 好吧,不确定字符串复制是深还是浅
  • 我的错。现在 OP 添加了代码,我看到了很大的改进空间和减少内存泄漏。
  • @Dialectius 我将如何获得这些改进?
  • 我添加了一些代码。而不是 new char[],switch 和 sprintf 只是创建一个字符串。当您省略 new char[] 时,您还将省略当前代码显示的内存泄漏。在向量中使用指向字符串的指针而不是字符串可能仍然会加快速度。
【解决方案4】:

除了std::string,这是一个C程序。

尝试使用 fstream,并使用 profiler 来检测瓶颈。

【讨论】:

    【解决方案5】:

    您可以尝试reserve 多个vector 值,以减少分配次数(代价高昂),正如 Dialecticus 所说(可能来自古罗马?)。

    但有些事情可能值得观察:如何存储文件中的字符串,是否执行连接等...

    在 C 语言中,字符串(据说不存在——它们没有像 STL 这样的库中的容器)需要更多的工作来处理,但至少我们清楚地知道在处理它们时会发生什么。在 STL 中,每个方便 操作(意味着需要更少的程序员工作)实际上可能需要在 string 类中的大量操作,具体取决于您如何使用它。

    因此,虽然分配/释放是一个代价高昂的过程,但其余的逻辑,尤其是字符串过程,可能/应该也可以查看。

    【讨论】:

      【解决方案6】:

      我相信这里的主要问题是你的字符串版本复制了两次——首先是动态分配的char[],称为nameid,然后是std::strings,而你的vector&lt;char *&gt;版本可能不会那样做。为了使字符串版本更快,您需要直接读入字符串并去除所有冗余副本

      【讨论】:

      • 如果我想直接读入字符串,我必须使用ifstream。我不认为我可以使用 fget 来做到这一点?
      • 您可以从一个空白字符串开始,使用.resize() 设置大小/容量,然后如果您想要类似于char 尽可能缓冲。或者您可以创建一个写入字符串的输出迭代器,并将字符串本质上视为您写入的缓冲区。
      【解决方案7】:

      流会为您处理很多繁重的工作。停止自己做这一切,让图书馆帮助你:

      void converter::updateNewList(){
          std::ifstream NewList ("NewList.txt");
      
          while(NewList.good()){
              int id (0);
              std::string name;
      
              // get the ID and name
              NewList >> id >> name;
      
              // ignore the rest of the line
              NewList.ignore( numeric_limits<streamsize>::max(), '\n');
      
              ids_.push_back (id);
              names_.push_back(name);
          }
      
          NewList.close();
      }
      

      无需手动进行空格标记。

      此外,您可能会发现此站点是一个有用的参考: http://www.cplusplus.com/reference/iostream/ifstream/

      【讨论】:

      • 编辑:NewList.ignore( std::numeric_limits&lt;std::streamsize&gt;::max(), '\n');
      • +1 为了向我展示光明,是否有 ids_ 和 names_ 作为字符串指针?我经常访问它们,瓶颈是我的代码
      • 请注意,我将您的 IDS 更改为整数。将数字存储为字符串有点浪费。至于访问字符串的瓶颈......你能发布一些你用来做那件事的代码吗?如果你在做不必要的复制,那可能就是问题所在。
      • 从关于搜索的其他网站,是的,地图绝对是要走的路。这会改变这种加载方法,但不会改变太多。而不是 push_back 用于两个向量,而是使用 your_map[name] = id; 加载一次地图。此加载方法中的其余逻辑可以保持不变。
      【解决方案8】:

      您可以使用分析器找出您的代码在哪里消耗的时间最多。例如,如果您使用 gcc,则可以使用 -pg 编译程序。当您运行它时,它会将分析结果保存在一个文件中。您可以在二进制文件上运行 gprof 以获得人类可读的结果。一旦你知道大部分时间都花在了哪里,你就可以发布那段代码来回答更多问题。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-05-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-02-20
        • 1970-01-01
        • 2021-07-19
        • 1970-01-01
        相关资源
        最近更新 更多