Tại sao Push_back trong vectơ C ++ không được khấu hao?


23

Tôi đang học C ++ và nhận thấy rằng thời gian chạy cho hàm Push_back cho vectơ là "khấu hao" không đổi. Tài liệu lưu ý thêm rằng "Nếu việc tái phân bổ xảy ra, việc tái phân bổ chính nó là theo tuyến tính trong toàn bộ kích thước."

Điều này không có nghĩa là hàm Push_back là , trong đóO(n) là độ dài của vectơ? Rốt cuộc, chúng ta quan tâm đến phân tích trường hợp xấu nhất, phải không?n

Tôi đoán, chủ yếu, tôi không hiểu tính từ "khấu hao" thay đổi thời gian chạy như thế nào.


Với máy RAM, việc phân bổ byte bộ nhớ không phải là thao tác O ( n ) - nó được coi là khá nhiều thời gian không đổi. nO(n)
usul

Câu trả lời:


24

Từ quan trọng ở đây là "khấu hao". Phân tích khấu hao là một kỹ thuật phân tích kiểm tra một chuỗi các hoạt động . Nếu toàn bộ chuỗi chạy trong thời gian T ( n ) , thì mỗi thao tác trong chuỗi chạy trong T ( n ) / n . Ý tưởng là trong khi một vài thao tác trong chuỗi có thể tốn kém, chúng không thể xảy ra thường xuyên đủ để cân nhắc chương trình. Điều quan trọng cần lưu ý là điều này khác với phân tích trường hợp trung bình so với một số phân phối đầu vào hoặc phân tích ngẫu nhiên. Phân tích khấu hao thành lập một trường hợp xấu nhấtnT(n)T(n)/nràng buộc cho hiệu suất của một thuật toán không phân biệt đầu vào. Nó được sử dụng phổ biến nhất để phân tích các cấu trúc dữ liệu, có trạng thái liên tục trong suốt chương trình.

Một trong những ví dụ phổ biến nhất được đưa ra là phân tích một ngăn xếp với các hoạt động đa nhân bật ra các phần tử . Một phân tích ngây thơ về bội số sẽ nói rằng trong trường hợp xấu nhất, bội số phải mất thời gian O ( n ) vì nó có thể phải bật ra khỏi tất cả các yếu tố của ngăn xếp. Tuy nhiên, nếu bạn nhìn vào một chuỗi các hoạt động, bạn sẽ nhận thấy rằng số lần bật không thể vượt quá số lần đẩy. Như vậy so với bất kỳ chuỗi n hoạt động số pops không thể vượt quá O ( n ) , và chạy rất multipop trong O ( 1 ) khấu hao theo thời gian mặc dù thỉnh thoảng một cuộc gọi duy nhất có thể mất nhiều thời gian hơn.kÔi(n)nÔi(n)Ôi(1)

Bây giờ làm thế nào điều này liên quan đến các vectơ C ++? Các vectơ được triển khai với các mảng để tăng kích thước của vectơ, bạn phải phân bổ lại bộ nhớ và sao chép toàn bộ mảng. Rõ ràng chúng tôi sẽ không muốn làm điều này rất thường xuyên. Vì vậy, nếu bạn thực hiện thao tác đẩy_back và vectơ cần phân bổ nhiều không gian hơn, nó sẽ tăng kích thước theo hệ số . Bây giờ, việc này chiếm nhiều bộ nhớ hơn, mà bạn có thể không sử dụng đầy đủ, nhưng một vài thao tác đẩy tiếp theo đều chạy trong thời gian không đổi.m

Bây giờ nếu chúng ta thực hiện phân tích khấu hao của hoạt động Push_back (mà tôi tìm thấy ở đây ), chúng ta sẽ thấy rằng nó chạy trong thời gian khấu hao không đổi. Giả sử bạn có mục và hệ số nhân của bạn là m . Sau đó, số lượng di dời được khoảng log m ( n ) . Việc tái phân bổ thứ i sẽ có giá tương ứng với m i , về kích thước của mảng hiện tại. Do đó tổng thời gian cho n đẩy lùi là log m ( n ) i = 1 m in mnmđăng nhậpm(n)tôimtôin , vì nó là một chuỗi hình học. Chia điều này chonthao tác và chúng ta nhận được rằng mỗi thao tác mấtmΣtôi= =1đăng nhậpm(n)mtôinmm-1n , một hằng số. Cuối cùng, bạn phải cẩn thận về việc chọn yếu tốmcủa bạn. Nếu nó quá gần1thì hằng số này trở nên quá lớn đối với các ứng dụng thực tế, nhưng nếumquá lớn, giả sử là 2, thì bạn bắt đầu lãng phí rất nhiều bộ nhớ. Tốc độ tăng trưởng lý tưởng thay đổi theo ứng dụng, nhưng tôi nghĩ một số triển khai sử dụng1.5.mm-1m1m1,5


12

Mặc dù @Marc đã đưa ra (những gì tôi nghĩ là) một phân tích xuất sắc, một số người có thể thích xem xét mọi thứ từ một góc độ hơi khác.

