【问题标题】:std::string performance for handling short stringsstd::string 处理短字符串的性能
【发布时间】:2014-12-24 06:55:17
【问题描述】:

Bjarne Stroustrup 和其他专家表示,在处理 Bjarne Stroustrup' articlemy previous question 中的短字符串时,C++ 比 C 更快

但在我的测试中,C++C 慢了大约 110%。

g++ 版本是 4.4.6(在 CentOS 6.3 上运行)。这是因为g++ 4.4.6 少了Rvalue Reference (move semantics)之类的c++11特性吗?

测试结果

$ time a.out input_file 的输出减去不调用 compose_X() 函数的执行时间

  • cpp 版本:0.192 秒
  • C 版本:0.091 秒

源代码

-O2编译

编辑

compose_cpp()compose_p() 来自 Bjarne 的文章。他说compose_cpp()compose_p()胖。我想通过实际测试来验证这个事实。

如果我测试的方式不对,我该如何改进测试?

#include <iostream>
#include <fstream>

#include <cstdlib>
#include <cstring>

std::string compose_cpp(const std::string& name, const std::string& domain)
{
    return name + '@' + domain;
}

char* compose_c(const char* name, const char* domain)
{
    char* res = (char*) malloc(strlen(name)+strlen(domain)+2);
    char* p = strcpy(res,name);

    p += strlen(name);
    *p = '@';
    strcpy(p+1,domain);

    return res;
}

int main(int argc, char* argv[])
{
    std::ifstream ifs;
    ifs.open(argv[1]);

    std::string email, domain;

    while (ifs.good())
    {
        ifs >> email;
        ifs >> domain;

        // std::string composed = compose_cpp(email, domain);

        char* composed = compose_c(email.c_str(), domain.c_str());
        free(composed);
    }

    ifs.close();
}

输入文件

输入文件的长度为 1 百万行。每行小于20字节,随机生成。

$ head -n 10 input.txt.1m
9742720 1981857.com
22504 4127435.com
342760 69167.com
53075 26710.com
3837481 1851920.com
98441 278536.com
4503887 9588108.com
193947 90885.com
42603 8166125.com
3587671 297296.com

【问题讨论】:

  • 你也试过-O3吗?
  • 你最近在 5 个关于 C++ 的神话 中看到了这个。
  • 0.402、0.299 分别用于 c++、c。
  • @InoSHeo 你测量的怎么样?
  • @InoSHeo 我之前问过“你是如何测量的”,你似乎忽略了我的问题。这对于其他复制相同结果的人来说是不可或缺的。

标签: c++


【解决方案1】:

我只是在这里猜测一下,因为我没有要测试的数据文件。我认为您的结果可能与 Stroustrup 的预期不符,因为他在这里所说的:

是的,C++ 版本,因为它不需要计算参数字符并且不使用空闲存储(动态内存)存储短参数字符串。

但是,我的理解是libstdc++ 对所有字符串(零长度字符串除外)使用动态内存。请参阅这个最近的 SO 回答关于 libstdc++std::string 对象的小尺寸的问题:https://stackoverflow.com/a/27631366/12711

使用短字符串优化的实现可能会获得更好的结果(例如 MSVC - 我不确定 clang 的 libc++ 是否使用它)。

【讨论】:

  • 我想补充一点,短字符串优化使长字符串性能更差(导致分支错误预测,并且当字符串较长时使数据局部性更差)。
  • 另外我认为 MSVC 对少于 16 个字符的字符串使用了短字符串优化,因此在 C++ 情况下您的测试可能仍会遇到内存分配开销。
  • 如果我使用 MSVC 进行测试,我会记住 16 个字符。谢谢。
  • @DietrichEpp:你确定吗?我认为动态分配将完全支配任何分支预测命中(这在很大程度上应该只在内存可能被分配或释放时发生),并且就数据局部性而言 - 无论是否使用短字符串优化,长字符串将存储在堆上的某个位置,因此位置命中将是相同的。
  • 在 libc++ std::string 实现中,一个字符串是三个单词。一个字节将短字与长字区分开来,当您想要访问字符数据时,必须进行检查。如果一些字符串短而一些长,则会出现分支错误预测。你说缓存位置不是问题是对的,但这是出于不同的原因——不管你是否使用短字符串优化,字符串都是三个单词。
【解决方案2】:

我冒昧地将您的测试程序扩展了一点。特别是,我添加了代码让它在内部而不是依赖于外部文件生成数据,添加了计时代码来隔离有问题的字符串处理,并让它在同一运行中同时执行 C++ 和 C 版本的字符串操作,所以它立即产生了我可以比较的结果。这给了我以下代码:

