Ví dụ hấp dẫn của các cấp phát C ++ tùy chỉnh?


176

Một số lý do thực sự tốt để bỏ std::allocatorqua một giải pháp tùy chỉnh là gì? Bạn đã chạy qua bất kỳ tình huống nào mà nó thực sự cần thiết cho tính chính xác, hiệu suất, khả năng mở rộng, v.v.? Bất kỳ ví dụ thực sự thông minh?

Phân bổ tùy chỉnh luôn là một tính năng của Thư viện chuẩn mà tôi không cần nhiều. Tôi chỉ tự hỏi liệu có ai ở đây trên SO có thể cung cấp một số ví dụ hấp dẫn để biện minh cho sự tồn tại của họ không.

Câu trả lời:


121

Như tôi đã đề cập ở đây , tôi đã thấy bộ cấp phát STL tùy chỉnh của Intel TBB cải thiện đáng kể hiệu năng của một ứng dụng đa luồng chỉ bằng cách thay đổi một

std::vector<T>

đến

std::vector<T,tbb::scalable_allocator<T> >

(đây là một cách nhanh chóng và thuận tiện để chuyển đổi bộ cấp phát để sử dụng các đống riêng tư luồng tiện lợi của TBB; xem trang 7 trong tài liệu này )


3
Cảm ơn vì liên kết thứ hai. Việc sử dụng các cấp phát để thực hiện các đống luồng riêng tư là thông minh. Tôi thích rằng đây là một ví dụ tốt về việc các bộ cấp phát tùy chỉnh có lợi thế rõ ràng trong một kịch bản không bị giới hạn tài nguyên (nhúng hoặc bảng điều khiển).
Naaff

7
Liên kết ban đầu hiện không còn tồn tại, nhưng CiteSeer có PDF: citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.71.8289
Arto Bendiken

1
Tôi phải hỏi: bạn có thể di chuyển một vectơ như vậy vào một chủ đề khác không? (Tôi đoán là không)
sellibitze

@sellibitze: Vì các vectơ đã bị thao túng từ bên trong các nhiệm vụ TBB và được sử dụng lại qua nhiều hoạt động song song và không có gì đảm bảo luồng công nhân TBB nào sẽ nhận nhiệm vụ, tôi kết luận nó hoạt động tốt. Mặc dù lưu ý rằng đó là một số vấn đề lịch sử với công cụ giải phóng TBB được tạo ra trên một luồng trong một luồng khác (rõ ràng là một vấn đề kinh điển với các luồng riêng tư và mô hình phân bổ và phân bổ của người tiêu dùng sản xuất. TBB tuyên bố rằng người cấp phát tránh các vấn đề này nhưng tôi đã thấy khác . Có thể đã sửa trong các phiên bản mới hơn.)
timday

@ArtoBendiken: Liên kết tải xuống tại liên kết của bạn dường như không hợp lệ.
einpoklum

81

Một lĩnh vực mà các bộ cấp phát tùy chỉnh có thể hữu ích là phát triển trò chơi, đặc biệt là trên các bảng điều khiển trò chơi, vì chúng chỉ có một lượng bộ nhớ nhỏ và không có trao đổi. Trên các hệ thống như vậy, bạn muốn đảm bảo rằng bạn có quyền kiểm soát chặt chẽ đối với từng hệ thống con, để một hệ thống không chính xác không thể đánh cắp bộ nhớ từ một hệ thống quan trọng. Những thứ khác như bộ cấp phát hồ bơi có thể giúp giảm phân mảnh bộ nhớ. Bạn có thể tìm thấy một bài viết dài, chi tiết về chủ đề này tại:

EASTL - Thư viện mẫu tiêu chuẩn nghệ thuật điện tử


14
+1 cho liên kết EASTL: "Trong số các nhà phát triển trò chơi, điểm yếu cơ bản nhất [của STL] là thiết kế phân bổ tiêu chuẩn và chính điểm yếu này là yếu tố đóng góp lớn nhất cho việc tạo ra EASTL."
Naaff

65

