2008-11-23 29 views
9

Tôi đang đọc trong một tệp văn bản lớn với 1,4 triệu dòng có kích thước 24 MB (trung bình 17 ký tự một dòng).Tại sao bộ nhớ dư thừa cho chuỗi trong Delphi?

Tôi đang sử dụng Delphi 2009 và tệp là ANSI nhưng được chuyển đổi thành Unicode khi đọc, vì vậy, bạn có thể nói văn bản sau khi được chuyển đổi có kích thước 48 MB.

(Edit: Tôi tìm thấy một ví dụ đơn giản hơn nhiều ...)

Tôi đang tải văn bản này vào một StringList đơn giản:

AllLines := TStringList.Create; 
    AllLines.LoadFromFile(Filename); 

tôi thấy rằng các dòng dữ liệu dường như mất nhiều bộ nhớ hơn 48 MB.

Thực tế, chúng sử dụng 155 MB bộ nhớ.

Tôi không quan tâm Delphi sử dụng 48 MB hoặc thậm chí nhiều đến 60 MB cho phép một số chi phí quản lý bộ nhớ. Nhưng 155 MB có vẻ quá nhiều.

Đây không phải là lỗi của StringList. Trước đây tôi đã thử tải các dòng vào cấu trúc bản ghi và tôi nhận được kết quả tương tự (160 MB).

Tôi không thấy hoặc hiểu điều gì có thể khiến Delphi hoặc trình quản lý bộ nhớ FastMM sử dụng gấp 3 lần lượng bộ nhớ cần thiết để lưu trữ chuỗi. Phân bổ đống không thể không hiệu quả, phải không?

Tôi đã sửa lỗi này và nghiên cứu nó hết mức có thể. Bất kỳ ý tưởng nào về lý do tại sao điều này có thể xảy ra hoặc những ý tưởng có thể giúp tôi giảm mức sử dụng dư thừa sẽ được đánh giá cao.

Lưu ý: Tôi đang sử dụng tệp "nhỏ hơn" này làm ví dụ. Tôi thực sự cố gắng để tải một tập tin 320 MB, nhưng Delphi là yêu cầu cho hơn 2 GB RAM và hết bộ nhớ vì yêu cầu chuỗi dư thừa này.

Addenum: Marco Cantu vừa ra mắt với a White Paper on Delphi and Unicode. Delphi 2009 đã tăng chi phí cho mỗi chuỗi từ 8 byte lên 12 byte (cộng thêm có thể là 4 cho con trỏ thực tế vào chuỗi). Thêm 16 byte cho mỗi 17x2 = 34 byte thêm gần 50%. Nhưng tôi thấy hơn 200% chi phí. 150% có thể là gì?


Thành công !! Cảm ơn tất cả các bạn đã đề xuất. Tất cả các bạn đã cho tôi suy nghĩ. Nhưng tôi sẽ phải cung cấp cho Jan Goyvaerts tín dụng cho câu trả lời, kể từ khi ông hỏi:

... tại sao bạn sử dụng TStringList? Phải tệp thực sự được lưu trữ trong bộ nhớ dưới dạng các dòng riêng biệt?

Điều đó dẫn tôi đến giải pháp thay vì tải tệp 24 MB dưới dạng chuỗi 1,4 triệu StringList, tôi có thể nhóm các dòng của mình thành các nhóm tự nhiên mà chương trình của tôi biết. Vì vậy, điều này dẫn đến 127.000 dòng được tải vào danh sách chuỗi.

Bây giờ mỗi dòng trung bình 190 ký tự thay vì 17. Chi phí trên mỗi dòng StringList là giống nhau nhưng bây giờ có nhiều dòng ít hơn.

Khi tôi áp dụng điều này vào tệp 320 MB, nó không còn hết bộ nhớ và hiện tải trong chưa đến 1 GB RAM. (Và nó chỉ mất khoảng 10 giây để tải, đó là khá tốt!)

Sẽ có thêm một chút xử lý để phân tích các dòng được nhóm, nhưng không đáng chú ý trong quá trình xử lý thời gian thực của từng nhóm.

(Trong trường hợp bạn tự hỏi, đây là chương trình phả hệ, và đây có thể là bước cuối cùng tôi cần để cho phép tải tất cả dữ liệu về một triệu người trong không gian địa chỉ 32 bit trong vòng chưa đầy 30 giây Vì vậy, tôi vẫn còn có một bộ đệm 20 giây để chơi với để thêm các chỉ mục vào dữ liệu sẽ được yêu cầu để cho phép hiển thị và chỉnh sửa dữ liệu.)

+0

Bạn đo lường bộ nhớ như thế nào? Tôi hy vọng không có cột Mem Usage từ Task Manager. Đó không phải là những gì bạn nghĩ. –