Một là xem xét một cách hơi khác để thực hiện việc tái phân bổ. Thay vì sao chép tất cả các phần tử từ bộ lưu trữ cũ sang bộ lưu trữ mới ngay lập tức, hãy xem xét chỉ sao chép một phần tử tại một thời điểm - tức là, mỗi khi bạn thực hiện một lần đẩy, nó sẽ thêm phần tử mới vào không gian mới và sao chép chính xác một phần tử hiện có yếu tố từ không gian cũ sang không gian mới. Giả sử hệ số tăng trưởng là 2, khá rõ ràng là khi không gian mới đầy, chúng tôi đã hoàn thành sao chép tất cả các yếu tố từ không gian cũ sang không gian mới và mỗi lần đẩy lại chính xác là thời gian không đổi. Vào thời điểm đó, chúng tôi sẽ loại bỏ không gian cũ, phân bổ một khối bộ nhớ mới có mức tăng gấp đôi và lặp lại quy trình.

Rõ ràng, chúng ta có thể tiếp tục điều này vô thời hạn (hoặc miễn là có sẵn bộ nhớ) và mỗi lần đẩy sẽ liên quan đến việc thêm một yếu tố mới và sao chép một yếu tố cũ.

Một triển khai điển hình vẫn có chính xác cùng số lượng bản sao - nhưng thay vì thực hiện từng bản sao một lần, nó sẽ sao chép tất cả các yếu tố hiện có cùng một lúc. Một mặt, bạn đã đúng: điều đó có nghĩa là nếu bạn nhìn vào các yêu cầu riêng lẻ của Push_back, một số trong số chúng sẽ chậm hơn đáng kể so với những cái khác. Tuy nhiên, nếu chúng ta nhìn vào mức trung bình dài hạn, số lượng sao chép được thực hiện cho mỗi lần gọi Push_back vẫn không đổi, bất kể kích thước của vectơ.

Mặc dù nó không liên quan đến độ phức tạp tính toán, tôi nghĩ rằng đáng để chỉ ra lý do tại sao việc làm như họ lại thuận lợi, thay vì sao chép một yếu tố trên mỗi lần đẩy, do đó thời gian cho mỗi lần đẩy lại không đổi. Có ít nhất ba lý do để xem xét.

Đầu tiên chỉ đơn giản là bộ nhớ có sẵn. Bộ nhớ cũ có thể được giải phóng cho các mục đích sử dụng khác chỉ sau khi sao chép xong. Nếu bạn chỉ sao chép một mục tại một thời điểm, khối bộ nhớ cũ sẽ được phân bổ lâu hơn nhiều. Thực tế, bạn có một khối cũ và một khối mới được phân bổ chủ yếu mọi lúc. Nếu bạn quyết định yếu tố tăng trưởng nhỏ hơn hai (mà bạn thường muốn), bạn sẽ cần nhiều bộ nhớ hơn được phân bổ mọi lúc.

Thứ hai, nếu bạn chỉ sao chép một phần tử cũ tại một thời điểm, việc lập chỉ mục vào mảng sẽ khó khăn hơn một chút - mỗi thao tác lập chỉ mục sẽ cần tìm hiểu xem phần tử tại chỉ mục đã cho hiện đang ở trong khối bộ nhớ cũ hay cái mới Điều đó không quá phức tạp bằng bất kỳ phương tiện nào, nhưng đối với một hoạt động cơ bản như lập chỉ mục vào một mảng, hầu như bất kỳ sự chậm lại nào cũng có thể là đáng kể.

Thứ ba, bằng cách sao chép tất cả cùng một lúc, bạn tận dụng lợi thế của bộ nhớ đệm tốt hơn nhiều. Sao chép tất cả cùng một lúc, bạn có thể hy vọng cả nguồn và đích sẽ nằm trong bộ đệm trong hầu hết các trường hợp, do đó, chi phí của lỗi bộ nhớ cache được khấu hao theo số lượng phần tử sẽ phù hợp với dòng bộ đệm. Nếu bạn sao chép một phần tử tại một thời điểm, bạn có thể dễ dàng bỏ lỡ bộ đệm cho mọi phần tử bạn sao chép. Điều đó chỉ thay đổi hệ số không đổi, không phức tạp, nhưng nó vẫn có thể khá đáng kể - đối với một máy điển hình, bạn có thể dễ dàng mong đợi hệ số từ 10 đến 20.

Có lẽ cũng đáng để xem xét hướng khác trong một lúc: nếu bạn đang thiết kế một hệ thống với các yêu cầu thời gian thực, thì có thể chỉ sao chép một yếu tố tại một thời điểm thay vì cùng một lúc. Mặc dù tốc độ tổng thể có thể (hoặc có thể không) thấp hơn, nhưng bạn vẫn bị giới hạn trên về thời gian thực hiện một lần thực hiện Push_back - giả sử bạn có một công cụ phân bổ thời gian thực (mặc dù vậy, nhiều thời gian thực các hệ thống chỉ đơn giản là cấm phân bổ động bộ nhớ, ít nhất là trong các phần có yêu cầu thời gian thực).


2
+1 Đây là một lời giải thích theo phong cách Feynman tuyệt vời .
Phục hồi lại
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.