Tôi đang làm việc trên một bộ cấp phát mmap cho phép các vectơ sử dụng bộ nhớ từ một tệp ánh xạ bộ nhớ. Mục tiêu là có các vectơ sử dụng bộ lưu trữ trực tiếp trong bộ nhớ ảo được ánh xạ bởi mmap. Vấn đề của chúng tôi là cải thiện việc đọc các tệp thực sự lớn (> 10 GB) vào bộ nhớ mà không cần chi phí sao chép, do đó tôi cần bộ cấp phát tùy chỉnh này.

Cho đến nay tôi có bộ xương của một bộ cấp phát tùy chỉnh (xuất phát từ std :: allocator), tôi nghĩ rằng đó là một điểm khởi đầu tốt để viết các bộ cấp phát riêng. Hãy sử dụng đoạn mã này theo bất cứ cách nào bạn muốn:

#include <memory>
#include <stdio.h>

namespace mmap_allocator_namespace
{
        // See StackOverflow replies to this answer for important commentary about inheriting from std::allocator before replicating this code.
        template <typename T>
        class mmap_allocator: public std::allocator<T>
        {
public:
                typedef size_t size_type;
                typedef T* pointer;
                typedef const T* const_pointer;

                template<typename _Tp1>
                struct rebind
                {
                        typedef mmap_allocator<_Tp1> other;
                };

                pointer allocate(size_type n, const void *hint=0)
                {
                        fprintf(stderr, "Alloc %d bytes.\n", n*sizeof(T));
                        return std::allocator<T>::allocate(n, hint);
                }

                void deallocate(pointer p, size_type n)
                {
                        fprintf(stderr, "Dealloc %d bytes (%p).\n", n*sizeof(T), p);
                        return std::allocator<T>::deallocate(p, n);
                }

                mmap_allocator() throw(): std::allocator<T>() { fprintf(stderr, "Hello allocator!\n"); }
                mmap_allocator(const mmap_allocator &a) throw(): std::allocator<T>(a) { }
                template <class U>                    
                mmap_allocator(const mmap_allocator<U> &a) throw(): std::allocator<T>(a) { }
                ~mmap_allocator() throw() { }
        };
}

Để sử dụng, hãy khai báo một thùng chứa STL như sau:

using namespace std;
using namespace mmap_allocator_namespace;

vector<int, mmap_allocator<int> > int_vec(1024, 0, mmap_allocator<int>());

Nó có thể được sử dụng ví dụ để ghi nhật ký bất cứ khi nào bộ nhớ được phân bổ. Cái gì là cần thiết là cấu trúc rebind, khác với container vector sử dụng các phương thức phân bổ / giải quyết siêu lớp.

Cập nhật: Trình phân bổ ánh xạ bộ nhớ hiện có sẵn tại https://github.com/johannervationoma / mmap_allocator và là LGPL. Hãy sử dụng nó cho các dự án của bạn.


17
Chỉ cần ngẩng cao đầu, xuất phát từ std :: allocator không thực sự là cách thành ngữ để viết phân bổ. Thay vào đó, bạn nên nhìn vào allocator_traits, cho phép bạn cung cấp tối thiểu chức năng và lớp đặc điểm sẽ cung cấp phần còn lại. Lưu ý rằng STL luôn sử dụng công cụ cấp phát của bạn thông qua allocator_traits, không trực tiếp, do đó bạn không cần phải tự mình tham khảo allocator_traits Không có nhiều động lực để lấy từ std :: allocator (mặc dù mã này có thể là điểm khởi đầu hữu ích bất kể).
Nir Friedman

25

Tôi đang làm việc với một công cụ lưu trữ MySQL sử dụng c ++ cho mã của nó. Chúng tôi đang sử dụng bộ cấp phát tùy chỉnh để sử dụng hệ thống bộ nhớ MySQL thay vì cạnh tranh với MySQL cho bộ nhớ. Nó cho phép chúng tôi đảm bảo rằng chúng tôi đang sử dụng bộ nhớ khi người dùng định cấu hình MySQL để sử dụng chứ không phải "thêm".


