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.
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.
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.
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.
string_view is a slice in an existing buffer, it does not require a memory allocation.
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 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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With