C++中Char*和String的速度对比

4
我有一个C++程序,它将从二进制文件中读取数据,最初我将数据存储在std::vector<char*> data中。现在我改变了代码,使用字符串代替char*,所以使用std::vector<std::string> data。例如,我必须从strcmp更改为compare
然而,我发现执行时间大大增加。对于一个样本文件,当我使用char*时,Linux机器上需要0.38秒,转换为字符串后需要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();
}

更新:后续问题:比较字符串的瓶颈,感谢非常有用的帮助!我以后不会再犯这些错误了!


7
对代码进行性能剖析,找出热点所在,然后在这里提出具体问题? - Oliver Charlesworth
2
你会对调试二进制文件进行分析吗?无论如何,你应该给我们实际的代码差异,以便查找瓶颈。 - Keynslug
6
代码很丑陋。这是C和C++的混合物。如果你用C++写,就应该使用C++的特性。 - Andrey
1
只是提醒一下,你的内存泄漏非常严重。你只释放了id和name的最后一个实例。如果将数据放入std::string向量中,可以在每次循环迭代结束时释放内存。如果是char*向量,则需要迭代向量并删除每个条目。 - Torlack
2
在编程中,我会想到使用std::fstream代替FILE*,以及使用std::stringstream代替sprintf。 - luke
显示剩余15条评论
8个回答

7

我的猜测是它应该与vector的性能有关

关于vector

std::vector 使用内部连续数组,意味着一旦数组已满,它就需要创建另一个更大的数组,并逐个复制字符串,这意味着复制构造和销毁具有相同内容的字符串,这是低效的...

为了轻松确认这一点,可以使用 std::vector<std::string *> 并查看性能是否有差异。

如果是这种情况,则可以执行以下四件事之一:

  1. 如果您知道(或有一个好的想法)向量的最终大小,请使用其方法reserve()在内部数组中预留足够的空间,以避免无用的重新分配。
  2. 使用std::deque,它几乎像一个向量一样工作。
  3. 使用std::list(它不会为其项提供随机访问)
  4. 使用std::vector<char *>

关于字符串

注意:我假设您的字符串\char *只创建一次,并且不进行修改(例如通过realloc、append等)。

字符串对象的内部缓冲区分配类似于malloc一个char *,因此您应该看到两者之间很少或没有区别。

现在,如果你的char *确实是char[SOME_CONSTANT_SIZE],那么你就可以避免使用malloc(因此,速度比std::string快)。

编辑

