Một biến thể pimpl có thể được thực hiện mà không có bất kỳ hình phạt hiệu suất?


9

Một trong những vấn đề của pimpl là hình phạt hiệu năng của việc sử dụng nó (cấp phát bộ nhớ bổ sung, thành viên dữ liệu không liền kề, bổ sung bổ sung, v.v.). Tôi muốn đề xuất một biến thể về thành ngữ pimpl sẽ tránh các hình phạt hiệu suất này với chi phí không nhận được tất cả các lợi ích của pimpl. Ý tưởng là để lại tất cả các thành viên dữ liệu riêng tư trong lớp và chỉ di chuyển các phương thức riêng tư sang lớp pimpl. Lợi ích so với pimpl cơ bản là bộ nhớ vẫn tiếp giáp nhau (không có chỉ định bổ sung). Những lợi ích so với việc không sử dụng pimpl là:

  1. Nó ẩn các chức năng riêng tư.
  2. Bạn có thể cấu trúc nó để tất cả các chức năng này sẽ có liên kết nội bộ và cho phép trình biên dịch tối ưu hóa mạnh mẽ hơn nó.

Vì vậy, ý tưởng của tôi là làm cho pimpl thừa hưởng từ chính lớp học (nghe có vẻ hơi điên rồ tôi biết, nhưng hãy chịu đựng tôi). Nó sẽ trông giống như thế này:

Trong tập tin Ah:

class A
{
    A();
    void DoSomething();
protected:  //All private stuff have to be protected now
    int mData1;
    int mData2;
//Not even a mention of a PImpl in the header file :)
};

Trong tệp A.cpp:

#define PCALL (static_cast<PImpl*>(this))

namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
    static_assert(sizeof(PImpl) == sizeof(A), 
                  "Adding data members to PImpl - not allowed!");
    void DoSomething1();
    void DoSomething2();
    //No data members, just functions!
};

void PImpl::DoSomething1()
{
    mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
    DoSomething2();
}

void PImpl::DoSomething2()
{
    mData2 = baz();
}

}
A::A(){}

void A::DoSomething()
{
    mData2 = foo();
    PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}

Theo như tôi thấy thì hoàn toàn không có hình phạt về hiệu suất khi sử dụng cái này so với không có pimpl và một số hiệu suất có thể đạt được và giao diện tệp tiêu đề sạch hơn. Một nhược điểm này có so với pimpl tiêu chuẩn là bạn không thể ẩn các thành viên dữ liệu để các thay đổi đối với các thành viên dữ liệu đó sẽ vẫn kích hoạt việc biên dịch lại mọi thứ phụ thuộc vào tệp tiêu đề. Nhưng theo cách tôi nhìn thấy, nó sẽ mang lại lợi ích đó hoặc lợi ích hiệu suất của việc các thành viên tiếp giáp nhau trong bộ nhớ (hoặc thực hiện hack này- "Tại sao Nỗ lực số 3 là đáng tin cậy"). Một cảnh báo khác là nếu A là một lớp templated, cú pháp sẽ gây khó chịu (bạn biết, bạn không thể sử dụng trực tiếp mData1, bạn cần phải làm điều này-> mData1 và bạn cần bắt đầu sử dụng tên kiểu chữ và có thể từ khóa mẫu cho các loại phụ thuộc và các kiểu templated, vv). Một cảnh báo khác là bạn không còn có thể sử dụng riêng tư trong lớp gốc, chỉ các thành viên được bảo vệ, vì vậy bạn không thể hạn chế quyền truy cập từ bất kỳ lớp kế thừa nào, không chỉ là pimpl. Tôi đã cố gắng nhưng không thể khắc phục được vấn đề này. Ví dụ, tôi đã thử biến pimpl thành một lớp mẫu bạn bè với hy vọng làm cho khai báo bạn bè đủ rộng để cho phép tôi xác định lớp pimpl thực tế trong một không gian tên ẩn danh, nhưng điều đó không hiệu quả. Nếu bất cứ ai có bất kỳ ý tưởng nào về cách giữ các thành viên dữ liệu riêng tư mà vẫn cho phép một lớp pimpl kế thừa được xác định trong một tên nơi ẩn danh để truy cập vào các thành viên đó, tôi thực sự muốn thấy nó! Điều đó sẽ loại bỏ đặt phòng chính của tôi từ việc sử dụng này.

