Làm thế nào chính xác là std :: string_view nhanh hơn const std :: string &?


221

std::string_viewđã đưa nó lên C ++ 17 và nó được khuyến khích sử dụng thay vì const std::string&.

Một trong những lý do là hiệu suất.

Ai đó có thể giải thích chính xác như thế nào std::string_viewlà / sẽ nhanh hơn const std::string&khi được sử dụng làm loại tham số không? (giả sử không có bản sao nào trong callee được tạo ra)


7
std::string_viewchỉ là một sự trừu tượng của cặp (char * started, char * end). Bạn sử dụng nó khi tạo một std::stringbản sao không cần thiết.
Câu hỏi

Theo tôi câu hỏi không phải là chính xác cái nào nhanh hơn, mà là khi nào nên sử dụng chúng. Nếu tôi cần một số thao tác trên chuỗi và nó không phải là vĩnh viễn và / hoặc giữ giá trị ban đầu, chuỗi_view là hoàn hảo vì tôi không cần phải tạo một bản sao của chuỗi cho nó. Nhưng nếu tôi chỉ cần kiểm tra một cái gì đó trên chuỗi bằng chuỗi :: find chẳng hạn, thì tham chiếu sẽ tốt hơn.
TheArquitect

@QuestionC bạn sử dụng nó khi bạn không muốn API của bạn để hạn chế để std::string(string_view thể chấp nhận mảng thô, vectơ, std::basic_string<>với allocators không mặc định vv vv vv Oh, và string_views khác rõ ràng)
sehe

Câu trả lời:


213

std::string_view là nhanh hơn trong một vài trường hợp.

Đầu tiên, std::string const&yêu cầu dữ liệu phải ở trong một std::string, chứ không phải mảng C thô, được char const*trả về bởi API C, std::vector<char>được tạo bởi một số công cụ khử lưu huỳnh, v.v. Việc chuyển đổi định dạng tránh sẽ tránh sao chép byte và (nếu chuỗi dài hơn SBO¹ cho việc std::stringthực hiện cụ thể ) tránh phân bổ bộ nhớ.

void foo( std::string_view bob ) {
  std::cout << bob << "\n";
}
int main(int argc, char const*const* argv) {
  foo( "This is a string long enough to avoid the std::string SBO" );
  if (argc > 1)
    foo( argv[1] );
}

Không có phân bổ được thực hiện trong string_viewtrường hợp, nhưng sẽ có nếu foolấy một std::string const&thay vì a string_view.

Lý do thực sự lớn thứ hai là nó cho phép làm việc với các chuỗi con mà không cần một bản sao. Giả sử bạn đang phân tích chuỗi json 2 gigabyte (!) ². Nếu bạn phân tích cú pháp đó std::string, mỗi nút phân tích cú pháp như vậy nơi họ lưu trữ tên hoặc giá trị của một nút sẽ sao chép dữ liệu gốc từ chuỗi 2 gb sang một nút cục bộ.

Thay vào đó, nếu bạn phân tích nó thành std::string_views, các nút tham chiếu đến dữ liệu gốc. Điều này có thể tiết kiệm hàng triệu phân bổ và giảm một nửa yêu cầu bộ nhớ trong khi phân tích cú pháp.

Việc tăng tốc bạn có thể nhận được chỉ đơn giản là vô lý.

Đây là một trường hợp cực đoan, nhưng các trường hợp "lấy một chuỗi con và làm việc với nó" khác cũng có thể tạo ra các tốc độ khá tốt với string_view.

Một phần quan trọng cho quyết định là những gì bạn mất bằng cách sử dụng std::string_view. Nó không nhiều, nhưng nó là một cái gì đó.

Bạn mất kết thúc null ẩn, và đó là về nó. Vì vậy, nếu cùng một chuỗi sẽ được chuyển đến 3 hàm, tất cả đều yêu cầu một bộ kết thúc null, chuyển đổi thành std::stringmột lần có thể là khôn ngoan. Do đó, nếu mã của bạn được biết là cần một bộ kết thúc null và bạn không mong đợi các chuỗi được cung cấp từ bộ đệm có nguồn gốc kiểu C hoặc tương tự, có thể mất một std::string const&. Nếu không thì lấy a std::string_view.

