Nối chuỗi hiệu quả trong C ++


108

Tôi đã nghe một vài người bày tỏ lo lắng về toán tử "+" trong std :: string và các cách giải quyết khác nhau để tăng tốc độ nối. Có cái nào thực sự cần thiết không? Nếu vậy, cách tốt nhất để nối các chuỗi trong C ++ là gì?


13
Về cơ bản, dấu + KHÔNG phải là một toán tử hòa hợp (vì nó tạo ra một chuỗi mới). Sử dụng + = để nối.
Martin York

1
Kể từ C ++ 11, có một điểm quan trọng: toán tử + có thể sửa đổi một trong các toán hạng của nó và trả về từng bước nếu toán hạng đó được chuyển bằng tham chiếu rvalue. libstdc++ làm điều này, chẳng hạn . Vì vậy, khi gọi toán tử + với thời gian tạm thời, nó có thể đạt được hiệu suất gần như tốt - có lẽ là một lập luận ủng hộ việc mặc định cho nó, vì lợi ích dễ đọc, trừ khi một điểm chuẩn cho thấy nó là một nút cổ chai. Tuy nhiên, một chuẩn variadic append()sẽ là cả tối ưu có thể đọc được ...
underscore_d

Câu trả lời:


85

Việc làm thêm chưa chắc đã đáng, trừ khi bạn thực sự cần sự hiệu quả. Bạn có thể sẽ có hiệu quả tốt hơn nhiều chỉ đơn giản bằng cách sử dụng toán tử + = thay thế.

Bây giờ sau tuyên bố từ chối trách nhiệm đó, tôi sẽ trả lời câu hỏi thực tế của bạn ...

Hiệu quả của lớp chuỗi STL phụ thuộc vào việc triển khai STL bạn đang sử dụng.

Bạn có thể đảm bảo hiệu quả và tự kiểm soát tốt hơn bằng cách thực hiện nối thủ công thông qua c chức năng tích hợp sẵn.

Tại sao toán tử + không hiệu quả:

Hãy xem giao diện này:

template <class charT, class traits, class Alloc>
basic_string<charT, traits, Alloc>
operator+(const basic_string<charT, traits, Alloc>& s1,
          const basic_string<charT, traits, Alloc>& s2)

Bạn có thể thấy rằng một đối tượng mới được trả về sau mỗi dấu +. Điều đó có nghĩa là một bộ đệm mới được sử dụng mỗi lần. Nếu bạn đang thực hiện nhiều + hoạt động bổ sung thì nó không hiệu quả.

Tại sao bạn có thể làm cho nó hiệu quả hơn:

  • Bạn đang đảm bảo tính hiệu quả thay vì tin tưởng một người được ủy quyền làm điều đó một cách hiệu quả cho bạn
  • lớp std :: string không biết gì về kích thước tối đa của chuỗi của bạn, cũng như tần suất bạn sẽ nối với nó. Bạn có thể có kiến ​​thức này và có thể làm những việc dựa trên thông tin này. Điều này sẽ dẫn đến việc phân bổ lại ít hơn.
  • Bạn sẽ kiểm soát bộ đệm theo cách thủ công để có thể chắc chắn rằng bạn sẽ không sao chép toàn bộ chuỗi vào bộ đệm mới khi bạn không muốn điều đó xảy ra.
  • Bạn có thể sử dụng ngăn xếp cho bộ đệm của mình thay vì đống sẽ hiệu quả hơn nhiều.
  • Toán tử string + sẽ tạo một đối tượng chuỗi mới và trả lại nó bằng cách sử dụng một bộ đệm mới.

Cân nhắc khi thực hiện:

  • Theo dõi độ dài chuỗi.
  • Giữ một con trỏ đến cuối chuỗi và bắt đầu, hoặc chỉ bắt đầu và sử dụng bắt đầu + độ dài làm phần bù để tìm phần cuối của chuỗi.
  • Đảm bảo rằng bộ đệm bạn đang lưu trữ chuỗi của mình đủ lớn để bạn không cần phải phân bổ lại dữ liệu
  • Sử dụng strcpy thay vì strcat để bạn không cần phải lặp qua độ dài của chuỗi để tìm phần cuối của chuỗi.

Cấu trúc dữ liệu dây:

Nếu bạn cần nối thực sự nhanh, hãy xem xét sử dụng cấu trúc dữ liệu dây .


6
Lưu ý: "STL" đề cập đến một thư viện mã nguồn mở hoàn toàn riêng biệt, ban đầu của HP, một số phần trong số đó được sử dụng làm cơ sở cho các phần của Thư viện C ++ Tiêu chuẩn ISO. "std :: string", tuy nhiên, chưa bao giờ là một phần của STL của HP, vì vậy việc tham chiếu "STL và" string "với nhau là hoàn toàn sai.
James Curran

1
Tôi sẽ không nói là sai khi sử dụng STL và chuỗi với nhau. Xem sgi.com/tech/stl/table_of_contents.html
Brian R. Bondy

1
Khi SGI tiếp quản việc bảo trì STL từ HP, nó đã được trang bị lại để phù hợp với Thư viện Chuẩn (đó là lý do tại sao tôi nói "không bao giờ là một phần của STL của HP"). Tuy nhiên, người khởi tạo chuỗi std :: là Ủy ban ISO C ++.
James Curran

2
Lưu ý: Nhân viên SGI chịu trách nhiệm duy trì STL trong nhiều năm là Matt Austern, người đồng thời đứng đầu nhóm Thư viện của Ủy ban Tiêu chuẩn hóa ISO C ++.
James Curran

4
Bạn có thể vui lòng làm rõ hoặc đưa ra một số điểm tại sao Bạn có thể sử dụng ngăn xếp cho bộ đệm của mình thay vì đống hiệu quả hơn nhiều. ? Sự khác biệt về hiệu quả này đến từ đâu?
h7r 13/03/13

76

Đặt trước không gian cuối cùng của bạn, sau đó sử dụng phương thức nối thêm với bộ đệm. Ví dụ: giả sử bạn mong đợi độ dài chuỗi cuối cùng của mình là 1 triệu ký tự:

std::string s;
s.reserve(1000000);

while (whatever)
{
  s.append(buf,len);
}

17

Tôi sẽ không lo lắng về nó. Nếu bạn làm điều đó trong một vòng lặp, các chuỗi sẽ luôn phân bổ trước bộ nhớ để giảm thiểu việc phân bổ lại - chỉ sử dụng operator+=trong trường hợp đó. Và nếu bạn làm điều đó theo cách thủ công, tương tự như thế này hoặc lâu hơn

a + " : " + c

Sau đó, nó tạo ra thời gian tạm thời - ngay cả khi trình biên dịch có thể loại bỏ một số bản sao giá trị trả về. Đó là bởi vì trong một lần gọi liên tiếp, operator+nó không biết liệu tham số tham chiếu có tham chiếu đến một đối tượng được đặt tên hay tạm thời được trả về từ một lệnh operator+gọi con hay không . Tôi không muốn lo lắng về nó trước khi không có hồ sơ đầu tiên. Nhưng hãy lấy một ví dụ cho thấy điều đó. Đầu tiên chúng tôi giới thiệu dấu ngoặc đơn để làm rõ ràng ràng buộc. Tôi đặt các đối số ngay sau khai báo hàm được sử dụng để làm rõ ràng. Dưới đó, tôi hiển thị biểu thức kết quả sau đó là:

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)

Bây giờ, thêm vào đó, tmp1là những gì được trả về bởi cuộc gọi đầu tiên tới toán tử + với các đối số được hiển thị. Chúng tôi cho rằng trình biên dịch thực sự thông minh và tối ưu hóa bản sao giá trị trả về. Vì vậy, chúng tôi kết thúc với một chuỗi mới chứa nối của a" : ". Bây giờ, điều này xảy ra:

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>