21

Nó có thể hữu ích để sử dụng các cấp phát tùy chỉnh để sử dụng một nhóm bộ nhớ thay vì heap. Đó là một ví dụ trong số nhiều người khác.

Đối với hầu hết các trường hợp, đây chắc chắn là một tối ưu hóa sớm. Nhưng nó có thể rất hữu ích trong các bối cảnh nhất định (thiết bị nhúng, trò chơi, v.v.).


3
Hoặc, khi nhóm bộ nhớ đó được chia sẻ.
Anthony

9

Tôi chưa viết mã C ++ với bộ cấp phát STL tùy chỉnh, nhưng tôi có thể tưởng tượng một máy chủ web được viết bằng C ++, sử dụng bộ cấp phát tùy chỉnh để tự động xóa dữ liệu tạm thời cần thiết để đáp ứng yêu cầu HTTP. Bộ cấp phát tùy chỉnh có thể giải phóng tất cả dữ liệu tạm thời ngay khi phản hồi được tạo.

Một trường hợp sử dụng có thể khác cho một cấp phát tùy chỉnh (mà tôi đã sử dụng) là viết một bài kiểm tra đơn vị để chứng minh rằng hành vi của một chức năng không phụ thuộc vào một phần của đầu vào của nó. Bộ cấp phát tùy chỉnh có thể lấp đầy vùng nhớ với bất kỳ mẫu nào.


5
Có vẻ như ví dụ đầu tiên là công việc của hàm hủy chứ không phải bộ cấp phát.
Michael Dorst

2
Nếu bạn lo lắng về chương trình của mình tùy thuộc vào nội dung ban đầu của bộ nhớ từ heap, thì việc chạy nhanh (tức là qua đêm!) Trong valgrind sẽ cho bạn biết cách này hay cách khác.
cdyson37

3
@anthropomorphic: Bộ hủy và bộ cấp phát tùy chỉnh sẽ hoạt động cùng nhau, bộ hủy sẽ chạy trước, sau đó xóa bộ cấp phát tùy chỉnh, sẽ không gọi miễn phí (...), nhưng sẽ được gọi miễn phí (...) sau đó, khi phục vụ yêu cầu đã kết thúc. Điều này có thể nhanh hơn phân bổ mặc định và giảm phân mảnh không gian địa chỉ.
pts

8

Khi làm việc với GPU hoặc các bộ đồng xử lý khác, đôi khi có ích khi phân bổ cấu trúc dữ liệu trong bộ nhớ chính theo cách đặc biệt . Cách phân bổ bộ nhớ đặc biệt này có thể được thực hiện trong một cấp phát tùy chỉnh một cách thuận tiện.

Lý do tại sao phân bổ tùy chỉnh thông qua thời gian chạy máy gia tốc có thể có lợi khi sử dụng máy gia tốc là như sau:

  1. thông qua phân bổ tùy chỉnh thời gian chạy hoặc trình điều khiển tăng tốc được thông báo về khối bộ nhớ
  2. ngoài ra, hệ điều hành có thể đảm bảo rằng khối bộ nhớ được phân bổ bị khóa trang (một số người gọi đây là bộ nhớ được ghim ), nghĩa là hệ thống con bộ nhớ ảo của hệ điều hành có thể không di chuyển hoặc xóa trang trong hoặc khỏi bộ nhớ
  3. nếu 1. và 2. giữ và truyền dữ liệu giữa khối bộ nhớ bị khóa trang và bộ tăng tốc được yêu cầu, bộ thực thi có thể truy cập trực tiếp dữ liệu vào bộ nhớ chính vì nó biết nó ở đâu và có thể chắc chắn hệ điều hành không di chuyển / loại bỏ nó
  4. việc này sẽ lưu một bản sao bộ nhớ sẽ xảy ra với bộ nhớ được phân bổ theo cách không bị khóa trang: dữ liệu phải được sao chép trong bộ nhớ chính đến khu vực tổ chức khóa trang từ với máy gia tốc có thể khởi tạo việc truyền dữ liệu (thông qua DMA )

