简体   繁体   中英

Why is std::string_view faster than const char*?

Or am I measuring something else?

In this code I have a stack of tags ( integers ). Each tag has a string representation ( const char* or std::string_view ). In the loop stack values are converted to the corresponding string values. Those values are appended to a preallocated string or assigned to an array element.

The results show that the version with std::string_view is slightly faster than the version with const char* .

Code:

#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;
}

Results on my machine are:

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 compiler explorer url: https://godbolt.org/z/SMrevx

UPD: Results after more accurate benchmarking (500 runs 300000 iterations):

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 url: https://godbolt.org/z/aU7zL_

So in the second case const char* is faster as expected. And the first case was explained in the answers.

Simply because with std::string_view you're passed the length and you don't have to insert a null char whenever you want a new string. char* has to search for the end everytime and if you want a substring you'll probably have to copy as you'll need a null char at the end of the substring.

std::string_view for practical purposes boils down to:

{
  const char* __data_;
  size_t __size_;
}

The standard actually specifies in sec. 24.4.2 that this is a pointer and size. It also specifies how certain operations work with string view. Most notably whenever you interact with std::string you will call the overload that also takes the size as input. Hence when you call append, this boils down to two different calls: str.append(sv) translates to str.append(sv.data(), sv.size()) .

The significant difference is that you now know the size of the string after the append , which means you also know whether you will have to reallocate your internal buffer, and how big you have to make it. If you don't know the size up-front you could start copying, but std::string gives the strong guarantee for append , so for practical purposes most libraries precompute the length in the char* overload , although technically it would also be possible to just remember the old-size and erase everything after if you don't finish successfully (doubt anyone does that, although it might be a local optimization for strings since destruction is trivial).

It may be due to string_view has the size of string value. The "const char*" hasn't information about size and has to define it.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM