Có phải là không hiệu quả để nối từng chuỗi một?


11

Tôi nhớ lại từ những ngày lập trình ở C rằng khi hai chuỗi được nối, HĐH phải cấp phát bộ nhớ cho chuỗi đã nối, sau đó chương trình có thể sao chép tất cả văn bản chuỗi sang vùng mới trong bộ nhớ, sau đó bộ nhớ cũ phải thủ công được phát hành Vì vậy, nếu điều này được thực hiện nhiều lần như trong trường hợp tham gia một danh sách, HĐH phải liên tục phân bổ ngày càng nhiều bộ nhớ hơn, chỉ để nó được phát hành sau lần ghép tiếp theo. Một cách tốt hơn để làm điều này trong C sẽ là xác định tổng kích thước của các chuỗi kết hợp và phân bổ bộ nhớ cần thiết cho toàn bộ danh sách các chuỗi đã tham gia.

Bây giờ trong các ngôn ngữ lập trình hiện đại (ví dụ C #), tôi thường thấy nội dung của các bộ sưu tập được nối với nhau bằng cách lặp qua bộ sưu tập và thêm tất cả các chuỗi, từng chuỗi một vào một tham chiếu chuỗi. Đây không phải là không hiệu quả, ngay cả với sức mạnh tính toán hiện đại?


để nó cho trình biên dịch và trình biên dịch, họ sẽ quan tâm đến nó, thời gian của bạn tốn kém hơn nhiều so với thời gian để nối chuỗi.
OZ_

7
Phụ thuộc vào việc triển khai - bạn thực sự nên kiểm tra tài liệu cho thư viện chuỗi cụ thể của mình. Có thể thực hiện các chuỗi nối bằng tham chiếu, trong thời gian O (1). Trong mọi trường hợp, nếu bạn cần nối một danh sách các chuỗi dài tùy ý, bạn nên sử dụng các lớp hoặc các hàm được thiết kế cho loại điều này.
sắp tới

Lưu ý rằng những thứ như nối chuỗi thường được xử lý bởi chức năng thư viện, không phải hệ điều hành. HĐH có thể tham gia vào việc cấp phát bộ nhớ, nhưng có lẽ không dành cho các đối tượng tương đối nhỏ như chuỗi.
Caleb

@Caleb HĐH có liên quan đến việc cấp phát TẤT CẢ bộ nhớ. Không tuân theo quy tắc này là một loại rò rỉ bộ nhớ. Ngoại lệ là khi bạn có các chuỗi mã hóa cứng trong ứng dụng; những người được viết dưới dạng dữ liệu nhị phân trong hội đồng được tạo. Nhưng ngay khi bạn thao tác (hoặc thậm chí chỉ định) một chuỗi, nó cần được lưu trữ trong bộ nhớ (nghĩa là bộ nhớ phải được cấp phát).
JSideris

4
@Bizorke Trong một kịch bản điển hình, bộ cấp phát bộ nhớ như malloc () (là một phần của thư viện chuẩn C, không phải HĐH) được sử dụng để phân bổ các khối bộ nhớ khác nhau từ bộ nhớ đã được HĐH cấp cho bộ xử lý. HĐH không cần tham gia trừ khi quá trình sử dụng ít bộ nhớ và cần yêu cầu nhiều hơn. Nó cũng có thể tham gia ở cấp độ thấp hơn nếu phân bổ gây ra lỗi trang. Vì vậy, có, hệ điều hành cuối cùng cung cấp bộ nhớ, nhưng nó không nhất thiết liên quan đến việc phân bổ chuỗi và các đối tượng khác trong quy trình.
Caleb

Câu trả lời:


21

Giải thích của bạn tại sao nó không hiệu quả là chính xác, ít nhất là trong các ngôn ngữ mà tôi quen thuộc (C, Java, C #), mặc dù tôi không đồng ý rằng việc thực hiện nối chuỗi chuỗi là rất phổ biến. Trong đoạn code C # tôi làm việc trên, có sử dụng dồi dào StringBuilder, String.Formatvv đó là tất cả bộ nhớ tiết kiệm techiniques để tránh quá phân bổ lại.

Vì vậy, để có được câu trả lời cho câu hỏi của bạn, chúng ta phải đặt một câu hỏi khác: nếu nó không bao giờ thực sự là một vấn đề để nối chuỗi, tại sao các lớp lại thích StringBuilderStringBuffertồn tại ? Tại sao việc sử dụng các lớp như vậy được bao gồm trong các sách và lớp lập trình bán sơ cấp? Tại sao lời khuyên tối ưu hóa trưởng thành dường như rất nổi bật?

Nếu hầu hết các nhà phát triển nối chuỗi là dựa trên câu trả lời của họ hoàn toàn dựa trên kinh nghiệm, thì hầu hết sẽ nói rằng nó không bao giờ tạo ra sự khác biệt và sẽ tránh sử dụng các công cụ như vậy để "dễ đọc hơn" for (int i=0; i<1000; i++) { strA += strB; }. Nhưng họ không bao giờ đo lường nó.

Câu trả lời thực sự cho câu hỏi này có thể được tìm thấy trong câu trả lời SO này , điều này cho thấy rằng trong một trường hợp, khi nối 50.000 chuỗi (tùy thuộc vào ứng dụng của bạn, có thể là một sự xuất hiện phổ biến), ngay cả những câu nhỏ, dẫn đến hiệu suất 1000 lần .

Nếu hiệu suất theo nghĩa đen hoàn toàn không có nghĩa gì cả, thì tất cả đều có nghĩa là bỏ đi. Nhưng tôi không đồng ý rằng việc sử dụng các lựa chọn thay thế (StringBuilder) là khó khăn hoặc ít đọc hơn , và do đó sẽ là một thực tiễn lập trình hợp lý không nên gọi biện pháp phòng thủ "tối ưu hóa sớm".

CẬP NHẬT:

Tôi nghĩ những gì nó đi xuống, là biết nền tảng của bạn và làm theo các thực tiễn tốt nhất của nó, đáng buồn là không phổ quát . Hai ví dụ từ hai "ngôn ngữ hiện đại" khác nhau:

  1. Trong một câu trả lời SO khác , các đặc tính hiệu suất ngược lại chính xác (mảng.join vs + =) đôi khi được tìm thấy trong JavaScript . Trong một số trình duyệt, nối chuỗi dường như được tối ưu hóa tự động và trong các trường hợp khác thì không. Vì vậy, khuyến nghị (ít nhất là trong câu hỏi SO đó), là chỉ cần nối và không lo lắng về nó.
  2. Trong một trường hợp khác, trình biên dịch Java có thể tự động thay thế phép nối bằng một cấu trúc hiệu quả hơn như StringBuilder. Tuy nhiên, như những người khác đã chỉ ra, điều này là không xác định, không được bảo đảm và sử dụng StringBuilder không ảnh hưởng đến khả năng đọc. Trong trường hợp cụ thể này, tôi có xu hướng đề nghị chống lại việc sử dụng phép nối cho các bộ sưu tập lớn hoặc dựa vào một hành vi trình biên dịch Java không xác định. Tương tự, trong .NET, không bao giờ tối ưu hóa loại sắp xếp .

Đó không hẳn là một tội lỗi chính đáng khi không biết mọi sắc thái của mọi nền tảng ngay lập tức, nhưng bỏ qua các vấn đề nền tảng quan trọng như thế này gần như sẽ chuyển từ Java sang C ++ và không quan tâm đến việc giải phóng bộ nhớ.


-1: chứa BS chính. strA + strBchính xác giống như sử dụng một StringBuilder. Nó có hiệu suất 1x. Hoặc 0x, tùy thuộc vào cách bạn đo. Để biết thêm chi tiết, mã hóa kinh dị.com / blog / 2009/01 / từ
amara

5
@sparkleshy: Tôi đoán là câu trả lời SO sử dụng Java và bài viết được liên kết của bạn sử dụng C #. Tôi đồng ý với những người nói "phụ thuộc vào việc thực hiện" và "đo lường nó cho môi trường cụ thể của bạn".
Kai Chan

1
@KaiChan: nối chuỗi về cơ bản là giống nhau trong java và c #
amara

3
@sparkleshy - Điểm lấy, nhưng sử dụng StringBuilder, String.Join, v.v. để nối chính xác hai chuỗi hiếm khi là một đề xuất, chưa từng có. Hơn nữa, câu hỏi của OP đặc biệt liên quan đến "nội dung của các bộ sưu tập được kết hợp với nhau", đây không phải là trường hợp (trong đó StringBuilder, v.v. rất có thể áp dụng). Bất kể, tôi sẽ cập nhật ví dụ của mình để hiểu rõ hơn.
Kevin McCormick

3
Tôi không quan tâm đến ngôn ngữ cho mục đích của câu hỏi này. Việc sử dụng bộ tạo chuỗi phía sau hậu trường trong một số ngôn ngữ giải thích lý do tại sao nó có thể không hiệu quả để nối toàn bộ danh sách các chuỗi, trả lời câu hỏi của tôi. Tuy nhiên, câu trả lời này đã giải thích rằng việc tham gia một danh sách có thể có khả năng gây nguy hiểm và khuyến nghị nhà xây dựng chuỗi thay thế. Tôi khuyên bạn nên thêm cách sử dụng trình biên dịch chuỗi của trình biên dịch vào câu trả lời của bạn để tránh mất danh tiếng hoặc giải thích sai.
JSideris

2

Nó không hiệu quả, đại khái cho những lý do bạn mô tả. Các chuỗi trong C # và Java là bất biến. Các thao tác trên chuỗi trả về một thể hiện riêng thay vì sửa đổi bản gốc, không giống như trong C. Khi nối nhiều chuỗi, một thể hiện riêng được tạo ở mỗi bước. Phân bổ và thu gom rác sau đó những trường hợp không sử dụng có thể gây ra hiệu quả. Chỉ quản lý bộ nhớ thời gian này được xử lý cho bạn bởi bộ thu gom rác.

Cả C # và Java đều giới thiệu một lớp StringBuilder dưới dạng một chuỗi có thể thay đổi được dành riêng cho loại nhiệm vụ này. Một tương đương trong C sẽ được sử dụng một danh sách các chuỗi được liên kết thay vì nối chúng trong một mảng. C # cũng cung cấp một phương thức Tham gia thuận tiện trên các chuỗi để tham gia bộ sưu tập các chuỗi.


1

Nói đúng ra là sử dụng chu kỳ CPU kém hiệu quả hơn, vì vậy bạn đã đúng. Nhưng những gì về thời gian của nhà phát triển, chi phí bảo trì, v.v. Nếu bạn thêm chi phí thời gian vào phương trình, hầu như luôn hiệu quả hơn để làm những gì dễ nhất, sau đó nếu cần, cấu hình và tối ưu hóa các bit chậm.
"Quy tắc tối ưu hóa chương trình đầu tiên: Đừng làm điều đó. Quy tắc tối ưu hóa chương trình thứ hai (chỉ dành cho chuyên gia!): Đừng làm điều đó."


3
quy tắc không hiệu quả lắm, tôi nghĩ vậy.
OZ_

@OZ_: Đây là một câu trích dẫn được sử dụng rộng rãi (Michael A. Jackson) và khác bởi những người như Donald Knuth ... Sau đó, có một câu nói mà tôi thường không sử dụng "Nhiều tội lỗi điện toán được cam kết nhân danh hiệu quả ( mà không nhất thiết phải đạt được nó) hơn bất kỳ lý do nào khác - bao gồm cả sự ngu ngốc mù quáng. "
mattnz

2
Tôi nên chỉ ra rằng Michael A. Jackson là một người Anh, vì vậy đó là Tối ưu hóa chứ không phải Tối ưu hóa . Tại một số điểm tôi thực sự nên sửa trang wikipedia . * 8 ')
Đánh dấu gian hàng

Tôi hoàn toàn đồng ý, bạn nên sửa những lỗi chính tả. Mặc dù ngôn ngữ mẹ đẻ của tôi là tiếng Anh tiếng Anh, tôi thấy việc nói tiếng Mỹ trên mạng nội bộ dễ dàng hơn .......
mattnz

sẽ không ai nghĩ đến người dùng. Bạn có thể làm cho nó nhanh hơn một chút để nhà phát triển tạo ra, nhưng sau đó mỗi một khách hàng của bạn phải chịu đựng điều đó. Viết mã của bạn cho họ, không phải cho bạn.
gbjbaanb

1

Thật khó để nói bất cứ điều gì về hiệu suất mà không có một bài kiểm tra thực tế. Gần đây, tôi đã rất ngạc nhiên khi phát hiện ra rằng trong JavaScript, việc nối chuỗi ngây thơ thường nhanh hơn giải pháp "tạo danh sách và tham gia" được đề xuất (thử nghiệm ở đây , so sánh t1 với t4). Tôi vẫn còn hoang mang về lý do tại sao điều đó xảy ra.

Một vài câu hỏi bạn có thể hỏi khi suy luận về hiệu suất (đặc biệt là liên quan đến việc sử dụng bộ nhớ) là: 1) đầu vào của tôi lớn như thế nào? 2) trình biên dịch của tôi thông minh như thế nào? 3) làm thế nào để thời gian chạy của tôi quản lý bộ nhớ? Điều này không đầy đủ, nhưng nó là một điểm khởi đầu.

  1. Làm thế nào lớn là đầu vào của tôi?

    Một giải pháp phức tạp thường sẽ có một chi phí cố định, có thể dưới dạng các hoạt động bổ sung sẽ được thực hiện hoặc có thể trong bộ nhớ thêm cần thiết. Vì các giải pháp đó được thiết kế để xử lý các trường hợp lớn, nên những người triển khai thường sẽ không gặp vấn đề gì khi đưa thêm chi phí đó, vì lợi ích ròng quan trọng hơn là tối ưu hóa vi mã. Vì vậy, nếu đầu vào của bạn đủ nhỏ, một giải pháp ngây thơ có thể có hiệu suất tốt hơn so với giải pháp phức tạp, nếu chỉ để tránh chi phí này. (xác định cái gì là "đủ nhỏ" là phần khó)

  2. Làm thế nào thông minh là trình biên dịch của tôi?

    Nhiều trình biên dịch đủ thông minh để "tối ưu hóa" các biến được ghi, nhưng không bao giờ đọc. Tương tự, một trình biên dịch tốt cũng có thể chuyển đổi cách nối chuỗi ngây thơ sang sử dụng thư viện (lõi) và, nếu nhiều trong số chúng được thực hiện mà không có bất kỳ lần đọc nào, thì không cần phải chuyển đổi lại thành chuỗi giữa các hoạt động đó (ngay cả khi mã nguồn của bạn dường như làm điều đó). Tôi không thể biết liệu có bất kỳ trình biên dịch nào ngoài đó thực hiện điều đó hay không, hoặc mức độ đã được thực hiện (AFAIK Java ít nhất thay thế một số concat trong cùng một biểu thức thành một chuỗi các hoạt động StringBuffer), nhưng đó là một khả năng.

  3. Làm thế nào để thời gian chạy của tôi quản lý bộ nhớ?

    Trong các CPU hiện đại, nút cổ chai thường không phải là bộ xử lý, mà là bộ đệm; nếu mã của bạn truy cập nhiều địa chỉ bộ nhớ "xa" trong một thời gian ngắn, thì thời gian cần thiết để di chuyển tất cả bộ nhớ đó giữa các mức bộ nhớ cache vượt xa hầu hết các tối ưu hóa trong hướng dẫn được sử dụng. Điều đó có tầm quan trọng đặc biệt trong thời gian chạy với bộ thu gom rác thế hệ, vì các biến được tạo gần đây nhất (ví dụ trong cùng phạm vi chức năng) thường sẽ nằm trong các địa chỉ bộ nhớ liền kề. Những thời gian chạy đó cũng thường xuyên di chuyển bộ nhớ qua lại giữa các cuộc gọi phương thức.

    Một cách nó có thể ảnh hưởng đến nối chuỗi (từ chối trách nhiệm: đây là một phỏng đoán hoang dã, tôi không đủ hiểu biết để nói chắc chắn) sẽ là nếu bộ nhớ cho người ngây thơ được phân bổ gần với phần còn lại của mã sử dụng nó (thậm chí nếu nó phân bổ và giải phóng nó nhiều lần), trong khi bộ nhớ cho đối tượng thư viện được phân bổ xa nó (vì vậy nhiều bối cảnh thay đổi trong khi mã của bạn tính toán, thư viện tiêu thụ, mã của bạn tính toán nhiều hơn, v.v. sẽ tạo ra nhiều lỗi bộ nhớ cache). Tất nhiên đối với các đầu vào lớn OTOH, bộ nhớ cache sẽ xảy ra dù sao đi nữa, do đó, vấn đề phân bổ nhiều lần trở nên rõ rệt hơn.