So sánh điều đó với những điều sau:

std::string f = "hello";
(f + c)
calls string operator+(string const&, string const&)(f, c)
  => tmp1 == <end result>

Nó sử dụng cùng một hàm cho một chuỗi tạm thời và cho một chuỗi được đặt tên! Vì vậy, các trình biên dịch để sao chép các đối số vào một chuỗi mới và append vào đó và gửi lại cho ra khỏi cơ thể của operator+. Nó không thể lấy bộ nhớ của một tạm thời và thêm vào đó. Biểu thức càng lớn thì càng phải thực hiện nhiều bản sao của chuỗi.

Tiếp theo Visual Studio và GCC sẽ hỗ trợ ngữ nghĩa di chuyển của c ++ 1x (bổ sung ngữ nghĩa sao chép ) và các tham chiếu rvalue như một bổ sung thử nghiệm. Điều đó cho phép tìm hiểu xem tham số có tham chiếu tạm thời hay không. Điều này sẽ làm cho những bổ sung như vậy nhanh chóng đáng kinh ngạc, vì tất cả những điều trên sẽ kết thúc trong một "đường ống bổ sung" mà không có bản sao.

Nếu nó trở thành một nút cổ chai, bạn vẫn có thể làm

 std::string(a).append(" : ").append(c) ...

Các appendlệnh gọi nối đối số vào *thisvà sau đó trả về một tham chiếu cho chính chúng. Vì vậy, không có việc sao chép thời gian tạm thời được thực hiện ở đó. Hoặc cách khác, operator+=có thể được sử dụng, nhưng bạn sẽ cần dấu ngoặc đơn xấu xí để sửa mức độ ưu tiên.


Tôi đã phải kiểm tra những người triển khai stdlib thực sự làm điều này. : P libstdc++để operator+(string const& lhs, string&& rhs)làm gì return std::move(rhs.insert(0, lhs)). Sau đó, nếu cả hai đều là tạm thời, operator+(string&& lhs, string&& rhs)nếu nó lhscó đủ dung lượng sẵn có sẽ chỉ trực tiếp append(). Nơi tôi nghĩ rằng điều này có nguy cơ chậm hơn operator+=là nếu lhskhông có đủ dung lượng, vì sau đó nó rơi trở lại rhs.insert(0, lhs), điều này không chỉ phải mở rộng bộ đệm & thêm nội dung mới như thế nào append(), mà còn cần phải thay đổi nội dung ban đầu của rhsbên phải.
underscore_d

Phần chi phí khác được so sánh operator+=operator+vẫn phải trả về một giá trị, vì vậy nó phải move()phụ thuộc vào toán hạng nào mà nó được thêm vào. Tuy nhiên, tôi đoán đó là một chi phí khá nhỏ (sao chép một vài con trỏ / kích thước) so với sao chép sâu toàn bộ chuỗi, vì vậy nó tốt!
underscore_d

11

Đối với hầu hết các ứng dụng, nó sẽ không thành vấn đề. Chỉ cần viết mã của bạn, vui mừng không biết chính xác cách hoạt động của toán tử + và chỉ giải quyết vấn đề của bạn nếu nó trở thành một nút cổ chai rõ ràng.


7
Tất nhiên nó không đáng cho hầu hết các trường hợp, nhưng điều này không thực sự trả lời câu hỏi của anh ta.
Brian R. Bondy

1
vâng. Tôi đồng ý chỉ cần nói "hồ sơ sau đó tối ưu hóa" có thể được đưa vào bình luận cho câu hỏi :)
Johannes Schaub - litb

6
Về mặt kỹ thuật, ông hỏi liệu đây có phải là "Cần thiết". Họ không, và điều này trả lời câu hỏi đó.
Samantha Branham