1
... không quên các khối bộ nhớ được căn chỉnh trang. Điều này đặc biệt hữu ích nếu bạn đang nói chuyện với một trình điều khiển (tức là với các GPU thông qua DMA) và không muốn rắc rối và chi phí cho việc tính toán bù đắp trong trang cho danh sách phân tán DMA của bạn.
Ngày

7

Tôi đang sử dụng phân bổ tùy chỉnh ở đây; bạn thậm chí có thể nói rằng nó là để làm việc xung quanh việc quản lý bộ nhớ động tùy chỉnh khác.

Bối cảnh: chúng tôi có quá tải cho malloc, calloc, miễn phí và các biến thể khác nhau của toán tử mới và xóa, và trình liên kết vui vẻ làm cho STL sử dụng chúng cho chúng tôi. Điều này cho phép chúng tôi thực hiện những việc như tự động nhóm đối tượng nhỏ, phát hiện rò rỉ, điền phân bổ, điền miễn phí, phân bổ đệm với các lệnh, căn chỉnh dòng bộ đệm cho một số allocs nhất định và miễn phí bị trì hoãn.

Vấn đề là, chúng ta đang chạy trong một môi trường nhúng - không có đủ bộ nhớ xung quanh để thực sự thực hiện kế toán phát hiện rò rỉ đúng cách trong một thời gian dài. Ít nhất, không phải trong RAM tiêu chuẩn - có một đống RAM khác có sẵn ở nơi khác, thông qua các chức năng phân bổ tùy chỉnh.

Giải pháp: viết một cấp tùy chỉnh mà sử dụng các đống mở rộng, và sử dụng nó chỉ trong ruột của kiến trúc theo dõi bộ nhớ bị rò rỉ ... Mọi thứ khác mặc định là quá tải mới / xóa bình thường mà làm theo dõi rò rỉ. Điều này tránh bản thân theo dõi trình theo dõi (và cũng cung cấp một chút chức năng đóng gói bổ sung, chúng tôi biết kích thước của các nút theo dõi).

Chúng tôi cũng sử dụng điều này để giữ dữ liệu hồ sơ chi phí chức năng, vì lý do tương tự; viết một mục nhập cho mỗi chức năng gọi và trả lại, cũng như chuyển mạch luồng, có thể tốn kém nhanh chóng. Bộ cấp phát tùy chỉnh một lần nữa cung cấp cho chúng ta các allocs nhỏ hơn trong vùng bộ nhớ gỡ lỗi lớn hơn.


5

Tôi đang sử dụng một công cụ phân bổ tùy chỉnh để đếm số lượng phân bổ / thỏa thuận trong một phần của chương trình của tôi và đo thời gian cần thiết. Có nhiều cách khác có thể đạt được nhưng phương pháp này rất thuận tiện đối với tôi. Điều đặc biệt hữu ích là tôi có thể sử dụng bộ cấp phát tùy chỉnh chỉ cho một tập hợp con trong các thùng chứa của mình.


4

Một tình huống thiết yếu: Khi viết mã phải hoạt động trên các ranh giới mô-đun (EXE / DLL), điều cần thiết là giữ cho phân bổ và xóa của bạn xảy ra chỉ trong một mô-đun.

Nơi tôi gặp phải đây là một kiến ​​trúc Plugin trên Windows. Ví dụ, điều quan trọng là, nếu bạn truyền một chuỗi std :: qua ranh giới DLL, thì bất kỳ sự phân bổ lại nào của chuỗi xảy ra từ vùng heap nơi nó bắt nguồn, KHÔNG phải là vùng heap trong DLL có thể khác *.

* Thực tế nó phức tạp hơn thế này, như thể bạn đang tự động liên kết với CRT, điều này có thể hoạt động được. Nhưng nếu mỗi DLL có một liên kết tĩnh đến CRT, bạn sẽ hướng đến một thế giới đau khổ, nơi các lỗi phân bổ ảo liên tục xảy ra.


