Quản lý bộ nhớ để truyền tin nhắn nhanh giữa các luồng trong C ++


9

Giả sử có hai luồng, giao tiếp bằng cách gửi không đồng bộ các thông điệp dữ liệu cho nhau. Mỗi luồng có một số loại hàng đợi tin nhắn.

Câu hỏi của tôi ở mức rất thấp: Điều gì có thể được dự kiến ​​là cách hiệu quả nhất để quản lý bộ nhớ? Tôi có thể nghĩ ra một số giải pháp:

  1. Người gửi tạo đối tượng thông qua new. Nhận cuộc gọi delete.
  2. Tập hợp bộ nhớ (để chuyển bộ nhớ trở lại cho người gửi)
  3. Thu gom rác (ví dụ: Boehm GC)
  4. (nếu các đối tượng đủ nhỏ) sao chép theo giá trị để tránh phân bổ heap hoàn toàn

1) là giải pháp rõ ràng nhất, vì vậy tôi sẽ sử dụng nó cho một nguyên mẫu. Rất có thể là nó đã đủ tốt rồi. Nhưng độc lập với vấn đề cụ thể của tôi, tôi tự hỏi kỹ thuật nào hứa hẹn nhất nếu bạn đang tối ưu hóa cho hiệu suất.

Tôi hy vọng việc gộp chung về mặt lý thuyết là tốt nhất, đặc biệt là vì bạn có thể sử dụng kiến ​​thức bổ sung về luồng thông tin giữa các luồng. Tuy nhiên, tôi sợ rằng đó cũng là điều khó khăn nhất để có được quyền. Rất nhiều điều chỉnh ... :-(

Việc thu gom rác phải khá dễ dàng để thêm vào sau đó (sau giải pháp 1) và tôi hy vọng nó sẽ hoạt động rất tốt. Vì vậy, tôi đoán rằng đó là giải pháp thiết thực nhất nếu 1) hóa ra quá kém hiệu quả.

Nếu các đối tượng nhỏ và đơn giản, sao chép theo giá trị có thể là nhanh nhất. Tuy nhiên, tôi sợ rằng nó buộc các hạn chế không cần thiết trong việc thực hiện các tin nhắn được hỗ trợ, vì vậy tôi muốn tránh nó.

Câu trả lời:


9

Nếu các đối tượng nhỏ và đơn giản, sao chép theo giá trị có thể là nhanh nhất. Tuy nhiên, tôi sợ rằng nó buộc các hạn chế không cần thiết trong việc thực hiện các tin nhắn được hỗ trợ, vì vậy tôi muốn tránh nó.

Nếu bạn có thể dự đoán giới hạn trên char buf[256], ví dụ: Một giải pháp thay thế thực tế nếu bạn không thể chỉ gọi phân bổ heap trong các trường hợp hiếm gặp:

struct Message
{
    // Stores the message data.
    char buf[256];

    // Points to 'buf' if it fits, heap otherwise.
    char* data;
};

3

Nó sẽ phụ thuộc vào cách bạn thực hiện các hàng đợi.

Nếu bạn đi với một mảng (kiểu vòng tròn), bạn cần đặt giới hạn trên về kích thước cho giải pháp 4. Nếu bạn đi với một hàng đợi được liên kết, bạn cần các đối tượng được phân bổ.

Sau đó, việc tổng hợp tài nguyên có thể được thực hiện dễ dàng khi bạn chỉ cần thay thế cái mới và xóa bằng AllocMessage<T>freeMessage<T>. Đề nghị của tôi sẽ là giới hạn số lượng kích thước tiềm năng Tcó thể có và làm tròn khi phân bổ bê tông messages.

Bộ sưu tập rác thẳng có thể hoạt động nhưng điều đó có thể gây ra tạm dừng lâu khi cần thu thập một phần lớn và (tôi nghĩ) sẽ hoạt động kém hơn một chút so với mới / xóa.


3

Nếu nó có trong C ++, chỉ cần sử dụng một trong những con trỏ thông minh - unique_ptr sẽ hoạt động tốt cho bạn, vì nó sẽ không xóa đối tượng cơ bản cho đến khi không ai xử lý được nó. Bạn chuyển đối tượng ptr cho người nhận theo giá trị và không bao giờ phải lo lắng về việc nên xóa luồng nào (trong trường hợp người nhận không nhận được đối tượng).

Bạn vẫn cần xử lý khóa giữa các luồng nhưng hiệu suất sẽ tốt vì không có bộ nhớ được sao chép (chỉ có chính đối tượng ptr, rất nhỏ).

Phân bổ bộ nhớ trên heap không phải là điều nhanh nhất từ ​​trước đến nay, do đó, pooling được sử dụng để làm cho việc này nhanh hơn nhiều. Bạn chỉ cần lấy khối tiếp theo từ một đống có kích thước sẵn trong một hồ bơi, vì vậy chỉ cần sử dụng một thư viện hiện có cho việc này.


