2010-03-07 31 views
6

Nếu tôi muốn phân bổ hàng triệu đối tượng của một lớp Foo và tôi muốn có bộ nhớ và hiệu quả về thời gian, tôi nên thiết kế lớp học Foo như thế nào?Làm thế nào để thiết kế một lớp thích hợp cho hàng triệu phân bổ?

Rõ ràng, Foo không được chứa nhiều dữ liệu thành viên.

Ngoài ra, tôi đoán, nó không nên sử dụng các chức năng ảo?

Và chi phí là bao nhiêu cho Foo để lấy được từ một lớp Cơ sở? Và từ một số lớp cơ sở?

Có mẹo nào khác để tạo hàng triệu đối tượng Foo rất hiệu quả không?

+0

Có chức năng ảo chỉ thêm 4 byte vào đối tượng (8 cho 64 bit). Bắt nguồn từ lớp cơ sở không có chi phí gì cả. (Những giả định đơn thừa kế.) – kennytm

+1

Bạn cũng nên xem xét việc phân bổ nhanh hơn bằng cách sử dụng tùy chỉnh cấp phát. – yesraaj

+3

Việc bạn nên làm phụ thuộc vào những gì bạn đang làm. Bạn đang làm gì đấy? – GManNickG

Trả lời

8

Tôi không nghĩ có nhiều điều để nói về việc thiết kế lớp học của bạn cho hàng triệu lượt phân bổ. Có, có giới hạn bộ nhớ rõ ràng, vì vậy nếu bạn có một số lượng sửa chữa bộ nhớ này có thể là một mối quan tâm thực sự cho bạn, nếu không bạn sẽ luôn luôn có nguy cơ hết bộ nhớ. Một con trỏ đến bảng ảo chỉ là, một con trỏ (4 hoặc 8 byte trên kiến ​​trúc 32 bit hoặc 64 bit), không chắc chắn đây là trường hợp trong nhiều thừa kế. Việc gọi các hàm ảo có phí trên của việc tra cứu ảo (và thêm bộ nhớ cache bỏ lỡ, nếu gần đây bạn không sử dụng nó), nhưng chỉ cho các hàm ảo và chúng có thể không bao giờ được gạch chân.

Nếu có nhiều giá trị lặp lại, bạn có thể muốn xem xét việc có cấu trúc dữ liệu riêng biệt (mẫu cân). Và để có hiệu quả, hãy đảm bảo rằng bạn có một nhà xây dựng và các nhà khai thác gán trọng lượng nhẹ (đặc biệt), đặc biệt nếu bạn dự định sử dụng các vectơ stl và tương tự.

Đây là tất cả những thứ khá đơn giản, vì vậy bây giờ bạn đã gợi ý thật của tôi:

gì thực sự sẽ giết quản lý bộ nhớ của bạn là nếu bạn nhận được sự phân mảnh, nơi mà tất cả của một đột ngột bạn có thể có một loạt các bộ nhớ, nhưng vẫn còn hư không để đưa các đối tượng của bạn (không đủ không gian tiếp giáp). Nếu bạn có nhiều phân bổ xen kẽ, điều này có thể trở thành một vấn đề thực sự, do đó bạn có thể muốn xem xét phân bổ các khối đối tượng lớn, giữ chúng trong một nhóm và sử dụng lại. Hoặc sử dụng một phân bổ tùy chỉnh (new-operator), nơi bạn preallocate một khối bộ nhớ mà là một bội số của kích thước đối tượng của bạn, và sử dụng nó cho các đối tượng của bạn.

+3

Tôi đồng ý. Flyweight + object-pool & object tái sử dụng. –

+0

Đồng ý. Ngoài ra, nếu có bất kỳ cách nào có thể để làm cho chúng bất biến, làm như vậy sẽ cho phép tái sử dụng. Nó có thể có được để có được với hàng ngàn đối tượng được tham chiếu trong hàng triệu địa điểm. Khó biết mà không cần biết thêm thông tin về miền sự cố. – kyoryu