Mặc dù vậy, tôi cảm thấy rằng những cảnh báo này được chấp nhận vì lợi ích của những gì tôi đề xuất.

Tôi đã thử tìm kiếm trực tuyến một số tài liệu tham khảo về thành ngữ "chỉ có chức năng" này nhưng không thể tìm thấy bất cứ điều gì. Tôi thực sự quan tâm đến những gì mọi người nghĩ về điều này. Có vấn đề nào khác với điều này hoặc lý do tôi không nên sử dụng điều này?

CẬP NHẬT:

Tôi đã tìm thấy đề xuất này rằng ít nhiều cố gắng hoàn thành chính xác những gì tôi đang có, nhưng thực hiện bằng cách thay đổi tiêu chuẩn. Tôi hoàn toàn đồng ý với đề xuất đó và hy vọng nó sẽ biến nó thành tiêu chuẩn (tôi không biết gì về quy trình đó nên tôi không biết khả năng đó sẽ xảy ra như thế nào). Tôi muốn có nhiều hơn để làm điều này thông qua một cơ chế ngôn ngữ được xây dựng. Đề xuất cũng giải thích những lợi ích của những gì tôi đang cố gắng đạt được tốt hơn tôi nhiều. Nó cũng không có vấn đề phá vỡ đóng gói như đề xuất của tôi (riêng tư -> được bảo vệ). Tuy nhiên, cho đến khi đề xuất đó biến nó thành tiêu chuẩn (nếu điều đó từng xảy ra), tôi nghĩ đề xuất của tôi làm cho nó có thể nhận được những lợi ích đó, với những cảnh báo mà tôi đã liệt kê.

CẬP NHẬT2:

Một trong những câu trả lời đề cập đến LTO như một giải pháp thay thế khả thi để nhận được một số lợi ích (tôi tối ưu hóa mạnh mẽ hơn tôi đoán). Tôi không thực sự chắc chắn chính xác những gì diễn ra trong các lần tối ưu hóa trình biên dịch khác nhau nhưng tôi có một chút kinh nghiệm với mã kết quả (tôi sử dụng gcc). Đơn giản chỉ cần đặt các phương thức riêng trong lớp gốc sẽ buộc chúng phải có liên kết bên ngoài.

Tôi có thể sai ở đây, nhưng cách tôi diễn giải đó là trình tối ưu hóa thời gian biên dịch không thể loại bỏ hàm ngay cả khi tất cả các trường hợp cuộc gọi của nó được đặt hoàn toàn bên trong TU đó. Vì một số lý do, ngay cả LTO cũng từ chối loại bỏ định nghĩa hàm ngay cả khi có vẻ như tất cả các trường hợp cuộc gọi trong toàn bộ nhị phân được liên kết đều được nội tuyến. Tôi tìm thấy một số tài liệu tham khảo nói rằng đó là vì trình liên kết không biết liệu bằng cách nào đó bạn vẫn gọi hàm bằng cách sử dụng các hàm con trỏ (mặc dù tôi không hiểu tại sao trình liên kết không thể hiểu rằng địa chỉ của phương thức đó không bao giờ được sử dụng ).

Đây không phải là trường hợp nếu bạn sử dụng đề xuất của tôi và đặt các phương thức riêng tư đó trong một pimpl bên trong một không gian tên ẩn danh. Nếu chúng được nội tuyến, các hàm sẽ KHÔNG xuất hiện trong (với -O3, bao gồm -finline-function) tệp đối tượng.

Theo cách tôi hiểu, trình tối ưu hóa, khi quyết định có hay không thực hiện một hàm, có tính đến tác động của nó đối với kích thước mã. Vì vậy, bằng cách sử dụng đề xuất của tôi, tôi sẽ làm cho nó tối ưu hơn một chút "rẻ hơn" để tối ưu hóa nội tuyến các phương thức riêng tư đó.


Việc sử dụng PCALLlà hành vi không xác định. Bạn không thể truyền Atới a PImplvà sử dụng nó trừ khi đối tượng cơ bản thực sự thuộc loại PImpl. Tuy nhiên, trừ khi tôi nhầm, người dùng sẽ chỉ tạo các đối tượng thuộc loại A.
BeeOnRope