Nếu bạn vượt qua các đối tượng qua ranh giới DLL, bạn nên sử dụng cài đặt Đa luồng (Gỡ lỗi) DLL (/ MD (d)) cho cả hai bên. C ++ không được thiết kế với sự hỗ trợ mô-đun. Ngoài ra, bạn có thể bảo vệ mọi thứ đằng sau giao diện COM và sử dụng CoTaskMem ALLoc. Đây là cách tốt nhất để sử dụng các giao diện plugin không bị ràng buộc với trình biên dịch, STL hoặc nhà cung cấp cụ thể.
gast128

Những người già cai trị cho điều đó là: Đừng làm điều đó. Không sử dụng các loại STL trong DLL API. Và không vượt qua trách nhiệm miễn phí bộ nhớ động qua ranh giới API DLL. Không có C ++ ABI - vì vậy nếu bạn coi mọi DLL là API C, bạn sẽ tránh được cả một lớp các vấn đề tiềm ẩn. Với chi phí của "vẻ đẹp c ++", tất nhiên. Hoặc như nhận xét khác cho thấy: Sử dụng COM. Chỉ đơn giản là C ++ là một ý tưởng tồi.
BitTickler

3

Một ví dụ về thời gian tôi đã sử dụng chúng là làm việc với các hệ thống nhúng rất hạn chế về tài nguyên. Hãy nói rằng bạn có 2k ram miễn phí và chương trình của bạn phải sử dụng một số bộ nhớ đó. Bạn cần lưu trữ 4-5 chuỗi ở đâu đó không phải trên ngăn xếp và ngoài ra, bạn cần có quyền truy cập rất chính xác vào nơi những thứ này được lưu trữ, đây là tình huống mà bạn có thể muốn viết phân bổ của riêng mình. Việc triển khai mặc định có thể phân mảnh bộ nhớ, điều này có thể không được chấp nhận nếu bạn không có đủ bộ nhớ và không thể khởi động lại chương trình của mình.

Một dự án tôi đang thực hiện là sử dụng AVR-GCC trên một số chip có công suất thấp. Chúng tôi đã phải lưu trữ 8 chuỗi có độ dài thay đổi nhưng với mức tối đa đã biết. Các thư viện triển khai chuẩn của công tác quản lý bộ nhớlà một trình bao bọc mỏng xung quanh malloc / free, theo dõi vị trí đặt các mục với nhau bằng cách thêm trước mỗi khối bộ nhớ được phân bổ bằng một con trỏ để chỉ qua phần cuối của bộ nhớ được phân bổ đó. Khi cấp phát một phần bộ nhớ mới, bộ cấp phát tiêu chuẩn phải đi qua từng phần của bộ nhớ để tìm khối tiếp theo khả dụng trong đó kích thước bộ nhớ được yêu cầu sẽ phù hợp. Trên nền tảng máy tính để bàn, điều này sẽ rất nhanh đối với một số mặt hàng này nhưng bạn phải nhớ rằng một số vi điều khiển này rất chậm và so sánh nguyên thủy. Ngoài ra, vấn đề phân mảnh bộ nhớ là một vấn đề lớn có nghĩa là chúng tôi thực sự không có lựa chọn nào khác ngoài cách tiếp cận khác.

Vì vậy, những gì chúng tôi đã làm là để thực hiện nhóm bộ nhớ của chúng ta . Mỗi khối bộ nhớ đủ lớn để phù hợp với chuỗi lớn nhất mà chúng ta sẽ cần trong đó. Điều này được phân bổ các khối bộ nhớ có kích thước cố định trước thời hạn và đánh dấu khối bộ nhớ nào hiện đang được sử dụng. Chúng tôi đã làm điều này bằng cách giữ một số nguyên 8 bit trong đó mỗi bit được biểu diễn nếu một khối nhất định được sử dụng. Chúng tôi đã đánh đổi việc sử dụng bộ nhớ ở đây để cố gắng làm cho toàn bộ quá trình nhanh hơn, trong trường hợp của chúng tôi là hợp lý khi chúng tôi đang đẩy chip vi điều khiển này gần với khả năng xử lý tối đa của nó.