3

Trong phạm vi mà một đối tượng có thể được cho là có hiệu quả, nó phải là nhỏ nếu rất nhiều được tạo ra, và các hoạt động phổ biến trên nó nên được inlineable nếu nhiều cuộc gọi được thực hiện. Về mặt bộ nhớ, các hàm ảo thường tốn 4 hoặc 8 byte (kích thước của một con trỏ) cho mỗi đối tượng cho đối tượng đầu tiên, miễn phí sau đó. Như những người khác đã nói, Flyweight là một cách để làm cho các đối tượng nhỏ hơn, nếu chúng chứa dữ liệu trùng lặp có thể được chia sẻ. Nếu hàng triệu đối tượng của bạn không chứa dữ liệu trùng lặp, hãy quên nó đi.

Mã đúng hơn là hiệu quả hoặc không hiệu quả. Các cuộc gọi ảo có thể tốn một số chi phí cuộc gọi, nhưng nó lên đến mã gọi, không phải là lớp, bao nhiêu lần mỗi hàm thành viên được gọi. Trong mọi trường hợp, nội tuyến là nơi tăng tốc độ lớn và các chức năng ảo chỉ là một cách cản trở một trang web cuộc gọi cụ thể được hưởng lợi từ nội tuyến. Nếu thiết kế của bạn được đơn giản hóa bởi có 27 hàm thành viên ảo, mỗi hàm được gọi mỗi tháng một lần, nhưng đảm bảo rằng 2 hàm được gọi là hàng triệu lần một giây có thể được người gọi đặt vào, thì không cần phải tránh các chức năng ảo .

Lớp cơ sở có chi phí khá giống với một đối tượng thành viên cùng loại. Với nhiều thừa kế, static_cast có thể không còn là một no-op vì nó thường là dành cho kế thừa đơn, nhưng điều đó có lẽ không đáng lo ngại. Thừa kế ảo và dynamic_cast cả hai có thể nhận được thú vị về số lượng công việc được thực hiện tại thời gian chạy, nhưng chỉ trên một quy mô tương tự như các chức năng thành viên ảo.

Tất cả những gì đã nói, điều chính bạn muốn làm là lấy một số mã chạy càng sớm càng tốt mà bắt chước hợp lý các đặc tính tạo và gọi đối tượng mà mã hoàn thành của bạn sẽ có. Sau đó, bạn sẽ biết loại hiệu suất mà bạn đang xem - nếu hoạt động quan trọng của bạn xuất hiện ít hoặc đủ nhanh với các chức năng ảo trong, không có điểm nào nảy ra với một số lược đồ thiết kế bị méo chỉ để tránh chúng. Nếu profiler của bạn cho bạn biết rằng tất cả thời gian đang được chi tiêu tạo ra các đối tượng, không sử dụng chúng một khi bạn có chúng, thì bạn cần phải xem xét phân bổ, chứ không phải chức năng của thành viên.

Lý do để ước tính sớm chỉ là bạn không muốn lãng phí thời gian thiết kế thứ gì đó chắc chắn sẽ không hoạt động. Vì vậy, các tiêu chuẩn cơ bản ("tôi có thể gọi new một nghìn lần một giây? Một triệu lần một giây? Chỉ cần nhanh từ nhiều chủ đề đồng thời?") Có thể nạp vào quá trình thiết kế, nhưng sau đó tất nhiên bạn không thể tối ưu hóa đúng cho đến khi bạn có một phiên bản chính đáng của mã thực sự của bạn.

4

Điều chính là giảm thiểu sử dụng newdelete bằng cách giữ các đối tượng đã sử dụng trong một hồ bơi và sử dụng lại chúng. Đừng lo lắng về các chức năng ảo. Chi phí gọi chúng thường là tương đối nhỏ so với bất kỳ điều gì khác đang xảy ra trong chương trình của bạn.