Câu trả lời:


8

Các điểm bán của mẫu Pimpl là:

  • tổng số đóng gói: không có thành viên dữ liệu (riêng tư) nào được đề cập trong tệp tiêu đề của đối tượng giao diện.
  • tính ổn định: cho đến khi bạn phá vỡ giao diện công cộng (trong C ++ bao gồm các thành viên riêng), bạn sẽ không bao giờ phải biên dịch lại mã phụ thuộc vào đối tượng giao diện. Điều này làm cho Pimpl trở thành một mô hình tuyệt vời cho các thư viện không muốn người dùng của họ biên dịch lại tất cả mã trên mỗi thay đổi nội bộ.
  • Đa hình và tiêm phụ thuộc: việc thực hiện hoặc hành vi của đối tượng giao diện có thể dễ dàng hoán đổi khi chạy, mà không yêu cầu mã phụ thuộc được biên dịch lại. Tuyệt vời nếu bạn cần chế giễu một cái gì đó cho một bài kiểm tra đơn vị.

Để đạt được hiệu quả này, Pimpl cổ điển bao gồm ba phần:

  • Một giao diện cho đối tượng triển khai, phải công khai và sử dụng các phương thức ảo cho giao diện:

    class IFrobnicateImpl
    {
    public:
        virtual int frobnicate(int) const = 0;
    };

    Giao diện này được yêu cầu phải ổn định.

  • Một đối tượng giao diện ủy nhiệm cho việc thực hiện riêng tư. Nó không phải sử dụng các phương thức ảo. Thành viên duy nhất được phép là một con trỏ để thực hiện:

    class Frobnicate
    {
        std::unique_ptr<IFrobnicateImpl> _impl;
    public:
        explicit Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl = nullptr);
        int frobnicate(int x) const { return _impl->frobnicate(x); }
    };
    
    ...
    
    Frobnicate::Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl /* = nullptr */)
    : _impl(std::move(impl))
    {
        if (!_impl)
            _impl = std::make_unique<DefaultImplementation>();
    }

    Tệp tiêu đề của lớp này phải ổn định.

  • Ít nhất một lần thực hiện

Pimpl sau đó mua cho chúng tôi rất nhiều sự ổn định cho một lớp thư viện, với chi phí phân bổ một đống và gửi thêm ảo.

Làm thế nào để giải pháp của bạn đo lường?

  • Nó đi với đóng gói. Vì các thành viên của bạn được bảo vệ, bất kỳ lớp con nào cũng có thể gây rối với họ.
  • Nó không đi với sự ổn định giao diện. Bất cứ khi nào bạn thay đổi thành viên dữ liệu của mình - và thay đổi đó chỉ là một lần tái cấu trúc - bạn sẽ phải biên dịch lại tất cả các mã phụ thuộc.
  • Nó không đi với lớp công văn ảo, ngăn chặn việc hoán đổi dễ dàng khi thực hiện.

Vì vậy, đối với mọi mục tiêu của mẫu Pimpl, bạn không hoàn thành mục tiêu này. Do đó, không hợp lý khi gọi mô hình của bạn là một biến thể của Pimpl, nó là một lớp bình thường hơn nhiều. Trên thực tế, nó tệ hơn một lớp bình thường vì các biến thành viên của bạn là riêng tư. Và bởi vì diễn viên đó là một điểm mong manh của sự mong manh.

Lưu ý rằng mẫu Pimpl không phải lúc nào cũng tối ưu - một mặt có sự đánh đổi giữa tính ổn định và tính đa hình và mặt khác là sự gọn nhẹ của bộ nhớ. Về mặt ngữ nghĩa, ngôn ngữ không thể có cả hai (không có biên dịch JIT). Vì vậy, nếu bạn tối ưu hóa vi mô để thu gọn bộ nhớ, rõ ràng Pimpl không phải là giải pháp phù hợp cho trường hợp sử dụng của bạn. Bạn cũng có thể ngừng sử dụng một nửa thư viện tiêu chuẩn, vì các lớp vectơ và chuỗi khủng khiếp này liên quan đến việc phân bổ bộ nhớ động ;-)