Điều đó nói rằng, tôi không ủng hộ việc sử dụng phương pháp này hay phương pháp đó, chỉ có việc kiểm tra và định hình và đo điểm chuẩn nên đi trước bất kỳ phân tích lý thuyết nào về hiệu suất, vì hầu hết các hệ thống hiện nay quá phức tạp để không hiểu chuyên sâu về chủ đề này.


Phải, tôi đồng ý rằng đây chắc chắn là một lĩnh vực mà trình biên dịch có thể nhận ra về mặt lý thuyết rằng bạn đang cố gắng thêm một chuỗi các chuỗi lại với nhau và sau đó tối ưu hóa như thể bạn đang sử dụng trình tạo chuỗi. Tuy nhiên, đây không phải là một việc nhỏ để làm và tôi không nghĩ rằng nó được thực hiện trong bất kỳ trình biên dịch hiện đại nào. Bạn vừa cho tôi một ý tưởng tuyệt vời cho một dự án nghiên cứu đại học: D.
JSideris

Kiểm tra câu trả lời này , trình biên dịch Java đã sử dụng StringBuilderdưới mui xe, tất cả những gì nó cần làm là không gọi toStringcho đến khi biến thực sự cần thiết. Nếu tôi nhớ lại một cách chính xác, nó thực hiện điều đó cho một biểu thức duy nhất, nghi ngờ duy nhất của tôi là liệu nó có áp dụng cho nhiều câu lệnh trong cùng một phương thức hay không. Tôi không biết gì về nội bộ .NET, nhưng tôi tin rằng một chiến lược tương tự cũng có thể được sử dụng bởi trình biên dịch C #.
mgibsonbr

0

Joel đã viết một bài viết tuyệt vời về chủ đề này một thời gian trở lại. Như một số người khác đã chỉ ra, nó phụ thuộc rất nhiều vào ngôn ngữ. Do cách các chuỗi được triển khai trong C (không kết thúc, không có trường độ dài), nên thường trình thư viện strcat tiêu chuẩn rất không hiệu quả. Joel trình bày một sự thay thế chỉ bằng một thay đổi nhỏ sẽ hiệu quả hơn nhiều.


-1

Có phải là không hiệu quả để nối từng chuỗi một?

Không.

Bạn đã đọc 'Bi kịch buồn của nhà hát tối ưu hóa vi mô' chưa?


4
"Tối ưu hóa sớm là gốc rễ của mọi tội lỗi." - Knuth
Scott C Wilson

4
Root của tất cả cái ác trong tối ưu hóa là lấy cụm từ này mà không có ngữ cảnh.
OZ_

Chỉ cần nói điều gì đó là đúng mà không cung cấp một số lý do hỗ trợ không hữu ích trên một diễn đàn như thế này.
Edward lạ

@Crazy Eddie: Bạn đã đọc lý do Jeff Atwood phải nói chưa?
Jim G.
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.