2
Khóa thường là một vấn đề lớn hơn nhiều so với sao chép bộ nhớ. Chỉ cần nói.
tdammers

Khi bạn viết unique_ptr, tôi đoán bạn có nghĩa là shared_ptr. Nhưng mặc dù không có nghi ngờ rằng việc sử dụng một con trỏ thông minh là tốt cho việc quản lý tài nguyên, nhưng điều đó không thay đổi thực tế rằng bạn đang sử dụng một số hình thức cấp phát và phân bổ bộ nhớ. Tôi nghĩ rằng câu hỏi này là cấp thấp hơn.
5gon12eder

3

Hiệu suất lớn nhất đạt được khi giao tiếp một đối tượng từ luồng này sang luồng khác là chi phí của việc lấy khóa. Đây là thứ tự của một vài micro giây, nhiều hơn đáng kể so với thời gian trung bình một cặp new/ deletemất (theo thứ tự một trăm nano giây). Việc newtriển khai lành mạnh cố gắng tránh bị khóa bằng gần như mọi giá để tránh hiệu suất của chúng bị ảnh hưởng.

Điều đó nói rằng, bạn muốn đảm bảo rằng bạn không cần phải lấy khóa khi giao tiếp các đối tượng từ luồng này sang luồng khác. Tôi biết hai phương pháp chung để đạt được điều này. Cả hai chỉ hoạt động một cách duy nhất giữa một người gửi và một người nhận:

  1. Sử dụng bộ đệm vòng. Cả hai quá trình điều khiển một con trỏ vào bộ đệm này, một là con trỏ đọc, cái còn lại là con trỏ ghi.

    • Người gửi trước tiên kiểm tra xem có chỗ để thêm một phần tử hay không bằng cách so sánh các con trỏ, sau đó thêm phần tử đó, sau đó tăng con trỏ ghi.

    • Người nhận kiểm tra nếu có một phần tử để đọc bằng cách so sánh các con trỏ, sau đó đọc phần tử đó, sau đó tăng con trỏ đọc.

    Các con trỏ cần phải là nguyên tử vì chúng được chia sẻ giữa các luồng. Tuy nhiên, mỗi con trỏ chỉ được sửa đổi bởi một luồng, các nhu cầu khác chỉ đọc quyền truy cập vào con trỏ. Các phần tử trong bộ đệm có thể là chính con trỏ, cho phép bạn dễ dàng định cỡ bộ đệm vòng của mình thành kích thước không thể tạo khối người gửi.

  2. Sử dụng một danh sách liên kết luôn chứa ít nhất một yếu tố. Người nhận có một con trỏ đến phần tử đầu tiên, người gửi có một con trỏ đến phần tử cuối cùng. Những con trỏ không được chia sẻ.

    • Người gửi tạo một nút mới cho danh sách được liên kết, đặt nextcon trỏ của nó thành nullptr. Sau đó, nó cập nhật nextcon trỏ của phần tử cuối cùng để trỏ đến phần tử mới. Cuối cùng, nó lưu trữ phần tử mới trong con trỏ của chính nó.

    • Người nhận theo dõi nextcon trỏ của phần tử đầu tiên để xem có dữ liệu mới nào không. Nếu vậy, nó xóa phần tử đầu tiên cũ, tiến con trỏ của chính nó để trỏ đến phần tử hiện tại và bắt đầu xử lý nó.

    Trong thiết lập này, các nextcon trỏ cần phải là nguyên tử và người gửi phải chắc chắn không bỏ qua phần tử cuối cùng thứ hai sau khi đã đặt nextcon trỏ. Ưu điểm là, tất nhiên, người gửi không bao giờ phải chặn.

Cả hai cách tiếp cận đều nhanh hơn nhiều so với bất kỳ cách tiếp cận dựa trên khóa nào, nhưng chúng đòi hỏi phải thực hiện cẩn thận để làm đúng. Và, tất nhiên, chúng đòi hỏi tính nguyên tử phần cứng riêng của ghi / tải con trỏ; nếu atomic<>việc triển khai của bạn sử dụng khóa trong nội bộ, bạn sẽ phải chịu số phận khá lớn.

Tương tự như vậy, nếu bạn có một vài độc giả và / hoặc nhà văn, bạn sẽ phải chịu nhiều thất vọng: Bạn có thể cố gắng đưa ra một kế hoạch không khóa, nhưng sẽ rất khó để thực hiện tốt nhất. Những tình huống này dễ dàng hơn nhiều để xử lý với một khóa. Tuy nhiên, một khi bạn lấy một khóa, bạn có thể ngừng lo lắng về new/ deletehiệu suất.


+1 Tôi phải kiểm tra giải pháp bộ đệm vòng này như là một giải pháp thay thế cho hàng đợi đồng thời bằng cách sử dụng các vòng lặp CAS. Nghe có vẻ rất hứa hẹn.
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.