Về lưu ý cuối cùng của bạn về việc sử dụng chuỗi và vectơ - nó rất gần với sự thật :). Tôi hiểu rằng pimpl có những lợi ích mà đề xuất của tôi đơn giản là không cung cấp. Mặc dù vậy, nó có lợi ích, được trình bày hùng hồn hơn trong liên kết tôi đã thêm vào CẬP NHẬT. Xem xét rằng hiệu suất đóng một phần rất lớn trong trường hợp sử dụng của tôi, làm thế nào bạn có thể so sánh đề xuất của tôi với việc không sử dụng pimpl? Bởi vì đối với trường hợp sử dụng của tôi, đó không phải là đề xuất của tôi so với pimpl, đó là đề xuất của tôi so với không có pimpl, vì pimpl có chi phí hiệu suất mà tôi không muốn phải chịu.
dcmm88

1
@ dcmm88 Nếu hiệu suất là mục tiêu số 1 của bạn, thì rõ ràng những thứ như chất lượng mã và đóng gói là trò chơi công bằng. Điều duy nhất giải pháp của bạn cải thiện so với các lớp bình thường là tránh biên dịch lại khi chữ ký phương thức riêng thay đổi. Trong cuốn sách của tôi, điều đó không quá nhiều, nhưng nếu đây là một lớp nền tảng trong một cơ sở mã lớn, thì việc triển khai kỳ lạ có thể đáng giá. Tham khảo biểu đồ XKCD tiện dụng này để xác định thời gian phát triển bạn có thể chìm vào rút ngắn thời gian biên dịch . Tùy thuộc vào mức lương của bạn, nâng cấp máy tính của bạn có thể rẻ hơn.
amon

1
Nó cũng cho phép các chức năng riêng có liên kết nội bộ, cho phép tối ưu hóa mạnh mẽ hơn.
dcmm88

1
Có vẻ như bạn trưng ra giao diện chung của lớp pimpl trong tệp tiêu đề của lớp gốc. AFAUI toàn bộ ý tưởng là để che giấu càng nhiều càng tốt. Thông thường theo cách tôi thấy pimpl đang được triển khai, tệp tiêu đề chỉ có một khai báo chuyển tiếp của lớp pimpl và cpp có định nghĩa đầy đủ về lớp pimpl. Tôi nghĩ khả năng hoán đổi đi kèm với một mẫu thiết kế khác - mẫu cầu. Vì vậy, tôi không chắc chắn công bằng khi nói đề xuất của tôi không cung cấp cho nó, khi pimpl thường không cung cấp nó.
dcmm88

Xin lỗi vì bình luận cuối cùng. Bạn thực sự có câu trả lời khá lớn, và vấn đề là ở cuối, vì vậy tôi đã bỏ lỡ nó
Bовић

3

Đối với tôi, những lợi thế không vượt trội hơn những nhược điểm.

Ưu điểm :

Nó có thể tăng tốc độ biên dịch, vì nó lưu lại một bản dựng lại nếu chỉ có chữ ký phương thức riêng đã thay đổi. Nhưng việc xây dựng lại là cần thiết nếu chữ ký phương thức công khai hoặc được bảo vệ hoặc thành viên dữ liệu riêng tư đã thay đổi và hiếm khi tôi phải thay đổi chữ ký phương thức riêng mà không chạm vào bất kỳ tùy chọn nào khác.

Nó có thể cho phép tối ưu hóa trình biên dịch mạnh mẽ hơn, nhưng LTO nên cho phép nhiều tối ưu hóa tương tự (ít nhất, tôi nghĩ rằng nó có thể - Tôi không phải là một chuyên gia tối ưu hóa trình biên dịch), ngoài ra còn có thể được tạo ra theo tiêu chuẩn và tự động.

Nhược điểm:

Bạn đã đề cập đến một vài nhược điểm: không thể sử dụng riêng tư và sự phức tạp với các mẫu. Tuy nhiên, đối với tôi, nhược điểm lớn nhất là nó đơn giản là lúng túng: một phong cách lập trình độc đáo, với các bước nhảy kiểu pimpl không chuẩn giữa giao diện và triển khai, sẽ không quen thuộc với bất kỳ người bảo trì nào trong tương lai hoặc các thành viên nhóm mới trong tương lai, và có thể được hỗ trợ kém bởi công cụ (xem, ví dụ, lỗi GDB này ).

