uni
uni

Reputation: 611

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.

Upvotes: 10

Views: 8109

Answers (3)

midor
midor

Reputation: 5557

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

Upvotes: 9

Paul Evans
Paul Evans

Reputation: 27577

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.

Upvotes: 20

Evgeny
Evgeny

Reputation: 1072

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.

Upvotes: 3

Related Questions