+0

Và chi phí bộ nhớ của các chức năng ảo là tối thiểu - một con trỏ duy nhất cho mỗi đối tượng (với vftable). Bạn sẽ có thể loại bỏ điều này bằng cách không có chức năng ảo. – kyoryu

+0

@kyoryu: Đúng vậy. Và tôi không thể biết số lượng của các đối tượng hoạt động ổn định là gì, vì vậy tôi không thể biết được kích thước có phải là một vấn đề hay không. –

3

Chỉ cần làm rõ một điểm trên phân bổ tùy chỉnh.

Với mặc định new, bạn có khả năng nhận được khá nhiều chi phí, nghĩa là, bộ nhớ bổ sung được cấp phát trên đầu trang của sizeof(Foo). Điều bạn thực sự muốn xảy ra là lan truyền chi phí này qua nhiều số Foo s.

Ý tưởng là thực hiện một cuộc gọi tới new để phân bổ một khối byte đủ lớn để giữ 100 hoặc 1000 (hoặc nhiều hơn?) Liền kề Foo s và sau đó phân bổ Foo s.

Nếu bạn giữ một hồ bơi được phân bổ trước Foo s, bạn có thể vẫn phải chịu phí trên bộ nhớ trên mỗi trường hợp, ngay cả khi nó nhanh hơn.

In The C++ pl2e, Stroustrup nói về byte 'trên máy tính của tôi', vì vậy bạn sẽ phải làm các thí nghiệm tự hỏi: Bao nhiêu bộ nhớ là thực sự đưa lên để phân bổ một đơn Foo với new?

Tra cứu bộ phân bổ hồ bơi (ví dụ Boost) hoặc bộ phân bổ đối tượng nhỏ (ví dụ: Loki).

+0

Điểm tốt, nhưng bạn không cần phải nhìn xa hơn tiêu chuẩn. 'std :: deque ' sẽ phân bổ nhiều khối liên tiếp của các đối tượng T. Số lượng T của mỗi khối được tối ưu hóa bởi nhà cung cấp trình biên dịch của bạn, người có lẽ hiểu được nền tảng của bạn. – MSalters

1

Về việc phân bổ đối tượng lớp học của bạn, hãy xem thư viện tăng Pool, có thể hiệu quả hơn cho nhiều phân bổ đối tượng nhỏ hơn thông thường mới/xóa thông qua hệ thống cấp phát. Nó cũng có thể giữ chúng gần nhau hơn trong bộ nhớ. Các fast_pool_allocator là rất tiện dụng như là một thực hiện cấp phát C++ mà bạn có thể dễ dàng thả vào ứng dụng của bạn để đánh giá lợi ích. Tôi muốn đề nghị sử dụng phân bổ anyway, vì nó sẽ làm cho nó dễ dàng hơn để so sánh các chương trình phân bổ khác nhau.Một điều cần xem xét là khi các đối tượng sẽ được tạo ra - ví dụ bạn có biết trước số lượng bạn cần (và do đó hệ thống gộp/tái sử dụng như được mô tả bởi những người khác sẽ hữu ích hay không?)), hoặc cho dù bạn chỉ biết rằng sẽ có O (1m) trong số họ yêu cầu tại các điểm khác nhau. Tạo số lượng lớn trong một lần nên nhanh hơn rất nhiều so với việc phân bổ nhiều số riêng lẻ. Từ công việc của riêng tôi, tôi thường thấy rằng việc cấp phát bộ nhớ lặp lại cho nhiều đối tượng nhỏ xuất hiện như một nút cổ chai lớn trong lược tả.

Tôi khuyên bạn nên gian lận một ứng dụng thử nghiệm sẽ mô phỏng số lượng phân bổ bạn cần và đánh giá các chiến lược khác nhau. Bạn có thể cần phải kết hợp nhiều hơn một.

1