Các mối quan tâm tiêu chuẩn về tối ưu hóa được áp dụng ở đây: Bạn đã đo lường rằng các tối ưu hóa mang lại sự cải thiện có ý nghĩa cho hiệu suất của bạn chưa? Hiệu suất của bạn sẽ tốt hơn bằng cách làm điều này hay bằng cách dành thời gian để duy trì điều này và đầu tư nó vào việc định hình các điểm nóng, cải thiện thuật toán, v.v.? Cá nhân, tôi muốn chọn một phong cách lập trình rõ ràng và đơn giản, với giả định rằng nó sẽ giải phóng thời gian để thực hiện tối ưu hóa mục tiêu. Nhưng đó là quan điểm của tôi về các loại mã tôi làm việc - đối với miền vấn đề của bạn, sự đánh đổi có thể khác.

Lưu ý bên lề: Cho phép riêng tư với pimpl chỉ có phương thức

Bạn đã hỏi về cách cho phép các thành viên tư nhân với đề xuất pimpl chỉ có phương pháp của bạn. Thật không may, tôi coi pimpl chỉ có phương pháp là một loại hack, nhưng nếu bạn đã quyết định rằng những ưu điểm vượt trội hơn những nhược điểm, thì bạn cũng có thể chấp nhận hack.

Ah:

#ifndef A_impl
#define A_impl private
#endif

class A
{
public:
    A();
    void DoSomething();
A_impl:
    int mData1;
    int mData2;
};

A.cpp:

#define A_impl public
#include "A.h"

Tôi hơi xấu hổ khi nói, nhưng tôi thích đề xuất riêng tư #define A_impl của bạn :). Tôi cũng không phải là một chuyên gia tối ưu hóa, nhưng tôi có một số kinh nghiệm với LTO và loay hoay với nó thực sự là điều khiến tôi nghĩ ra điều này. Tôi sẽ cập nhật câu hỏi của mình với thông tin tôi có về việc sử dụng nó và tại sao nó vẫn không cung cấp đầy đủ lợi ích mà phương pháp của tôi - chỉ pimpl cung cấp.
dcmm88

@ dcmm88 - Cảm ơn thông tin về LTO. Đó không phải là thứ tôi có nhiều kinh nghiệm.
Josh Kelley

1

Bạn có thể sử dụng std::aligned_storageđể khai báo lưu trữ cho pimpl của bạn tại lớp giao diện.

class A
{
std::aligned_storage< 128 > _storage;
public:
  A();
};

Trong quá trình triển khai, bạn có thể xây dựng lớp Pimpl tại chỗ _storage:

class Pimpl
{
  int _some_data;
};

A::A()
{
  ::new(&_storage) Pimpl();
  // accessing Pimpl: *(Pimpl*)(_storage);
}

A::~A()
{
  ((Pimpl*)(_storage))->~Pimpl(); // calls destructor for inline pimpl
}

Quên đề cập đến việc bạn cần tăng kích thước lưu trữ một lần nữa, do đó buộc phải biên dịch lại. Bạn thường sẽ có thể siết chặt nhiều thay đổi không kích hoạt tính năng biên dịch lại - nếu bạn cho bộ lưu trữ hơi chậm.
Fabio

0

Không, bạn không thể thực hiện nó mà không bị phạt hiệu suất. PIMPL, về bản chất, đó là một hình phạt về hiệu suất, khi bạn đang áp dụng một quyết định trong thời gian chạy.

Tất nhiên, điều này phụ thuộc vào chính xác những gì bạn muốn gián tiếp. Một số thông tin đơn giản là không được người tiêu dùng sử dụng - như chính xác những gì bạn định đặt vào 64 byte được căn chỉnh 4 byte của bạn. Nhưng các thông tin khác - giống như thực tế là bạn muốn 64 byte được liên kết 4 byte cho đối tượng của bạn.

PIMPL chung mà không có hình phạt hiệu suất sẽ không tồn tại và sẽ không bao giờ tồn tại. Đó là cùng một thông tin mà bạn từ chối với người dùng của mình rằng họ muốn sử dụng để tối ưu hóa. Nếu bạn đưa nó cho họ, IMPL của bạn không được trừu tượng hóa; nếu bạn từ chối nó với họ, họ không thể tối ưu hóa. Bạn có thể có nó trong cả hai cách.


