Tại sao dây rất chậm?


23

Ngay từ lớp học lập trình đầu tiên của tôi ở trường trung học, tôi đã nghe nói rằng các hoạt động chuỗi chậm hơn - tức là tốn kém hơn - so với "hoạt động trung bình" huyền thoại. Tại sao làm cho họ chậm như vậy? (Câu hỏi này để lại có chủ ý rộng.)


11
Nếu bạn biết rằng những "hoạt động trung bình" này là huyền thoại, ít nhất bạn có thể cho chúng tôi biết một số trong số chúng là gì không? Cho rằng bạn đang hỏi một câu hỏi mơ hồ như vậy, thật khó để tin vào lời khẳng định của bạn rằng những hoạt động không xác định này thực sự là hoang đường.
seh

1
@seh, thật không may, tôi thực sự không thể trả lời điều đó. Vài lần tôi thực sự đã hỏi mọi người chuỗi nào chậm hơn, họ chỉ nhún vai và nói "họ chỉ chậm thôi". Ngoài ra, nếu tôi có thông tin cụ thể hơn, đây sẽ là một câu hỏi cho SO, không phải lập trình viên; nó đã là một đường biên giới.
Pops

Điểm là gì? Nếu nói chuỗi thực sự chậm, bạn sẽ ngừng sử dụng chúng?
Tulains Córdova

Quên đi. Nếu ai đó nói với bạn những điều vô nghĩa như vậy, thì câu hỏi ngược lại là: "Thật sao? Họ có phải không? Chúng ta có nên sử dụng một mảng int không?"
Ingo

Câu trả lời:


47

"Hoạt động trung bình" diễn ra trên các nguyên thủy. Nhưng ngay cả trong các ngôn ngữ nơi các chuỗi được coi là nguyên thủy, chúng vẫn là các mảng dưới mui xe và làm bất cứ điều gì liên quan đến toàn bộ chuỗi đều mất thời gian O (N), trong đó N là độ dài của chuỗi.

Ví dụ: thêm hai số thường mất 2-4 hướng dẫn ASM. Ghép nối ("thêm") hai chuỗi yêu cầu cấp phát bộ nhớ mới và một hoặc hai bản sao chuỗi, liên quan đến toàn bộ chuỗi.

Một số yếu tố ngôn ngữ có thể làm cho nó tồi tệ hơn. Ví dụ, trong C, một chuỗi chỉ đơn giản là một con trỏ tới một mảng ký tự kết thúc null. Điều này có nghĩa là bạn không biết nó dài bao nhiêu, vì vậy không có cách nào để tối ưu hóa vòng lặp sao chép chuỗi bằng các thao tác di chuyển nhanh; bạn cần sao chép một ký tự một lần để có thể kiểm tra từng byte cho bộ kết thúc null.


4
Và một số ngôn ngữ nhất định làm cho nó tốt hơn nhiều: Mã hóa của Delphi về độ dài chuỗi ở đầu mảng làm cho việc nối chuỗi rất nhanh.
Frank Shearar

4
@gablin: Nó cũng giúp bằng cách làm cho chuỗi sao chép chính nó nhanh hơn rất nhiều. Khi bạn biết kích thước lên phía trước, bạn không phải sao chép một byte mỗi lần và kiểm tra từng byte cho bộ kết thúc null, vì vậy bạn có thể sử dụng kích thước đầy đủ của bất kỳ thanh ghi nào, bao gồm cả SIMD, để di chuyển dữ liệu, thực hiện nó nhanh hơn tới 16 lần.
Mason Wheeler

4
@mathepic: Vâng, và điều đó sẽ tốt cho bạn, nhưng khi bạn bắt đầu tương tác với libc hoặc mã bên ngoài khác, nó sẽ mong đợi char*, chứ không phải a strbuf, và bạn quay lại quảng trường 1. Chỉ có rất nhiều bạn có thể làm khi một thiết kế xấu được đưa vào ngôn ngữ.
Mason Wheeler

6
@mathepic: Tất nhiên là bufcon trỏ ở đó. Tôi không bao giờ ngụ ý rằng nó không có sẵn; đúng hơn, điều đó là cần thiết. Bất kỳ mã nào không biết về loại chuỗi được tối ưu hóa nhưng không chuẩn của bạn, bao gồm cả những thứ cơ bản như thư viện chuẩn , vẫn phải quay lại chậm, không an toàn char*. Bạn có thể gọi FUD đó nếu bạn muốn, nhưng điều đó không làm cho nó không đúng.
Mason Wheeler