阅读更新后的代码后,我发现以下问题。
  1. 如果ids_和names_是向量,并且你有最少的行数的想法,那么你应该在ids_和names_上使用reserve()
  2. 考虑将ids_和names_制作成deque或列表。
  3. faaNames_应该是一个std::map,甚至是一个std::unordered_map(或者你的编译器上有任何hash_map)。你目前的搜索是两个for循环,这是相当昂贵和低效的。
  4. 在比较其内容之前,请考虑比较字符串的长度。在C++中,字符串的长度(即std::string::length())是一个零成本操作。
  5. 现在,我不知道你正在做什么isFound变量,但如果你只需要找到一个真正的等式,那么我想你应该在算法上工作(我不知道是否已经有一个,参见http://www.cplusplus.com/reference/algorithm/),但我相信这个搜索可以通过思考而更加高效。

其他评论:

  1. 不要在STL中使用int表示大小和长度,至少要使用size_t。在64位系统中,size_t将变成64位,而int仍然是32位,因此你的代码无法支持64位(尽管我很少见到8GB字符串...但还是最好正确处理...)

编辑2

这两个(所谓的C和C ++)代码是不同的。“C代码”预期ID和名称长度小于5,否则程序将出现错误。“C++代码”没有这样的限制。如果确认名称和ID始终小于5个字符,则此限制是进行大规模优化的基础。


3
假设这个问题是出在旧版的C++标准上,而不是C++0x,并且编译器已经实现了移动语义。 - Šimon Tóth
+1 为回答问题点赞。是的,额外的 malloc/free 可能是他减速的源头。再加上必要的 memcpy 来初始化/复制字符串。 - Torlack
Let_Me_Be: 还有创建放入向量的初始字符串的成本。 - Torlack
@Paercebal,您可以假设名称和ID始终少于5个字符。 - Elpezmuerto
我试图使用std::vector<std::string *>,但是失败了。为什么声明std::vector<std::string*> names_然后 names_.push_back(line.substr(0,l))或者names_.push_back(*line.substr(0,l))不起作用呢? - Elpezmuerto
显示剩余5条评论

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

在添加字符串之前,向量已经存在。他问为什么字符串更慢。使用指向字符串的指针也不会提高速度,因为字符串使用浅拷贝。 - Andrey
不确定字符串复制是深拷贝还是浅拷贝。 - Andrey
我的错。现在看到 OP 添加了代码,我发现有很多改进的空间和减少内存泄漏的方法。 - Dialecticus
@Dialectius,我该如何获得这些改进? - Elpezmuerto
我添加了一些代码。不要使用new char[]、switch和sprintf,而是创建一个字符串。当你省略new char[]时,也会省略当前代码所表现出的内存泄漏。并且,使用指向字符串的指针来代替字符串放在向量中可能仍然是正确的,可以加快速度。 - Dialecticus

3

在修复问题之前,请确保它是瓶颈。否则你会浪费时间。此类优化属于微观优化。如果你正在使用C++进行微观优化,请考虑使用裸露的C。


如果您决定使用裸C,https://dev59.com/pnE95IYBdhLWcg3wSsCD 可能会有所帮助。 - N 1.1
1
不需要“全C”。如果这真的是瓶颈,拥有一个std::vector<char *>是一个很好的优化。 - paercebal
@paercebal 我真的怀疑它会成为瓶颈。 - Andrey
我的观点更多地涉及“全C”的方式。如果你闪亮的汽车的悬挂系统不够高效,就没必要换车,只需更换悬挂系统。在C++中,只需找到瓶颈代码,用安全接口包装它,并隐藏在不那么安全但更高效的代码中。例如,我编写了一个组件,其中一个结构体是瓶颈。该结构体有一个std::string和一个int,并且被频繁复制。我将其变成了一个类,隐藏在char[32]和int中,并且复制方法使用了memcpy。但对于组件的其余部分,它仍然是一个完整、安全的C++对象。 - paercebal

2

流处理可以帮你完成很多繁重的工作。不要再自己全部搞定,让库来帮助你:

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<std::streamsize>::max(), '\n'); 的翻译如下: - Elpezmuerto
+1 给我指点迷津,有没有可能将ids_和names_作为字符串指针?我经常访问它们,这可能是我的代码瓶颈。 - Elpezmuerto
请注意,我将您的IDS更改为整数。将数字存储为字符串有些浪费。至于访问字符串的瓶颈...您能否发布一些用于执行此操作的代码?如果您进行了不必要的复制,那可能就是问题所在。 - Tim
从那个关于搜索的网站上来看,使用地图绝对是正确的方法。这将改变加载方法,但不会太多。只需使用your_map[name] = id;一次加载地图,而不是为两个向量使用push_back。此加载方法中的其余逻辑可以保持不变。 - Tim

2

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

尝试使用fstream,并使用分析器检测瓶颈。


2
你可以尝试使用 reserve 函数来预留一定数量的 vector 值,以减少分配(这是昂贵的),正如 Dialecticus 所说(可能来自古罗马?)。
但是需要注意的是:你如何存储文件中的字符串,是否执行字符串连接等等。
在 C 中,字符串(实际上不存在 - 它们没有像 STL 那样的库容器)需要更多的处理工作,但至少我们清楚地知道了处理它们时发生了什么。在 STL 中,每个 方便的 操作(意味着需要程序员投入更少的工作)实际上可能需要很多操作,在 string 类内部进行,具体取决于你的使用方式。
因此,虽然分配/释放是一个昂贵的过程,但其他逻辑,特别是字符串处理,也可能/应该被看作是需要进行优化的。

2

我认为这里的主要问题是你的字符串版本将东西复制了两次——首先是动态分配的名为nameidchar[]中,然后再复制到std::string中,而你的vector<char *>版本可能不会这样做。为了使字符串版本更快,你需要直接读取字符串并消除所有冗余的副本。


如果我想直接读取字符串,我必须使用 ifstream。我认为我不能使用 fget 做到这一点? - Elpezmuerto
你可以从一个空字符串开始,使用.resize()设置大小/容量,然后通过重载的operator[]直接写入字符串,如果你想要一个尽可能像char缓冲区的东西。或者你可以创建一个输出迭代器,将其写入字符串,并将字符串视为一个缓冲区进行写入。 - Chris Dodd

1
你可以使用性能分析器来找出代码消耗时间最多的地方。例如,如果你正在使用gcc编译器,可以使用-pg选项编译程序。运行程序后,它会将性能分析结果保存在一个文件中。然后你可以运行gprof命令对二进制文件进行分析,得到易于阅读的结果。一旦你知道了哪些代码消耗了大量时间,就可以将这部分代码发布出来以便进一步提问。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接