Nếu std::string_viewcó một cờ cho biết nếu nó bị hủy kết thúc (hoặc một cái gì đó lạ hơn) thì nó sẽ xóa ngay cả lý do cuối cùng để sử dụng a std::string const&.

Có một trường hợp lấy std::stringkhông có const&là tối ưu hơn a std::string_view. Nếu bạn cần sở hữu một bản sao của chuỗi vô thời hạn sau cuộc gọi, lấy theo giá trị là hiệu quả. Bạn sẽ ở trong trường hợp SBO (và không phân bổ, chỉ cần một vài bản sao ký tự để sao chép nó) hoặc bạn sẽ có thể di chuyển bộ đệm được phân bổ heap thành cục bộ std::string. Có hai lần quá tải std::string&&std::string_viewcó thể nhanh hơn, nhưng chỉ một chút, và nó sẽ gây ra sự phình to mã khiêm tốn (có thể khiến bạn mất tất cả các mức tăng tốc độ).


Optimization Tối ưu hóa bộ đệm nhỏ

² Trường hợp sử dụng thực tế.


8
Bạn cũng mất quyền sở hữu. Điều này chỉ đáng quan tâm nếu chuỗi được trả về và nó có thể là bất cứ thứ gì ngoài chuỗi con của bộ đệm được đảm bảo tồn tại đủ lâu. Trên thực tế, mất quyền sở hữu là một vũ khí rất hai lưỡi.
Ded repeatator

SBO nghe lạ. Tôi đã luôn nghe SSO (tối ưu hóa chuỗi nhỏ)
phuclv

@phu Chắc chắn rồi; nhưng chuỗi không phải là điều duy nhất bạn sử dụng thủ thuật trên.
Yakk - Adam Nevraumont

@phuclv SSO chỉ là một trường hợp cụ thể của SBO, viết tắt của tối ưu hóa bộ đệm nhỏ . Điều khoản thay thế là opt dữ liệu nhỏ. , đối tượng nhỏ opt. , hoặc chọn kích thước nhỏ. .
Daniel Langr

59

Một cách mà string_view cải thiện hiệu suất là nó cho phép loại bỏ các tiền tố và hậu tố một cách dễ dàng. Trong phần mở rộng, string_view chỉ có thể thêm kích thước tiền tố vào một con trỏ vào một số bộ đệm chuỗi hoặc trừ kích thước hậu tố từ bộ đếm byte, điều này thường nhanh. Mặt khác, std :: chuỗi phải sao chép các byte của nó khi bạn làm một cái gì đó như lớp nền (theo cách này bạn có được một chuỗi mới sở hữu bộ đệm của nó, nhưng trong nhiều trường hợp bạn chỉ muốn lấy một phần của chuỗi gốc mà không cần sao chép). Thí dụ:

std::string str{"foobar"};
auto bar = str.substr(3);
assert(bar == "bar");

Với std :: string_view:

std::string str{"foobar"};
std::string_view bar{str.c_str(), str.size()};
bar.remove_prefix(3);
assert(bar == "bar");

Cập nhật:

Tôi đã viết một điểm chuẩn rất đơn giản để thêm một số số thực. Tôi đã sử dụng thư viện điểm chuẩn google tuyệt vời . Các chức năng điểm chuẩn là:

string remove_prefix(const string &str) {
  return str.substr(3);
}
string_view remove_prefix(string_view str) {
  str.remove_prefix(3);
  return str;
}
static void BM_remove_prefix_string(benchmark::State& state) {                
  std::string example{"asfaghdfgsghasfasg3423rfgasdg"};
  while (state.KeepRunning()) {
    auto res = remove_prefix(example);
    // auto res = remove_prefix(string_view(example)); for string_view
    if (res != "aghdfgsghasfasg3423rfgasdg") {
      throw std::runtime_error("bad op");
    }
  }
}
// BM_remove_prefix_string_view is similar, I skipped it to keep the post short

Các kết quả

(x86_64 linux, gcc 6.2, " -O3 -DNDEBUG"):

Benchmark                             Time           CPU Iterations
-------------------------------------------------------------------
BM_remove_prefix_string              90 ns         90 ns    7740626
BM_remove_prefix_string_view          6 ns          6 ns  120468514

2
Thật tuyệt khi bạn cung cấp một điểm chuẩn thực tế. Điều này thực sự cho thấy những gì có thể đạt được trong các trường hợp sử dụng có liên quan.
Daniel Kamil Kozar

