为什么std :: string_view比const char *更快?

8

我是否在测量其他内容?

这段代码中有一个标签堆栈(整数)。每个标签都有一个字符串表示形式(const char*std::string_view)。 在循环中,堆栈值被转换为相应的字符串值。这些值被追加到预分配的字符串或者赋值给数组元素。

结果显示,使用std::string_view的版本比使用const char*的版本稍微快一些。

代码:

#include <array>
#include <iostream>
#include <chrono>
#include <stack>
#include <string_view>

using namespace std;

int main()
{
    enum Tag : int { TAG_A, TAG_B, TAG_C, TAG_D, TAG_E, TAG_F };
    constexpr const char* tag_value[] = 
        { "AAA", "BBB", "CCC", "DDD", "EEE", "FFF" };
    constexpr std::string_view tag_values[] =
        { "AAA", "BBB", "CCC", "DDD", "EEE", "FFF" };

    const size_t iterations = 10000;
    std::stack<Tag> stack_tag;
    std::string out;
    std::chrono::steady_clock::time_point begin;
    std::chrono::steady_clock::time_point end;

    auto prepareForBecnhmark = [&stack_tag, &out](){
        for(size_t i=0; i<iterations; i++)
            stack_tag.push(static_cast<Tag>(i%6));
        out.clear();
        out.reserve(iterations*10);
    };

// Append to string
    prepareForBecnhmark();
    begin = std::chrono::steady_clock::now();
    for(size_t i=0; i<iterations; i++) {
        out.append(tag_value[stack_tag.top()]);
        stack_tag.pop();
    }
    end = std::chrono::steady_clock::now();
    std::cout << out[100] << "append string const char* = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;

    prepareForBecnhmark();
    begin = std::chrono::steady_clock::now();
    for(size_t i=0; i<iterations; i++) {
        out.append(tag_values[stack_tag.top()]);
        stack_tag.pop();
    }
    end = std::chrono::steady_clock::now();
    std::cout << out[100] << "append string string_view= " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;

// Add to array
    prepareForBecnhmark();
    std::array<const char*, iterations> cca;
    begin = std::chrono::steady_clock::now();
    for(size_t i=0; i<iterations; i++) {
        cca[i] = tag_value[stack_tag.top()];
        stack_tag.pop();
    }
    end = std::chrono::steady_clock::now();
    std::cout << "fill array const char* = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;

    prepareForBecnhmark();
    std::array<std::string_view, iterations> ccsv;
    begin = std::chrono::steady_clock::now();
    for(size_t i=0; i<iterations; i++) {
        ccsv[i] = tag_values[stack_tag.top()];
        stack_tag.pop();
    }
    end = std::chrono::steady_clock::now();
    std::cout << "fill array string_view = " << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << "[µs]" << std::endl;
    std::cout << ccsv[ccsv.size()-1] << cca[cca.size()-1] << std::endl;

    return 0;
}

我的电脑上的结果是:

Aappend string const char* = 97[µs]
Aappend string string_view= 72[µs]
fill array const char* = 35[µs]
fill array string_view = 18[µs]

Godbolt编译器资源管理器链接:https://godbolt.org/z/SMrevx

更新:更精确的基准测试结果(500次运行300000次迭代):

Caverage append string const char* = 2636[µs]
Caverage append string string_view= 2096[µs]
average fill array const char* = 526[µs]
average fill array string_view = 568[µs]

Godbolt链接:https://godbolt.org/z/aU7zL_

所以在第二种情况下,使用const char*如预期的一样更快。而第一种情况已经在答案中解释过了。


1
从反汇编来看,似乎const char*变量在运行时调用了strlen。我认为编译器可以在编译时为string_view执行此操作。 - n314159
2
这里没有理由使用 std::endl;你可以将 \n 作为要打印的字符串的一部分。打印在定时区域之外,因此刷新是否发生都应该没问题,但如果输出将被发送到管道,则 cout / stdio 缓冲区可能足够大,直到结束才需要刷新。在每个定时段之间不进行系统调用是一件好事。 - Peter Cordes
3个回答

18

使用std::string_view的原因很简单,因为你可以传递长度,而且每次想要一个新字符串时不需要插入空字符。而char*必须每次都搜寻结尾,如果要获取子串,可能需要复制并在子串末尾添加空字符。


我猜分支预测/缓存预测单元也会喜欢这种方法。使用长度而不是终止符,让CPU能够准确预测需要执行多少次获取。必须在获取到终止符之前去获取它,而这样一来就无法确定是否为终止符了。 - Tiger4Hire
3
@uni:如果您先对另一个进行基准测试,这些数字会改变吗?您的总基准测试非常快速,以至于CPU可能刚刚在那时达到最大涡轮增压。或者第一个数组的页面错误成本比第二个数组更高。简而言之,您结果中的这部分可能是由于天真的微基准测试方法造成的。 - Peter Cordes
1
@uni:或许这是真的;我尝试反转它们并在Godbolt上运行,结果显示使用fill array string_view = 19[µs],而const char*则为61[µs]。循环基本相当,假设它们不会掉到调用“operator delete”的部分。(当然,string-view对象宽16字节,并且使用movdqa / movaps进行复制)。我不知道,必须使用性能计数器在本地尝试,或者单步执行以查看是否发生删除调用。增加迭代次数可以减少差异比率:https://godbolt.org/z/jvM8Cr - Peter Cordes
1
@PeterCordes 我按照你的建议增加了迭代次数和基准运行次数,以获得500次运行的时间平均值。以下是结果:迭代次数 10000 50000 100000 200000 300000 string_view 17 87 183 368 588 const char* 17 88 177 353 526 delta 0 -1 6 15 62在这种情况下,差异微不足道,const char*现在更快了。 - uni
1
@uni:这更有意义。对于大的迭代计数,const char* 可能更快,因为它更小,而这些大的大小意味着更大的数组,开始出现 L1 或甚至 L2 缓存未命中。(现代 x86-64 可以像 16 字节对象一样快速地复制 8 字节对象,特别是当它们对齐时。)仍然不确定没有重复循环会减慢小尺寸的速度。 - Peter Cordes
显示剩余2条评论

8

std::string_view在实际应用中的含义可以概括为:

{
  const char* __data_;
  size_t __size_;
}

标准实际上在第24.4.2节中指定了这是一个指针和大小。它还指定了某些操作如何与字符串视图一起工作。最重要的是,每当您与std::string交互时,您将调用还需要大小作为输入的重载。因此,当您调用append时,这会简化为两个不同的调用:str.append(sv)转换为str.append(sv.data(), sv.size())

显着的区别在于,现在您知道append之后字符串的大小,这意味着您也知道是否需要重新分配内部缓冲区,以及您需要使其多大。如果您事先不知道大小,您可以开始复制,但是std::stringappend提供强保证,因此出于实际目的,大多数库都预先计算char *重载中的长度,尽管从技术上讲,如果您没有成功完成操作,只要记住旧大小并清除之后的所有内容即可,但这种方法可能仅适用于字符串的局部优化,因为销毁是微不足道的。


3
可能是由于string_view具有字符串值的大小,而"const char*"没有关于大小的信息,必须定义它。

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