+0

Để đo bộ nhớ, tôi sử dụng GlobalMemoryStatusEx. Xem: http://msdn.microsoft.com/en-us/library/aa366589(VS.85).aspx – lkessler

+0

Bạn nên kiểm tra dung lượng bộ nhớ thực sự được sử dụng trong Delphi. Các Delphi MM sẽ suballocate các khối lớn hơn nó thu được từ hệ điều hành, và phát hành chúng cho hệ điều hành chỉ khi có thể (phân mảnh và như thế có thể phủ nhận nó), vì vậy những gì Windows nhìn thấy và những gì Delphi không có thể khác nhau. Nếu bạn sử dụng thư viện FastMM đầy đủ có sẵn từ Sourceforge, nó có các cơ sở để truy vấn phân bổ MM cho bạn cái nhìn sâu hơn về những gì đang diễn ra. Nếu không, bạn có thể sử dụng một trình thông báo bộ nhớ (tức là AQTime) để kiểm tra nó và xem bộ nhớ được phân bổ, khi nào và tại sao. –

Trả lời

9

Bạn đã tự hỏi tôi trả lời câu hỏi của bạn tại đây. Tôi không biết lý do chính xác tại sao bạn lại thấy mức sử dụng bộ nhớ cao như vậy, nhưng bạn cần phải nhớ rằng TStringList làm được nhiều việc hơn là chỉ tải tệp của bạn. Mỗi bước này yêu cầu bộ nhớ có thể dẫn đến phân mảnh bộ nhớ. TStringList cần tải tập tin của bạn vào bộ nhớ, chuyển đổi nó từ Ansi sang Unicode, chia nó thành một chuỗi cho mỗi dòng và xếp các dòng đó vào một mảng sẽ được phân bổ lại nhiều lần.

Câu hỏi của tôi với bạn là tại sao bạn sử dụng TStringList? Phải tệp thực sự được lưu trữ trong bộ nhớ dưới dạng các dòng riêng biệt? Bạn sẽ sửa đổi các tập tin trong bộ nhớ, hoặc chỉ hiển thị các phần của nó?Giữ tập tin trong bộ nhớ như một phần lớn và quét toàn bộ điều với các biểu thức chính quy phù hợp với các phần bạn muốn sẽ có nhiều bộ nhớ hiệu quả hơn lưu trữ các dòng riêng biệt.

Ngoài ra, toàn bộ tệp có được chuyển đổi sang Unicode không? Trong khi ứng dụng của bạn là Unicode, tệp của bạn là Ansi. Khuyến nghị chung của tôi là chuyển đổi đầu vào Ansi thành Unicode càng sớm càng tốt, vì làm như vậy sẽ tiết kiệm được các chu kỳ CPU. Nhưng khi bạn có 320 MB dữ liệu Ansi sẽ giữ nguyên như dữ liệu Ansi, mức tiêu thụ bộ nhớ sẽ là nút cổ chai. Hãy thử giữ các tập tin như Ansi trong bộ nhớ, và chỉ chuyển đổi các phần bạn sẽ được hiển thị cho người dùng như Ansi.

Nếu tệp 320 MB không phải là tệp dữ liệu bạn đang trích xuất một số thông tin nhất định, nhưng tập dữ liệu bạn muốn sửa đổi, hãy xem xét chuyển đổi nó thành cơ sở dữ liệu quan hệ và để cơ sở dữ liệu lo lắng về cách quản lý bộ dữ liệu khổng lồ với RAM giới hạn.

+0

Cảm ơn bạn Jan cho ý tưởng của bạn, mà mang lại cho tôi nhiều hơn để suy nghĩ về. Đề xuất của bạn về "chunk" làm cho tôi muốn thử tải các nhóm chuỗi, trung bình khoảng 150 ký tự cho mỗi nhóm thay vì 17 ký tự trên mỗi dòng. Phần mềm phả hệ phải là Unicode. – lkessler

+1

Tất nhiên phần mềm của bạn phải là Unicode. Nhưng điều đó không có nghĩa là bạn cần giữ 320 MB dữ liệu trong bộ nhớ trong Unicode, khi nguồn không phải là Unicode. –

1

Bạn có dựa vào Windows để cho bạn biết dung lượng bộ nhớ chương trình đang sử dụng? Nó khét tiếng vì đã phóng đại bộ nhớ được ứng dụng Delphi sử dụng.

Mặc dù vậy, tôi thấy rất nhiều việc sử dụng bộ nhớ bổ sung trong mã của bạn.

Cấu trúc bản ghi của bạn là 20 byte - nếu có một bản ghi như vậy trên mỗi dòng bạn đang xem nhiều dữ liệu hơn cho bản ghi hơn là cho văn bản.