1
@DanielKamilKozar Cảm ơn bạn đã phản hồi. Tôi cũng nghĩ rằng điểm chuẩn là có giá trị, đôi khi chúng thay đổi mọi thứ.
Pavel Davydov

47

Có 2 lý do chính:

  • string_view là một lát trong bộ đệm hiện có, nó không yêu cầu cấp phát bộ nhớ
  • string_view được thông qua bởi giá trị, không phải bởi tham chiếu

Những lợi thế của việc có một lát là nhiều:

  • bạn có thể sử dụng nó với char const* hoặc char[]không phân bổ bộ đệm mới
  • Bạn có thể lấy nhiều lát và các phần phụ vào một bộ đệm hiện có mà không cần phân bổ
  • chuỗi con là O (1), không phải O (N)
  • ...

Hiệu suất tốt hơn và phù hợp hơn trên tất cả.


Truyền theo giá trị cũng có lợi thế hơn so với chuyển qua tham chiếu, vì răng cưa.

Cụ thể, khi bạn có một std::string const& tham số, không có gì đảm bảo rằng chuỗi tham chiếu sẽ không bị sửa đổi. Kết quả là, trình biên dịch phải tìm nạp lại nội dung của chuỗi sau mỗi cuộc gọi vào một phương thức mờ (con trỏ tới dữ liệu, độ dài, ...).

Mặt khác, khi truyền một string_viewgiá trị, trình biên dịch có thể xác định tĩnh rằng không có mã nào khác có thể sửa đổi độ dài và con trỏ dữ liệu bây giờ trên ngăn xếp (hoặc trong các thanh ghi). Kết quả là, nó có thể "lưu trữ" chúng qua các lệnh gọi hàm.


36

Một điều nó có thể làm là tránh xây dựng một std::stringđối tượng trong trường hợp chuyển đổi ngầm định từ một chuỗi kết thúc null:

void foo(const std::string& s);

...

foo("hello, world!"); // std::string object created, possible dynamic allocation.
char msg[] = "good morning!";
foo(msg); // std::string object created, possible dynamic allocation.

12
Có thể nói rằng const std::string str{"goodbye!"}; foo(str);có lẽ sẽ không nhanh hơn với chuỗi_view so với chuỗi &
Martin Bonner hỗ trợ Monica

1
Wont string_viewthể chậm vì nó có để sao chép hai con trỏ như trái ngược với một con trỏ trong const string&?
balki

9

std::string_viewvề cơ bản chỉ là một bao bọc xung quanh a const char*. Và vượt qua const char*có nghĩa là sẽ có một con trỏ nhỏ hơn trong hệ thống so với vượt qua const string*(hoặc const string&), bởi vì string*ngụ ý gì đó như:

string* -> char* -> char[]
           |   string    |

Rõ ràng với mục đích truyền các đối số const, con trỏ đầu tiên là không cần thiết.

ps Một điểm khác biệt về tài chính giữa std::string_viewconst char*, tuy nhiên, là chuỗi_view không bắt buộc phải kết thúc bằng null (chúng có kích thước tích hợp) và điều này cho phép ghép chuỗi ngẫu nhiên tại chỗ một cách ngẫu nhiên.


4
Những gì với downvote? std::string_views chỉ là const char*s ưa thích , thời gian. GCC thực hiện chúng như thế này:class basic_string_view {const _CharT* _M_str; size_t _M_len;}
n.caillou

4
chỉ cần đạt tới 65 nghìn đại diện (từ 65 hiện tại của bạn) và đây sẽ là câu trả lời được chấp nhận (sóng tới đám đông sùng bái hàng hóa) :)
mlvljr

7
@mlvljr Không ai qua được std::string const*. Và sơ đồ đó là không thể hiểu được. @ n.caillou: Nhận xét của bạn đã chính xác hơn câu trả lời. Điều đó làm cho string_viewnhiều hơn "ưa thích char const*" - nó thực sự khá rõ ràng.
sehe

@sehe Tôi có thể là không ai, không có vấn đề gì (tức là chuyển một con trỏ (hoặc tham chiếu) đến một chuỗi const, tại sao không?) :)
mlvljr

2
@sehe Bạn có hiểu rằng từ góc độ tối ưu hóa hoặc thực thi, std::string const*std::string const&giống nhau không?
n.caillou
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.