Đủ công bằng, nhưng nó chắc chắn cần thiết cho một số ứng dụng. Vì vậy, trong các ứng dụng đó, câu trả lời rút gọn thành: 'tự mình xử lý vấn đề'
Brian R. Bondy

4
@Pesto Có một quan niệm sai lầm trong thế giới lập trình rằng hiệu suất không quan trọng và chúng ta có thể bỏ qua toàn bộ thỏa thuận vì máy tính ngày càng nhanh hơn. Vấn đề là, đó không phải là lý do tại sao mọi người lập trình bằng C ++ và đó không phải là lý do tại sao họ đăng câu hỏi về tràn ngăn xếp về cách nối chuỗi hiệu quả.
MrFox

7

Không giống như .NET System.Strings, chuỗi std :: của C ++ có thể thay đổi được và do đó có thể được xây dựng thông qua phép nối đơn giản cũng nhanh như thông qua các phương thức khác.


2
Đặc biệt nếu bạn sử dụng Reserve () để tạo bộ đệm đủ lớn cho kết quả trước khi bắt đầu.
Mark Ransom

tôi nghĩ anh ấy đang nói về toán tử + =. nó cũng nối, mặc dù đó là một trường hợp suy biến. james là một vc ++ mvp vì vậy tôi hy vọng anh ấy có một số manh mối của c ++: p
Johannes Schaub - litb

1
Tôi không nghi ngờ trong một giây rằng anh ấy có kiến ​​thức sâu rộng về C ++, chỉ là có một sự hiểu lầm về câu hỏi. Câu hỏi đặt ra về hiệu quả của toán tử + trả về các đối tượng chuỗi mới mỗi khi nó được gọi, và do đó sử dụng bộ đệm char mới.
Brian R. Bondy

1
vâng. nhưng sau đó anh ấy yêu cầu toán tử trường hợp + là chậm, cách tốt nhất là làm một nối. và ở đây toán tử + = xuất hiện trong trò chơi. nhưng tôi đồng ý câu trả lời của james hơi ngắn. nó làm cho nó âm thanh giống như tất cả chúng ta có thể sử dụng toán tử + và đó là đầu hiệu quả: p
Johannes Schaub - litb

@ BrianR.Bondy operator+không phải trả về một chuỗi mới. Người triển khai có thể trả về một trong các toán hạng của nó, đã được sửa đổi, nếu toán hạng đó được chuyển bằng tham chiếu rvalue. libstdc++ làm điều này, chẳng hạn . Vì vậy, khi gọi operator+với thời gian tạm thời, nó có thể đạt được hiệu suất tương tự hoặc gần như tốt - có thể là một lập luận khác ủng hộ việc mặc định cho nó trừ khi một điểm chuẩn cho thấy nó đại diện cho một nút cổ chai.
underscore_d


4

Trong Imperfect C ++ , Matthew Wilson trình bày một trình nối chuỗi động tính toán trước độ dài của chuỗi cuối cùng để chỉ có một phân bổ trước khi nối tất cả các phần. Chúng ta cũng có thể triển khai một trình nối tĩnh bằng cách chơi với các mẫu biểu thức .

Loại ý tưởng đó đã được thực hiện trong triển khai STLport std :: string - điều đó không phù hợp với tiêu chuẩn vì vụ hack chính xác này.


Glib::ustring::compose()từ các liên kết glibmm đến GLib thực hiện điều đó: ước tính và reserve()là độ dài cuối cùng dựa trên chuỗi định dạng được cung cấp và các varargs, sau đó append()là từng (hoặc thay thế được định dạng của nó) trong một vòng lặp. Tôi kỳ vọng đây là một cách làm việc khá phổ biến.
underscore_d

4

std::string operator+phân bổ một chuỗi mới và sao chép hai chuỗi toán hạng mỗi lần. lặp lại nhiều lần và nó trở nên đắt đỏ, O (n).