Có một số lần khác tôi có thể thấy việc viết bộ cấp phát tùy chỉnh của riêng bạn trong ngữ cảnh của các hệ thống nhúng, ví dụ: nếu bộ nhớ cho chuỗi không nằm trong ram chính như thường xảy ra trên các nền tảng này .


3

Liên kết bắt buộc để nói chuyện CppCon 2015 của Andrei Alexandrescu về người cấp phát:

https://www.youtube.com/watch?v=LIb3L4vKZ7U

Điều tuyệt vời là chỉ cần nghĩ ra chúng sẽ khiến bạn nghĩ ra ý tưởng về cách bạn sẽ sử dụng chúng :-)


2

Đối với bộ nhớ dùng chung, điều quan trọng không chỉ là đầu chứa mà cả dữ liệu chứa trong bộ nhớ dùng chung.

Bộ cấp phát của Boost :: Inter Process là một ví dụ điển hình. Tuy nhiên, như bạn có thể đọc ở đây, điều này không đủ, để làm cho tất cả các bộ chứa STL tương thích với bộ nhớ (Do các ánh xạ khác nhau trong các quy trình khác nhau, con trỏ có thể "phá vỡ").


2

Cách đây không lâu, tôi thấy giải pháp này rất hữu ích với tôi: Công cụ cấp phát C ++ 11 nhanh cho các thùng chứa STL . Nó tăng tốc một chút các container STL trên VS2017 (~ 5x) cũng như trên GCC (~ 7x). Nó là một cấp phát mục đích đặc biệt dựa trên nhóm bộ nhớ. Nó có thể được sử dụng với các thùng chứa STL chỉ nhờ vào cơ chế bạn yêu cầu.


1

Cá nhân tôi sử dụng Loki :: Allocator / SmallObject để tối ưu hóa việc sử dụng bộ nhớ cho các đối tượng nhỏ - nó cho thấy hiệu quả tốt và đáp ứng hiệu suất nếu bạn phải làm việc với một lượng vừa phải các đối tượng thực sự nhỏ (1 đến 256 byte). Nó có thể hiệu quả gấp ~ 30 lần so với phân bổ mới / xóa C ++ tiêu chuẩn nếu chúng ta nói về việc phân bổ số lượng vừa phải của các đối tượng nhỏ có nhiều kích cỡ khác nhau. Ngoài ra, có một giải pháp dành riêng cho VC có tên là "QuickHeap", nó mang lại hiệu suất tốt nhất có thể (phân bổ và phân bổ các hoạt động chỉ cần đọc và viết địa chỉ của khối được phân bổ / trả về heap, tương ứng với tối đa 99. (9)% trường hợp - phụ thuộc vào cài đặt và khởi tạo), nhưng với chi phí vượt trội đáng chú ý - nó cần hai con trỏ cho mỗi mức độ và thêm một cho mỗi khối bộ nhớ mới. Nó '

Vấn đề với việc triển khai mới / xóa C ++ tiêu chuẩn là nó thường chỉ là một trình bao bọc cho phân bổ C malloc / miễn phí và nó hoạt động tốt cho các khối bộ nhớ lớn hơn, như 1024+ byte. Nó có một chi phí đáng chú ý về hiệu năng và đôi khi, bộ nhớ thêm được sử dụng để ánh xạ. Vì vậy, trong hầu hết các trường hợp, các bộ cấp phát tùy chỉnh được triển khai theo cách tối đa hóa hiệu suất và / hoặc giảm thiểu lượng bộ nhớ bổ sung cần thiết để phân bổ các đối tượng nhỏ (≤1024 byte).


1

Trong một mô phỏng đồ họa, tôi đã thấy các cấp phát tùy chỉnh được sử dụng cho

  1. Các ràng buộc sắp xếp std::allocatorkhông hỗ trợ trực tiếp.
  2. Giảm thiểu sự phân mảnh bằng cách sử dụng các nhóm riêng biệt cho thời gian ngắn (chỉ khung này) và phân bổ dài 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.