Hơn nữa, một chuỗi có chi phí 4 byte vốn có - một 25% khác.

Tôi tin rằng có một số lượng nhất định phân bổ chi tiết trong xử lý đống của Delphi nhưng tôi không nhớ nó là gì. Ngay cả ở 8 byte (hai con trỏ cho một danh sách liên kết của các khối miễn phí) bạn đang xem xét một 25%.

Lưu ý rằng chúng tôi đã tăng hơn 150%.

+0

Chi phí của một UnicodeString là bốn byte cho độ dài, bốn byte cho số tham chiếu và hai byte cho null ở cuối. –

+0

Trong ví dụ trước của tôi với các bản ghi, tôi đã chỉ rõ rằng tôi đã so sánh việc tải bản ghi và gán chuỗi để tải bản ghi mà không gán chuỗi. Vì vậy, sự khác biệt là do chuỗi một mình, và không phải là 20 byte trong hồ sơ. – lkessler

8

Điều gì sẽ xảy ra nếu bạn tạo hồ sơ gốc của mình sử dụng AnsiString? Điều đó cắt nó một nửa ngay lập tức? Chỉ vì Delphi mặc định UnicodeString không có nghĩa là bạn phải sử dụng nó.

Ngoài ra, nếu bạn biết chính xác độ dài của mỗi chuỗi (trong một hoặc hai ký tự) thì tốt hơn nên sử dụng chuỗi ngắn ngay cả và cạo thêm vài byte nữa.

Tôi rất tò mò nếu có thể có cách nào tốt hơn để thực hiện những gì bạn đang cố gắng làm. Tải 320 MB văn bản vào bộ nhớ có thể không phải là giải pháp tốt nhất, ngay cả khi bạn có thể tải xuống chỉ yêu cầu 320 MB

+0

Câu trả lời hay và tôi sẽ nghĩ về nó. Chương trình của tôi được thiết kế cho Unicode, do đó, nó sẽ là một sự xấu hổ để phải quay trở lại ANSI cho các tập tin rất lớn. Tôi có thể thử ánh xạ bộ nhớ tệp. Tôi không nghĩ rằng nó sẽ đủ nhanh cho những gì tôi cần - nhưng bạn không bao giờ biết cho đến khi bạn thử. – lkessler

4

Theo mặc định, TStringList của Delphi 2009 đọc một tệp là ANSI, trừ khi có Dấu đơn hàng Byte xác định tệp dưới dạng tệp khác hoặc nếu bạn cung cấp mã hóa làm tham số thứ hai tùy chọn của LoadFromFile.

Vì vậy, nếu bạn thấy rằng TStringList chiếm nhiều bộ nhớ hơn bạn nghĩ, thì có điều gì đó khác đang diễn ra.

+0

Cảm ơn, Nick. Hmmm ... Không thể tưởng tượng những gì khác đang xảy ra. Ví dụ của tôi khá đơn giản. – lkessler

3

Bạn có cơ hội biên dịch chương trình bằng các nguồn FastMM từ sourceforge và có định nghĩa FullDebugMode không? Trong trường hợp đó, FastMM không thực sự phát hành các khối bộ nhớ không sử dụng, điều này sẽ giải thích được vấn đề.

+0

Suy nghĩ tốt, nhưng không. Tôi đang sử dụng FastMM trong Delphi 2009. Tùy chọn duy nhất tôi đã thay đổi là tùy chọn trình biên dịch để bật Định dạng Chuỗi Kiểm tra Tắt, như đã được đề xuất trên một số blog. – lkessler

6

Tôi sử dụng Delphi 2009 và tệp ANSI nhưng được chuyển thành Unicode khi đọc, vì vậy bạn có thể nói văn bản sau khi được chuyển đổi có kích thước 48 MB.

Xin lỗi, nhưng tôi không hiểu điều này chút nào. Nếu bạn có nhu cầu cho chương trình của bạn để được Unicode, chắc chắn các tập tin được "ANSI" (nó phải có một số ký tự đặt, như WIN1252 hoặc ISO8859_1) không phải là điều đúng. Đầu tiên tôi sẽ chuyển đổi nó thành UTF8. Nếu tệp không chứa bất kỳ ký tự nào> = 128, nó sẽ không thay đổi một thứ gì (thậm chí nó sẽ có cùng kích thước), nhưng bạn đã chuẩn bị cho tương lai.

Bây giờ bạn có thể tải nó vào chuỗi UTF8, sẽ không tăng gấp đôi mức tiêu thụ bộ nhớ của bạn. On-the-fly-chuyển đổi của vài chuỗi có thể được hiển thị trên màn hình cùng một lúc để chuỗi Unicode Delphi sẽ chậm hơn, nhưng cho bộ nhớ nhỏ hơn chương trình của bạn sẽ thực hiện tốt hơn trên hệ thống với ít (miễn phí) ký ức.