Những người khác đã đề cập đến mẫu Flyweight, nhưng vì bạn đã gắn thẻ cho C++, tôi sẽ xem xét Boost.Flyweight. Thiết kế của nó phù hợp với nhu cầu của bạn và nếu bạn cần phát minh lại bánh xe, bạn luôn có thể kiểm tra nguồn của nó để biết chi tiết.

1

Đối với những gì đáng giá ... bạn cũng có thể thu được từ việc sử dụng thành ngữ khác.

Tôi biết mẫu Flyweight hoàn toàn là cơn thịnh nộ, nhưng ở đây bạn cũng có thể hưởng lợi từ việc không phân bổ hàng triệu đối tượng đó.

Nếu có vẻ lạ, hãy nghĩ đến đối tượng String trong Python. Như trong nhiều ngôn ngữ gần đây, String là không thay đổi trong Python. Tất nhiên, đối tượng bạn thao tác có thể thay đổi, nhưng thực tế String không: tay cầm của bạn chỉ đơn giản là di chuyển.

Tất nhiên, Python có bộ sưu tập rác tự động giúp việc này trở nên dễ dàng hơn nhiều, nhưng nó cũng có thể làm việc cho bạn. Dưới đây là một phác thảo:

class FooImpl; 

class Foo 
{ 
public: 
    explicit Foo(int i): mImpl(FooImpl::Build(i)) {} 

    int foo() const { return mImpl->foo(); } 

    void foo(int i) { mImpl = mImpl->foo(i); } 

private: 
    const FooImpl* mImpl; 
}; // class Foo 

class FooImpl 
{ 
public: 
    static const FooImpl* Build(int i) 
    { 
    typedef std::unordered_set<FooImpl> foos_type; 
    FooImpl tmp(i); 
    foos_type::iterator it = gFooImplCollection.insert(tmp); 
    return &(*it); 
    } 

    int foo() const { return mFoo; } 

    const FooImpl* foo(int i) const 
    { 
    return Build(i); 
    } 

    // Useful thingy 
    bool operator==(const FooImpl& rhs) const { return mFoo == rhs.mFoo; } 
    size_t hash() const { return mFoo; } 

private: 
    explicit FooImpl(int i): mFoo(i) {} 

    int mFoo; 
}; 

std::unordered_set<FooImpl> gFooImplCollection; 

Tất nhiên, điều này rất thô, chỉ để cung cấp cho bạn một ý tưởng. Nếu số lượng các mục khác nhau có thể là quan trọng, bạn cần Bộ sưu tập rác.

Garbage Collection là một chủ đề khác, tôi thích để lại cho bạn với ý tưởng của một lớp Immutable lõi (cho thấy chỉ const phương pháp) và một tay cầm có thể thay đổi (mà chỉ đơn giản là thay đổi lớp lõi nó trỏ tới khi yêu cầu thay đổi).

Và bây giờ mà bạn đã dành thời gian để đọc, Boost có nó: Boost.Flyweight :)

Lưu ý:

Điều quan trọng là chính xác vẻ như, vì Foo là vụ phải được phân bổ (trên ngăn xếp) hàng triệu lần, kích thước của nó sẽ càng gần với con trỏ càng tốt. Điều này được thực hiện bằng cách sử dụng Intrusive couting tham chiếu (tôi hy vọng đó là những gì Boost làm). Ngoài ra, không cần thiết phải có các phương thức virtual trong Foo, virtual nằm trong số FooImpl và thực tế có thể gọi AbstractFactory phía sau hậu trường.

Như vậy, kể từ Foo:

  • không có bất kỳ lớp cơ sở
  • không có bất kỳ phương pháp ảo
  • chỉ có một thuộc tính (một con trỏ)

kích thước hiệu quả của nó sẽ là kích thước của con trỏ ... đó là tốt nhất bạn có thể hy vọng nếu bạn không muốn lưu trữ một id một chi phí tra cứu incur tại mỗi cuộc gọi :)

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