Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 523
uni Avatar asked Feb 01 '20 09:02

uni


People also ask

When should you use string_view?

string_view is useful when you want to avoid unnecessary copies. String_views are less memory-intensive to construct and copy. The creation of string_view from literals does not require a dynamic allocation.

What is std::string_view?

The std::string_view, from the C++17 standard, is a read-only non-owning reference to a char sequence. The motivation behind std::string_view is that it is quite common for functions to require a read-only reference to an std::string-like object where the exact type of the object does not matter.

What is string_view C++?

The string_view family of template specializations provides an efficient way to pass a read-only, exception-safe, non-owning handle to the character data of any string-like objects with the first element of the sequence at position zero.

Does string_view allocate memory?

string_view is a slice in an existing buffer, it does not require a memory allocation.


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

like image 127
Paul Evans Avatar answered Sep 28 '22 12:09

Paul Evans


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 probably precompute the length and the required buffer, 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).

like image 41
midor Avatar answered Sep 28 '22 13:09

midor