#include <iostream>
#include <fstream>
#include <vector>
#include <string>

#include <cstdlib>
#include <cstring>

#include <ctime>

char* compose_c(const char* name, const char* domain)
{
    char* res = (char*) malloc(strlen(name)+strlen(domain)+2);
    char* p = strcpy(res,name);

    p += strlen(name);
    *p = '@';
    strcpy(p+1,domain);

    return res;
}

std::string rand_string(int size){
    std::string ret;

    for (int i = 0; i < size; i++)
        ret.push_back(rand() % 10 + '0');
    return ret;
}

struct address {
    std::string email, domain;

    address() : email(rand_string(5)), domain(rand_string(4) + ".com") { }
};

struct composed {
    std::string addr;
    composed(address const &a) : addr(a.email + "@" + a.domain) {}
};

void report(clock_t d, std::string const &label){
    std::cout << double(d) / CLOCKS_PER_SEC << " seconds for " << label << "\n";
}

int main(int argc, char **argv) {
    static const int NUM = 1024 * 1024;

    std::vector<address> addresses(NUM);

    clock_t start = clock();
    {
        std::vector<composed> c{ addresses.begin(), addresses.end() };
    }
    report(clock() - start, "C++");

    std::vector<char *> c_results(addresses.size());

    clock_t start_c = clock();
    for (int i = 0; i < addresses.size(); i++)
        c_results[i] = compose_c(addresses[i].email.c_str(), addresses[i].domain.c_str());
    for (char *c : c_results)
        free(c);
    report(clock() - start_c, "C");
}

然后我使用 VC++ 2013、x64 使用以下标志编译它:-O2b2 -GL -EHsc。当我运行它时,我得到的结果是:

0.071 seconds for C++
0.12 seconds for C

虽然运行与运行之间存在一些变化,但这些都是相当具有代表性的结果——C 代码所用的时间几乎(但不完全)是 C++ 代码的两倍。

请注意,尽管我实际上给了 C 版本一些不公平的优势。 C++ 代码的时间不仅包括执行字符串操作的时间,还包括创建和销毁向量以保存结果的时间。对于 C 代码,我提前准备了向量,然后为创建和销毁字符串本身的代码计时。

为了确保这个结果不是侥幸,我还尝试更改一些自变量。例如,将我们组成的地址字符串的数量增加 10 倍会增加总时间,但对比率几乎没有影响:

0.714 seconds for C++
1.206 seconds for C

同样,更改顺序以使 C 代码先运行,然后 C++ 代码运行后没有明显效果。

我想我应该补充一下:事实上,这段代码实际上并没有像原来那样使用compose_cpp 函数,而是选择将该功能合并到composed 的构造函数中。为了完整起见,我确实写了一个使用compose_cpp的版本,如下所示:

std::vector<std::string> composed;
composed.reserve(NUM);

clock_t start = clock();

for (auto const &a : addresses)
    composed.push_back(compose_cpp(a.email, a.domain));

这实际上稍微改善了时序,但我猜这主要是由于将时间从时序代码中移出创建向量本身,并且没有足够大非常关心的区别:

0.631 seconds for C++
1.21 seconds for C

这些结果在很大程度上取决于标准库的实现——具体来说,std::string 实现了短字符串优化这一事实。在相同的硬件上运行相同的代码,但使用缺乏这种优化的实现(在我的例子中是 gcc 4.9.1 的 nuwen MinGW 发行版)给出了相当不同的结果:

2.689 seconds for C++
1.131 seconds for C

在这种情况下,C 代码比来自 VC++ 的代码快一点,但 C++ 代码的速度慢了大约 4 倍。我尝试了一些不同的编译器标志(-O2 与 -O3 等)但它们的影响很小——对于本次测试,缺乏短字符串优化显然主导了其他因素。

底线:我认为这证实了 C++ 代码可以比 C 代码快得多,但是实现这种速度更多地取决于实现的质量。如果实现未能提供短字符串优化,C++ 代码很容易比 C 版本慢 2 倍而不是快 2 倍。

【讨论】:

  • 感谢您分享各种测试结果!圣诞快乐;)
  • 那么,我在this gist 中的测试显示VC13 的C 版本比它的C++ 版本快9 个字符的短字符串有什么“错误”?
  • 我不知道是否仍然如此,但是我上次使用 msvc 时使用了经过检查的 STL(这是使用 vc++2010,所以情况可能会有所不同)。在这种情况下,C++ 代码包括边界检查和其他健全性检查,但 C 代码不包括。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-08
  • 2016-01-21
  • 2013-01-23
  • 1970-01-01
  • 2011-06-30
  • 2017-06-06
相关资源
最近更新 更多