Tôi đã cung cấp một đề xuất "pimpl một phần" có thể mà tôi cho rằng không có hình phạt về hiệu suất và có thể đạt được hiệu suất. Bạn có thể bực bội khi gọi tôi là bất cứ điều gì liên quan đến pimpl, nhưng tôi đang che giấu một số chi tiết thực hiện (các phương thức riêng tư). Tôi có thể gọi nó là ẩn thực hiện phương thức riêng tư, nhưng đã chọn gọi nó là một biến thể của pimpl, hoặc pimpl chỉ phương thức.
dcmm88

0

Với tất cả sự tôn trọng và không có bất kỳ ý định giết chết sự phấn khích này, tôi không thấy bất kỳ lợi ích thiết thực nào mà điều này phục vụ từ quan điểm thời gian biên dịch. Rất nhiều lợi ích pimplssẽ đến từ việc ẩn các chi tiết loại do người dùng định nghĩa. Ví dụ:

struct Foo
{
    Bar data;
};

... trong trường hợp như vậy, chi phí biên dịch nặng nhất xuất phát từ thực tế là, để xác định Foo, chúng ta phải biết các yêu cầu về kích thước / căn chỉnh của Bar(có nghĩa là chúng ta phải yêu cầu định nghĩa đệ quy Bar).

Nếu bạn không ẩn các thành viên dữ liệu, thì một trong những lợi ích quan trọng nhất từ ​​phối cảnh thời gian biên dịch sẽ bị mất. Ngoài ra còn có một số mã có vẻ nguy hiểm tiềm tàng ở đó nhưng tiêu đề không trở nên nhẹ hơn và tệp nguồn ngày càng nặng hơn với nhiều chức năng chuyển tiếp hơn, do đó, nó có khả năng tăng, thay vì giảm, tổng thời gian biên dịch.

Tiêu đề nhẹ hơn là chìa khóa

Để giảm thời gian xây dựng, bạn muốn có thể hiển thị một kỹ thuật dẫn đến tiêu đề trọng lượng nhẹ hơn đáng kể (thường bằng cách cho phép bạn không còn đệ quy #includemột số tiêu đề khác vì bạn ẩn các chi tiết không còn yêu cầu định struct/classnghĩa nhất định). Đó là nơi mà chính hãng pimplscó thể có một tác động đáng kể, phá vỡ các chuỗi bao gồm tiêu đề và mang lại các tiêu đề độc lập hơn nhiều với tất cả các chi tiết riêng tư được ẩn đi.

Cách an toàn hơn

Nếu bạn muốn làm một cái gì đó như thế này, thì việc sử dụng một friendđịnh nghĩa trong tệp nguồn của bạn sẽ đơn giản hơn rất nhiều so với một lớp kế thừa cùng một lớp mà bạn không thực sự khởi tạo bằng các thủ thuật truyền con trỏ để gọi các phương thức trên một đối tượng không có căn cứ hoặc đơn giản là sử dụng các hàm độc lập với liên kết bên trong tệp nguồn nhận các tham số thích hợp để thực hiện các công việc cần thiết (ít nhất có thể cho phép bạn ẩn một số phương thức riêng tư khỏi tiêu đề để tiết kiệm rất nhỏ trong thời gian biên dịch và một chút phòng ngọ nguậy để tránh biên dịch xếp tầng).

Phân bổ cố định

Nếu bạn muốn loại pimpl rẻ nhất, mẹo chính là sử dụng bộ cấp phát cố định. Đặc biệt là khi tổng hợp các pimpl với số lượng lớn, kẻ giết người lớn nhất là mất địa phương không gian và các lỗi trang bắt buộc bổ sung khi truy cập pimpl lần đầu tiên. Bằng cách phân bổ các nhóm bộ nhớ giúp loại bỏ bộ nhớ cho các pimpl được phân bổ và trả lại bộ nhớ cho nhóm thay vì giải phóng bộ nhớ đó, chi phí cho một khối lượng cá thể pimpl giảm đáng kể. Mặc dù vậy, nó vẫn không miễn phí từ quan điểm hiệu năng, nhưng rẻ hơn rất nhiều và thân thiện với bộ nhớ cache / trang 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.