std::string appendoperator+=mặt khác, tăng dung lượng lên 50% mỗi khi chuỗi cần phát triển. Điều này làm giảm đáng kể số lần cấp phát bộ nhớ và hoạt động sao chép, O (log n).


Tôi không chắc tại sao điều này bị phản đối. Con số 50% không được yêu cầu trong Tiêu chuẩn, nhưng IIRC hoặc 100% là các thước đo tăng trưởng phổ biến trong thực tế. Mọi thứ khác trong câu trả lời này dường như không thể chối cãi.
underscore_d

Nhiều tháng sau, tôi cho rằng nó không phải là tất cả chính xác, vì nó được viết rất lâu sau khi C ++ 11 ra mắt và quá tải operator+nơi một hoặc cả hai đối số được chuyển bằng tham chiếu rvalue có thể tránh cấp phát toàn bộ một chuỗi mới bằng cách nối vào bộ đệm hiện có của một trong các toán hạng (mặc dù họ có thể phải phân bổ lại nếu nó không đủ dung lượng).
underscore_d

2

Đối với dây nhỏ thì không thành vấn đề. Nếu bạn có các chuỗi lớn, bạn nên lưu trữ chúng dưới dạng vector hoặc trong một số bộ sưu tập khác dưới dạng các bộ phận. Và thêm thuật toán của bạn để làm việc với tập dữ liệu như vậy thay vì một chuỗi lớn.

Tôi thích std :: ostringstream để nối phức tạp.


2

Như với hầu hết mọi thứ, không làm điều gì đó dễ hơn làm.

Nếu bạn muốn xuất các chuỗi lớn sang GUI, có thể là bất cứ thứ gì bạn đang xuất ra có thể xử lý các chuỗi thành từng phần tốt hơn là một chuỗi lớn (ví dụ: nối văn bản trong trình soạn thảo văn bản - thường chúng giữ các dòng riêng biệt cấu trúc).

Nếu bạn muốn xuất ra một tệp, hãy truyền dữ liệu trực tuyến thay vì tạo một chuỗi lớn và xuất ra.

Tôi chưa bao giờ thấy cần phải làm cho việc nối nhanh hơn cần thiết nếu tôi đã loại bỏ sự nối không cần thiết khỏi mã chậm.


2

Có thể là hiệu suất tốt nhất nếu bạn phân bổ trước (dự trữ) không gian trong chuỗi kết quả.

template<typename... Args>
std::string concat(Args const&... args)
{
    size_t len = 0;
    for (auto s : {args...})  len += strlen(s);

    std::string result;
    result.reserve(len);    // <--- preallocate result
    for (auto s : {args...})  result += s;
    return result;
}

Sử dụng:

std::string merged = concat("This ", "is ", "a ", "test!");

0

Một mảng ký tự đơn giản, được đóng gói trong một lớp giúp theo dõi kích thước mảng và số byte được phân bổ là nhanh nhất.

Bí quyết là chỉ thực hiện một phân bổ lớn khi bắt đầu.

tại

https://github.com/pedro-vicente/table-string

Điểm chuẩn

Đối với Visual Studio 2015, xây dựng gỡ lỗi x86, cải tiến cơ bản về C ++ std :: string.

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  

1
OP quan tâm đến cách nối hiệu quả std::string. Họ không yêu cầu một lớp chuỗi thay thế.
underscore_d

0

Bạn có thể thử cái này với bộ nhớ đặt trước cho từng món:

namespace {
template<class C>
constexpr auto size(const C& c) -> decltype(c.size()) {
  return static_cast<std::size_t>(c.size());
}

constexpr std::size_t size(const char* string) {
  std::size_t size = 0;
  while (*(string + size) != '\0') {
    ++size;
  }
  return size;
}

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept {
  return N;
}
}

template<typename... Args>
std::string concatStrings(Args&&... args) {
  auto s = (size(args) + ...);
  std::string result;
  result.reserve(s);
  return (result.append(std::forward<Args>(args)), ...);
}
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.