7
Mọi người, có một cột Joel Spolsky về quan điểm của Frank Shearer's: Back to Basics
user16764

14

Đây là một chủ đề cũ và tôi nghĩ rằng các câu trả lời khác là tuyệt vời, nhưng bỏ qua một cái gì đó, vì vậy đây là 2 xu của tôi.

Cú pháp phủ đường

Vấn đề với các chuỗi là chúng là công dân hạng hai trong hầu hết các ngôn ngữ và trên thực tế hầu hết thời gian không thực sự là một phần của đặc tả ngôn ngữ: chúng là một cấu trúc do thư viện thực hiện với một số lớp phủ cú pháp thỉnh thoảng ở trên cùng để làm cho họ bớt đau để sử dụng.

Hậu quả trực tiếp của việc này là ngôn ngữ che giấu một phần rất lớn sự phức tạp của chúng khỏi tầm nhìn của bạn và bạn phải trả giá cho những tác dụng phụ lén lút vì bạn phát triển thành thói quen coi chúng như một thực thể nguyên tử cấp thấp, giống như các loại nguyên thủy khác (như được giải thích bởi câu trả lời được bình chọn hàng đầu và các loại khác).

Chi tiết thực hiện

Good Ol 'Array

Một trong những yếu tố của "độ phức tạp" cơ bản này là hầu hết các cài đặt chuỗi sẽ sử dụng cấu trúc dữ liệu đơn giản với một số không gian bộ nhớ liền kề để biểu diễn chuỗi: mảng ol 'tốt của bạn.

Điều này có ý nghĩa tốt, làm phiền bạn, vì bạn muốn truy cập vào toàn bộ chuỗi nhanh chóng. Nhưng điều đó tiềm ẩn chi phí khủng khiếp khi bạn muốn thao tác chuỗi này. Truy cập một phần tử ở giữa có thể nhanh nếu bạn biết bạn đang theo chỉ mục nào, nhưng tìm kiếm một phần tử dựa trên một điều kiện thì không.

Ngay cả việc trả lại kích thước của chuỗi có thể tốn kém, nếu ngôn ngữ của bạn không lưu trữ độ dài của chuỗi và cần phải chạy qua chuỗi đó để đếm các ký tự.

Vì những lý do tương tự, việc thêm các phần tử vào chuỗi của bạn sẽ chứng minh tốn kém vì rất có thể bạn sẽ cần phân bổ lại một số bộ nhớ cho hoạt động này xảy ra.

Vì vậy, các ngôn ngữ khác nhau có cách tiếp cận khác nhau cho các vấn đề này. Chẳng hạn, Java đã tự do biến các chuỗi của nó thành bất biến vì một số lý do hợp lệ (độ dài bộ đệm, an toàn luồng) và cho các đối tác có thể thay đổi của nó (StringBuffer và StringBuilder) sẽ chọn phân bổ kích thước bằng cách sử dụng các khối có kích thước lớn hơn để không cần phân bổ mọi lúc, nhưng hy vọng cho các trường hợp tốt nhất. Nó thường hoạt động tốt, nhưng mặt trái là đôi khi phải trả cho các tác động bộ nhớ.

Hỗ trợ Unicode

Ngoài ra, một lần nữa, điều này là do thực tế là lớp phủ đường cú pháp trong ngôn ngữ của bạn che giấu điều này để bạn chơi tốt, bạn thường không nghĩ đó là điều khoản hỗ trợ unicode (đặc biệt là miễn là bạn không thực sự cần nó và đánh vào bức tường đó). Và một số ngôn ngữ, đang suy nghĩ về phía trước, không triển khai các chuỗi với các mảng cơ bản của các nguyên hàm char 8 bit đơn giản. Họ đã nướng trong UTF-8 hoặc UTF-16 hoặc những gì bạn có hỗ trợ cho bạn và hậu quả là mức tiêu thụ bộ nhớ lớn hơn rất nhiều, thường không cần thiết và thời gian xử lý lớn hơn để phân bổ bộ nhớ, xử lý chuỗi, và thực hiện tất cả các logic đi đôi với thao tác các điểm mã.