Bây giờ nếu chương trình của bạn vẫn tiêu thụ quá nhiều bộ nhớ với TStringList, bạn luôn có thể sử dụng TStrings hoặc thậm chí IStrings trong chương trình của bạn và viết một lớp thực hiện IStrings hoặc thừa kế TStrings và không giữ tất cả các dòng trong bộ nhớ. Một số ý tưởng đến với tâm trí:

  1. Đọc tệp vào TMemoryStream và duy trì một chuỗi con trỏ đến các ký tự đầu tiên của dòng. Trả về một chuỗi là dễ dàng sau đó, bạn chỉ cần trả về một chuỗi thích hợp giữa bắt đầu của dòng và bắt đầu của một trong những tiếp theo, với CR và NL tước.

  2. Nếu điều này vẫn tiêu thụ quá nhiều bộ nhớ, hãy thay thế TMemoryStream bằng TFileStream, và không duy trì một mảng con trỏ char, nhưng một loạt các tập tin bù đắp cho dòng bắt đầu.

  3. Bạn cũng có thể sử dụng các chức năng API của Windows cho các tệp ánh xạ bộ nhớ. Điều đó cho phép bạn làm việc với các địa chỉ bộ nhớ thay vì bù đắp tập tin, nhưng không tiêu tốn nhiều bộ nhớ như ý tưởng đầu tiên.

+0

3 ý tưởng của bạn là tốt. Nhưng chuyển đổi sang UTF8 là không hiệu quả và sai trong Delphi 2009. Tôi hoặc phải giữ nó trong ANSI và chuyển đổi sang Unicode khi tôi cần, hoặc hấp thụ thêm 24 MB (mà tôi sẵn sàng làm) và chuyển đổi sang Unicode cho chương trình để sử dụng. – lkessler

+0

Xin lỗi, nhưng tôi không đồng ý. UTF8 là định dạng phù hợp để lưu trữ dữ liệu và trao đổi dữ liệu, và vì I/O chậm hơn nhiều so với xử lý CPU, nên cung cấp cho bạn không chỉ các tệp đĩa nhỏ hơn mà hiệu năng tốt hơn. Dù định dạng chuỗi nội bộ, tôi luôn sử dụng UTF8 cho các tệp dữ liệu. – mghie

+1

Các tệp dữ liệu thường có giá trị lớn hơn nhiều so với mã chương trình, vì vậy việc tối ưu hóa cho một môi trường lập trình cụ thể là sai. Định dạng của chúng phải có tính biểu đạt nhưng hiệu quả, tốt nhất là được chuẩn hóa. UTF8 cung cấp cho bạn tất cả những điều đó và phổ biến nhất bên ngoài Windows. Có gì không thích? – mghie

0

Tại sao bạn tải lượng dữ liệu đó vào TStringList? Danh sách chính nó sẽ có một số chi phí. Có thể TTextReader có thể giúp bạn.

+0

TTextReader chỉ giúp Phân tích cú pháp đầu vào. Tôi đã làm điều đó đã bản thân mình rất hiệu quả. Sau đó tôi phải đặt các dòng phân tích ở đâu đó. Ban đầu tôi đã thử sử dụng hồ sơ và tìm thấy vấn đề sử dụng bộ nhớ này. Sau đó, tôi đã tìm thấy cùng một vấn đề trong TStringList và để lại câu hỏi đó như một ví dụ đơn giản hơn. – lkessler

1

Một phần của nó có thể là thuật toán phân bổ khối. Khi danh sách của bạn phát triển, nó bắt đầu tăng số lượng bộ nhớ được cấp phát tại mỗi đoạn. Tôi đã không xem xét nó trong một thời gian dài, nhưng tôi tin rằng nó đi một cái gì đó giống như tăng gấp đôi số tiền cuối cùng được phân bổ mỗi khi nó hết bộ nhớ. Khi bạn bắt đầu để đối phó với các danh sách lớn, phân bổ của bạn cũng lớn hơn nhiều so với bạn cuối cùng cần.

EDIT- Như lkessler đã chỉ ra, sự gia tăng này thực sự chỉ 25%, nhưng nó vẫn được coi là một phần của vấn đề. nếu bạn chỉ vượt ra ngoài điểm bùng phát, có thể có một khối lượng bộ nhớ khổng lồ được phân bổ cho danh sách không được sử dụng.

+0

Đó là một gợi ý tốt, nhưng TStringList.Grow chỉ tăng kích thước thêm 25% mỗi lần. Vì vậy, chi phí cao nhất là do đây là 25%. – lkessler

Các vấn đề liên quan