Kết quả của tất cả những điều này, là khi bạn làm một cái gì đó tương đương bằng mã giả thành:

hello = "hello,"
world = " world!"
str = hello + world

Có thể là không - mặc dù tất cả những nỗ lực tốt nhất mà các nhà phát triển ngôn ngữ đã bỏ ra để họ hành xử như bạn ngoại trừ - một cách đơn giản như:

a = 1;
b = 2;
shouldBeThree = a + b

Theo dõi, bạn có thể muốn đọc:


Tốt bổ sung cho các cuộc thảo luận hiện tại.
Abel

Tôi chỉ nhận ra đây là câu trả lời tốt nhất vì tuyên bố huyền thoại có thể được áp dụng cho bất cứ điều gì như mã hóa RSA là chậm. Lý do duy nhất để chuỗi được đặt ở vị trí đáng xấu hổ này là bởi vì toán tử cộng được cung cấp cho chuỗi trong hầu hết các ngôn ngữ, khiến người mới không biết về chi phí đằng sau hoạt động.
Codism

@Abel: cảm ơn, dường như với tôi là chỗ cho nhiều chi tiết chung chung hơn.
haylem

@Codism: cảm ơn, rất vui vì bạn thích nó. Tôi thực sự nghĩ rằng điều này có thể được áp dụng cho nhiều trường hợp trong đó nó chỉ là vấn đề phức tạp bị che giấu (và chúng tôi không chú ý nhiều đến các chi tiết cấp thấp nữa cho đến khi cuối cùng chúng tôi cần phải chạm vào một nút cổ chai hoặc gạch đá nào đó ).
haylem

1

Cụm từ "hoạt động trung bình" có lẽ là viết tắt cho một hoạt động duy nhất của máy Chương trình lưu trữ truy cập ngẫu nhiên theo lý thuyết . Đây là cỗ máy lý thuyết mà nó được sử dụng để phân tích thời gian chạy của các thuật toán khác nhau.

Các hoạt động chung thường được thực hiện để tải, thêm, trừ, lưu trữ, chi nhánh. Có lẽ cũng đọc, in và tạm dừng.

Nhưng hầu hết các hoạt động chuỗi yêu cầu một số các hoạt động cơ bản. Ví dụ, sao chép một chuỗi thường yêu cầu thao tác sao chép và do đó, một số thao tác tỷ lệ thuận với độ dài của chuỗi (nghĩa là "tuyến tính"). Tìm một chuỗi con bên trong một chuỗi khác cũng có độ phức tạp tuyến tính.


1

Nó hoàn toàn phụ thuộc vào hoạt động, cách biểu diễn chuỗi và tối ưu hóa tồn tại. Nếu các chuỗi có độ dài 4 hoặc 8 byte (và được căn chỉnh), thì chúng không nhất thiết phải chậm hơn - nhiều thao tác sẽ nhanh như nguyên thủy. Hoặc, nếu tất cả các chuỗi có hàm băm 32 bit hoặc 64 bit, nhiều thao tác cũng sẽ nhanh như vậy (mặc dù bạn phải trả chi phí băm trước).

Nó cũng phụ thuộc vào những gì bạn có nghĩa là "chậm". Hầu hết các chương trình sẽ xử lý chuỗi rất nhanh cho những gì cần thiết. So sánh chuỗi có thể không nhanh bằng so sánh hai int, nhưng chỉ có hồ sơ sẽ tiết lộ "chậm" nghĩa là gì đối với chương trình của bạn.


0

Hãy để tôi trả lời câu hỏi của bạn bằng một câu hỏi. Tại sao nói một chuỗi từ mất nhiều thời gian hơn nói một từ?


2
Nó không nhất thiết phải như vậy.
dùng16764

3
Supercalifragilisticexpialidocious
Spoike

s / từ / âm tiết / g
Caleb

Hãy để tôi trả lời câu hỏi của bạn bằng một câu hỏi: tại sao bạn không nói câu trả lời của bạn có nghĩa là gì? Rốt cuộc, nó không rõ ràng làm thế nào nó có thể được hiểu là áp dụng cho một số hệ thống thời gian chạy.
PJTraill
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.