Chi phí hiệu suất của việc có một phương thức ảo trong một lớp C ++ là bao nhiêu?


107

Có ít nhất một phương thức ảo trong một lớp C ++ (hoặc bất kỳ lớp cha nào của nó) có nghĩa là lớp đó sẽ có một bảng ảo và mọi cá thể sẽ có một con trỏ ảo.

Vì vậy, chi phí bộ nhớ là khá rõ ràng. Điều quan trọng nhất là chi phí bộ nhớ trên các phiên bản (đặc biệt nếu các phiên bản nhỏ, chẳng hạn nếu chúng chỉ để chứa một số nguyên: trong trường hợp này, việc có một con trỏ ảo trong mọi phiên bản có thể tăng gấp đôi kích thước của các phiên bản. Đối với không gian bộ nhớ được sử dụng hết bởi các bảng ảo, tôi đoán nó thường không đáng kể so với không gian được sử dụng hết bởi mã phương thức thực.

Điều này đưa tôi đến câu hỏi của mình: có chi phí hiệu suất có thể đo lường được (tức là tác động tốc độ) để tạo một phương pháp ảo không? Sẽ có một tra cứu trong bảng ảo trong thời gian chạy, sau mỗi lần gọi phương thức, vì vậy nếu có rất nhiều lần gọi phương thức này và nếu phương thức này rất ngắn, thì có thể có một lần truy cập hiệu suất có thể đo lường được không? Tôi đoán nó phụ thuộc vào nền tảng, nhưng có ai chạy một số điểm chuẩn chưa?

Lý do tôi hỏi là tôi đã gặp lỗi xảy ra do một lập trình viên quên định nghĩa một phương thức ảo. Đây không phải là lần đầu tiên tôi thấy sai lầm kiểu này. Và tôi nghĩ: tại sao chúng ta lại thêm từ khóa ảo khi cần thay vì loại bỏ từ khóa ảo khi chúng ta hoàn toàn chắc chắn rằng nó không cần thiết? Nếu chi phí hiệu suất thấp, tôi nghĩ tôi sẽ chỉ đơn giản đề xuất những điều sau trong nhóm của mình: chỉ cần đặt mọi phương thức ảo theo mặc định, bao gồm cả hàm hủy, trong mọi lớp và chỉ xóa nó khi bạn cần. Điều đó nghe có vẻ điên rồ với bạn?



7
So sánh các cuộc gọi ảo và không ảo là không hiệu quả. Chúng cung cấp các chức năng khác nhau. Nếu bạn muốn so sánh các lệnh gọi hàm ảo với hàm tương đương C, bạn cần thêm chi phí của mã triển khai tính năng tương đương của hàm ảo.
Martin York

Đó là câu lệnh switch hoặc câu lệnh if lớn. Nếu bạn khéo léo, bạn có thể triển khai lại bằng cách sử dụng bảng con trỏ hàm nhưng xác suất sai sẽ cao hơn nhiều.
Martin York


7
Câu hỏi là về các lời gọi hàm không cần ảo, vì vậy việc so sánh là có ý nghĩa.
Mark Ransom

Câu trả lời:


104

Tôi đã chạy một số thời gian trên bộ xử lý PowerPC theo thứ tự 3ghz. Trên kiến ​​trúc đó, một lệnh gọi hàm ảo tốn 7 nano giây lâu hơn một lệnh gọi hàm trực tiếp (không ảo).

Vì vậy, không thực sự đáng lo lắng về chi phí trừ khi hàm giống như một bộ truy cập Get () / Set () tầm thường, trong đó bất kỳ thứ gì khác ngoài nội tuyến đều là lãng phí. Chi phí 7ns trên một chức năng nội tuyến đến 0,5ns là nghiêm trọng; chi phí 7ns trên một chức năng mất 500ms để thực thi là vô nghĩa.

Chi phí lớn của các hàm ảo không thực sự là việc tra cứu con trỏ hàm trong vtable (đó thường chỉ là một chu kỳ đơn), nhưng bước nhảy gián tiếp thường không thể được dự đoán theo nhánh. Điều này có thể gây ra bong bóng đường ống lớn vì bộ xử lý không thể tìm nạp bất kỳ lệnh nào cho đến khi bước nhảy gián tiếp (lệnh gọi thông qua con trỏ hàm) đã ngừng hoạt động và một con trỏ lệnh mới được tính toán. Vì vậy, chi phí của một lệnh gọi hàm ảo lớn hơn nhiều so với khi nhìn vào tổ hợp ... nhưng vẫn chỉ 7 nano giây.

Chỉnh sửa: Andrew, Not Sure và những người khác cũng nêu ra điểm rất tốt là một lệnh gọi hàm ảo có thể gây ra lỗi bộ nhớ cache lệnh: nếu bạn chuyển đến một địa chỉ mã không có trong bộ nhớ cache thì toàn bộ chương trình sẽ dừng lại trong khi hướng dẫn được tìm nạp từ bộ nhớ chính. Đây luôn là một sự chững lại đáng kể: trên Xenon, khoảng 650 chu kỳ (theo thử nghiệm của tôi).

Tuy nhiên, đây không phải là vấn đề cụ thể đối với các hàm ảo vì ngay cả một lệnh gọi hàm trực tiếp cũng sẽ gây ra lỗi nếu bạn chuyển đến các hướng dẫn không có trong bộ nhớ cache. Điều quan trọng là liệu hàm đã được chạy trước đó hay chưa (khiến nó có nhiều khả năng nằm trong bộ nhớ cache) và liệu kiến ​​trúc của bạn có thể dự đoán các nhánh tĩnh (không phải ảo) và tìm nạp các lệnh đó vào bộ nhớ cache trước thời hạn hay không. PPC của tôi thì không, nhưng có lẽ phần cứng gần đây nhất của Intel thì có.

Việc kiểm soát thời gian của tôi đối với ảnh hưởng của icache không thực thi (có chủ ý, vì tôi đang cố gắng kiểm tra đường ống CPU một cách cô lập), vì vậy họ giảm giá đó.


3
Chi phí trong các chu kỳ gần bằng với số lượng các giai đoạn đường ống từ khi tìm nạp đến khi kết thúc nhánh rẽ. Đó không phải là một chi phí không đáng kể và nó có thể tăng lên, nhưng trừ khi bạn đang cố gắng viết một vòng lặp hiệu suất cao chặt chẽ, có lẽ sẽ có những con cá lớn hơn để bạn rán.
Crashworks

Dài hơn 7 nano giây so với những gì. Nếu một cuộc gọi bình thường là 1 nano giây là đáng kể nếu một cuộc gọi bình thường là 70 nano giây thì không.
Martin York

Nếu bạn nhìn vào thời gian, tôi thấy rằng đối với một hàm có giá 0,66ns nội tuyến, chi phí vi sai của một lệnh gọi hàm trực tiếp là 4,8ns và một hàm ảo 12,3ns (so với nội tuyến). Bạn nói rõ rằng nếu bản thân hàm có giá một phần nghìn giây, thì 7 ns không có nghĩa là gì.
Crashworks

2
Giống như 600 chu kỳ, nhưng đó là một điểm tốt. Tôi đã bỏ nó ra khỏi thời gian vì tôi chỉ quan tâm đến chi phí do bong bóng đường ống dẫn và phần mở đầu / epilog. Việc bỏ lỡ icache cũng dễ dàng xảy ra đối với một lệnh gọi hàm trực tiếp (Xenon không có bộ dự đoán nhánh icache).
Crashworks

2
Chi tiết nhỏ, nhưng liên quan đến "Tuy nhiên đây không phải là vấn đề cụ thể đối với ..." nó tệ hơn một chút đối với công văn ảo vì có một trang bổ sung (hoặc hai trang nếu nó xảy ra nằm trên ranh giới trang) phải nằm trong bộ nhớ cache - cho Bảng công văn ảo của lớp.
Tony Delroy

19

Chắc chắn có chi phí có thể đo lường được khi gọi một hàm ảo - lệnh gọi phải sử dụng vtable để giải quyết địa chỉ của hàm cho loại đối tượng đó. Các hướng dẫn thêm là ít lo lắng nhất của bạn. Các vtables không chỉ ngăn chặn nhiều tối ưu hóa trình biên dịch tiềm năng (vì loại trình biên dịch là đa hình) mà chúng còn có thể hủy bỏ I-Cache của bạn.

Tất nhiên, những hình phạt này có đáng kể hay không phụ thuộc vào ứng dụng của bạn, tần suất thực thi các đường dẫn mã đó và các mẫu kế thừa của bạn.

Theo tôi, mặc định mọi thứ đều là ảo là một giải pháp chung cho một vấn đề mà bạn có thể giải quyết theo những cách khác.

Có lẽ bạn có thể nhìn vào cách các lớp được thiết kế / tài liệu / viết. Nói chung, tiêu đề cho một lớp phải làm rõ những hàm nào có thể bị ghi đè bởi các lớp dẫn xuất và cách chúng được gọi. Nhờ các lập trình viên viết tài liệu này sẽ hữu ích trong việc đảm bảo chúng được đánh dấu chính xác là ảo.

Tôi cũng sẽ nói rằng việc khai báo mọi chức năng là ảo có thể dẫn đến nhiều lỗi hơn là chỉ quên đánh dấu một cái gì đó là ảo. Nếu tất cả các chức năng là ảo, mọi thứ có thể được thay thế bằng các lớp cơ sở - công khai, bảo vệ, riêng tư - mọi thứ trở thành trò chơi công bằng. Các lớp con do ngẫu nhiên hoặc cố ý có thể thay đổi hành vi của các hàm sau đó gây ra các vấn đề khi được sử dụng trong triển khai cơ sở.


Tối ưu hóa bị mất lớn nhất là nội tuyến, đặc biệt nếu hàm ảo thường nhỏ hoặc trống.
Zan Lynx

@Andrew: quan điểm thú vị. Tuy nhiên, tôi hơi không đồng ý với đoạn cuối cùng của bạn: nếu một lớp cơ sở có một hàm savedựa trên việc triển khai cụ thể một hàm writetrong lớp cơ sở, thì đối với tôi, có vẻ như saveđược mã hóa kém hoặc writephải là riêng tư.
MiniQuark

2
Chỉ vì viết là riêng tư không ngăn nó bị ghi đè. Đây là một lập luận khác để không làm cho mọi thứ trở nên ảo theo mặc định. Trong mọi trường hợp, tôi đã nghĩ đến điều ngược lại - một triển khai chung chung và được viết tốt được thay thế bằng một thứ có hành vi cụ thể và không tương thích.
Andrew Grant,

Được bình chọn trên bộ nhớ đệm - trên bất kỳ cơ sở mã hướng đối tượng lớn nào, nếu bạn không tuân theo các thực tiễn về hiệu suất cục bộ mã, thì rất dễ khiến các cuộc gọi ảo của bạn bỏ lỡ bộ nhớ cache và gây ra sự cố.
Không chắc chắn vào

Và sự cố về icache có thể thực sự nghiêm trọng: 600 chu kỳ trong các thử nghiệm của tôi.
Crashworks

9

Nó phụ thuộc. :) (Bạn có mong đợi điều gì khác không?)

Khi một lớp nhận được một hàm ảo, nó không còn có thể là một kiểu dữ liệu POD nữa, (nó có thể không phải là một trước đây, trong trường hợp này, điều này sẽ không tạo ra sự khác biệt) và điều đó làm cho toàn bộ phạm vi tối ưu hóa là không thể.

std :: copy () trên các loại POD đơn giản có thể sử dụng quy trình ghi nhớ đơn giản, nhưng các loại không phải POD phải được xử lý cẩn thận hơn.

Quá trình xây dựng trở nên chậm hơn rất nhiều vì vtable phải được khởi tạo. Trong trường hợp xấu nhất, sự khác biệt về hiệu suất giữa kiểu dữ liệu POD và không phải POD có thể đáng kể.

Trong trường hợp xấu nhất, bạn có thể thấy thực thi chậm hơn 5 lần (con số đó được lấy từ một dự án đại học mà tôi đã thực hiện gần đây để thực hiện lại một vài lớp thư viện tiêu chuẩn. Vùng chứa của chúng tôi mất khoảng 5 lần để tạo ngay khi kiểu dữ liệu mà nó lưu trữ có vtable)

Tất nhiên, trong hầu hết các trường hợp, bạn không thể thấy bất kỳ sự khác biệt hiệu suất có thể đo lường nào, điều này chỉ đơn giản là để chỉ ra rằng trong một số trường hợp biên giới, nó có thể tốn kém.

Tuy nhiên, hiệu suất không phải là yếu tố chính của bạn ở đây. Làm cho mọi thứ trở nên ảo không phải là một giải pháp hoàn hảo vì những lý do khác.

Việc cho phép mọi thứ bị ghi đè trong các lớp dẫn xuất khiến việc duy trì các bất biến của lớp khó hơn nhiều. Làm thế nào để một lớp đảm bảo rằng nó luôn ở trạng thái nhất quán khi bất kỳ một trong các phương thức của nó có thể được xác định lại bất kỳ lúc nào?

Làm cho mọi thứ trở nên ảo có thể loại bỏ một số lỗi tiềm ẩn, nhưng nó cũng giới thiệu những lỗi mới.


7

Nếu bạn cần chức năng của công văn ảo, bạn phải trả giá. Ưu điểm của C ++ là bạn có thể sử dụng một triển khai rất hiệu quả của công văn ảo được cung cấp bởi trình biên dịch, thay vì một phiên bản có thể không hiệu quả mà bạn tự triển khai.

Tuy nhiên, nếu bạn không cần thiết, việc lo lắng cho bản thân sẽ có thể đi quá xa. Và hầu hết các lớp không được thiết kế để kế thừa - để tạo ra một lớp cơ sở tốt đòi hỏi nhiều hơn là làm cho các chức năng của nó ảo.


Câu trả lời hay nhưng, IMO, không đủ nhấn mạnh trong hiệp 2: tự loạng choạng với chi phí nếu bạn không cần, khá thẳng thắn là dở hơi - đặc biệt là khi sử dụng ngôn ngữ này có câu thần chú là "đừng trả tiền cho những gì bạn không 'không sử dụng. " Làm cho mọi thứ ảo theo mặc định cho đến khi ai đó giải thích tại sao nó có thể / phải là không ảo là một chính sách đáng ghê tởm.
underscore_d

5

Công văn ảo là một thứ tự có độ lớn chậm hơn so với một số lựa chọn thay thế - không phải do điều hướng quá nhiều như việc ngăn chặn nội tuyến. Dưới đây, tôi minh họa điều đó bằng cách đối chiếu công văn ảo với một triển khai nhúng số "loại (-xác định)" trong các đối tượng và sử dụng câu lệnh switch để chọn mã loại cụ thể. Điều này tránh hoàn toàn chi phí cuộc gọi hàm - chỉ thực hiện một bước nhảy cục bộ. Có một chi phí tiềm ẩn đối với khả năng bảo trì, phụ thuộc biên dịch lại, v.v. thông qua việc bản địa hóa bắt buộc (trong chuyển đổi) của chức năng loại cụ thể.


THỰC HIỆN

#include <iostream>
#include <vector>

// virtual dispatch model...

struct Base
{
    virtual int f() const { return 1; }
};

struct Derived : Base
{
    virtual int f() const { return 2; }
};

// alternative: member variable encodes runtime type...

struct Type
{
    Type(int type) : type_(type) { }
    int type_;
};

struct A : Type
{
    A() : Type(1) { }
    int f() const { return 1; }
};

struct B : Type
{
    B() : Type(2) { }
    int f() const { return 2; }
};

struct Timer
{
    Timer() { clock_gettime(CLOCK_MONOTONIC, &from); }
    struct timespec from;
    double elapsed() const
    {
        struct timespec to;
        clock_gettime(CLOCK_MONOTONIC, &to);
        return to.tv_sec - from.tv_sec + 1E-9 * (to.tv_nsec - from.tv_nsec);
    }
};

int main(int argc)
{
  for (int j = 0; j < 3; ++j)
  {
    typedef std::vector<Base*> V;
    V v;

    for (int i = 0; i < 1000; ++i)
        v.push_back(i % 2 ? new Base : (Base*)new Derived);

    int total = 0;

    Timer tv;

    for (int i = 0; i < 100000; ++i)
        for (V::const_iterator i = v.begin(); i != v.end(); ++i)
            total += (*i)->f();

    double tve = tv.elapsed();

    std::cout << "virtual dispatch: " << total << ' ' << tve << '\n';

    // ----------------------------

    typedef std::vector<Type*> W;
    W w;

    for (int i = 0; i < 1000; ++i)
        w.push_back(i % 2 ? (Type*)new A : (Type*)new B);

    total = 0;

    Timer tw;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
        {
            if ((*i)->type_ == 1)
                total += ((A*)(*i))->f();
            else
                total += ((B*)(*i))->f();
        }

    double twe = tw.elapsed();

    std::cout << "switched: " << total << ' ' << twe << '\n';

    // ----------------------------

    total = 0;

    Timer tw2;

    for (int i = 0; i < 100000; ++i)
        for (W::const_iterator i = w.begin(); i != w.end(); ++i)
            total += (*i)->type_;

    double tw2e = tw2.elapsed();

    std::cout << "overheads: " << total << ' ' << tw2e << '\n';
  }
}

KẾT QUẢ THỰC HIỆN

Trên hệ thống Linux của tôi:

~/dev  g++ -O2 -o vdt vdt.cc -lrt
~/dev  ./vdt                     
virtual dispatch: 150000000 1.28025
switched: 150000000 0.344314
overhead: 150000000 0.229018
virtual dispatch: 150000000 1.285
switched: 150000000 0.345367
overhead: 150000000 0.231051
virtual dispatch: 150000000 1.28969
switched: 150000000 0.345876
overhead: 150000000 0.230726

Điều này cho thấy phương pháp tiếp cận chuyển đổi kiểu số nội tuyến nhanh hơn khoảng (1,28 - 0,23) / (0,344 - 0,23) = 9,2 lần. Tất nhiên, điều đó dành riêng cho cờ và phiên bản được kiểm tra / biên dịch chính xác của hệ thống, v.v., nhưng nói chung là chỉ định.


BÌNH LUẬN RE VIRTUAL DISPATCH

Cần phải nói rằng mặc dù chi phí cuộc gọi hàm ảo là một cái gì đó hiếm khi quan trọng, và sau đó chỉ dành cho những chức năng được gọi là tầm thường (như getters và setters). Thậm chí sau đó, bạn có thể cung cấp một chức năng duy nhất để nhận và thiết lập nhiều thứ cùng một lúc, giảm thiểu chi phí. Mọi người lo lắng về cách gửi công văn ảo quá nhiều - vì vậy hãy làm hồ sơ trước khi tìm ra các lựa chọn thay thế khó xử. Vấn đề chính với chúng là chúng thực hiện một lệnh gọi hàm ngoài dòng, mặc dù chúng cũng phân định vị trí mã được thực thi để thay đổi các mẫu sử dụng bộ nhớ cache (tốt hơn hoặc (thường xuyên hơn) tệ hơn).


Tôi đã hỏi một câu hỏi liên quan đến mã của bạn vì tôi có một số kết quả "lạ" bằng cách sử dụng g++/ clang-lrt. Tôi nghĩ điều đáng nói ở đây cho những độc giả tương lai.
Holt

@Holt: câu hỏi hay với kết quả thần bí! Tôi sẽ xem xét kỹ hơn trong vài ngày tới nếu tôi có một nửa cơ hội. Chúc mừng.
Tony Delroy

3

Chi phí bổ sung hầu như không có gì trong hầu hết các trường hợp. (tha thứ cho lối chơi chữ). Xuất tinh đã đăng các biện pháp tương đối hợp lý.

Điều lớn nhất bạn từ bỏ là khả năng tối ưu hóa do nội tuyến. Chúng có thể đặc biệt tốt nếu hàm được gọi với các tham số không đổi. Điều này hiếm khi tạo ra sự khác biệt thực sự, nhưng trong một số trường hợp, điều này có thể rất lớn.


Về tối ưu hóa:
Điều quan trọng là phải biết và xem xét chi phí tương đối của các cấu trúc ngôn ngữ của bạn. Ký hiệu Big O là một nửa câu chuyện - ứng dụng của bạn mở rộng như thế nào . Nửa còn lại là nhân tố không đổi trước mặt nó.

Theo nguyên tắc chung, tôi sẽ không cố gắng tránh các chức năng ảo, trừ khi có dấu hiệu rõ ràng và cụ thể rằng đó là cổ chai. Một thiết kế sạch sẽ luôn đặt lên hàng đầu - nhưng chỉ một bên liên quan không được làm tổn thương người khác quá mức .


Ví dụ có sẵn: Một trình hủy ảo trống trên một mảng gồm một triệu phần tử nhỏ có thể cày nát ít nhất 4MB dữ liệu, phá hủy bộ nhớ cache của bạn. Nếu bộ hủy đó có thể được nội tuyến, dữ liệu sẽ không bị chạm vào.

Khi viết mã thư viện, những cân nhắc như vậy không còn sớm. Bạn không bao giờ biết có bao nhiêu vòng lặp sẽ được đặt xung quanh hàm của bạn.


2

Trong khi tất cả những người khác đều đúng về hiệu suất của các phương thức ảo và như vậy, tôi nghĩ vấn đề thực sự là liệu nhóm có biết về định nghĩa của từ khóa ảo trong C ++ hay không.

Hãy xem xét đoạn mã này, đầu ra là gì?

#include <stdio.h>

class A
{
public:
    void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

Không có gì đáng ngạc nhiên ở đây:

A::Foo()
B::Foo()
A::Foo()

Như không có gì là ảo. Nếu từ khóa ảo được thêm vào phía trước Foo trong cả hai lớp A và B, chúng tôi nhận được điều này cho đầu ra:

A::Foo()
B::Foo()
B::Foo()

Khá nhiều thứ mà mọi người mong đợi.

Bây giờ, bạn đã đề cập rằng có lỗi do ai đó quên thêm từ khóa ảo. Vì vậy, hãy xem xét mã này (nơi từ khóa ảo được thêm vào lớp A, nhưng không phải lớp B). Đầu ra sau đó là gì?

#include <stdio.h>

class A
{
public:
    virtual void Foo()
    {
        printf("A::Foo()\n");
    }
};

class B : public A
{
public:
    void Foo()
    {
        printf("B::Foo()\n");
    }
};

int main(int argc, char** argv)
{    
    A* a = new A();
    a->Foo();

    B* b = new B();
    b->Foo();

    A* a2 = new B();
    a2->Foo();

    return 0;
}

Trả lời: Tương tự như nếu từ khóa ảo được thêm vào B? Lý do là chữ ký của B :: Foo khớp chính xác với A :: Foo () và bởi vì Foo của A là ảo nên chữ B cũng vậy.

Bây giờ hãy xem xét trường hợp Foo của B là ảo và của A thì không. Đầu ra sau đó là gì? Trong trường hợp này, đầu ra là

A::Foo()
B::Foo()
A::Foo()

Từ khóa ảo hoạt động xuống dưới theo thứ bậc, không phải lên trên. Nó không bao giờ làm cho các phương thức của lớp cơ sở là ảo. Lần đầu tiên một phương thức ảo gặp phải trong hệ thống phân cấp là khi tính đa hình bắt đầu. Không có cách nào để các lớp sau làm cho các lớp trước có các phương thức ảo.

Đừng quên rằng các phương thức ảo có nghĩa là lớp này cung cấp cho các lớp tương lai khả năng ghi đè / thay đổi một số hành vi của nó.

Vì vậy, nếu bạn có một quy tắc để loại bỏ từ khóa ảo, nó có thể không có tác dụng như mong muốn.

Từ khóa ảo trong C ++ là một khái niệm mạnh mẽ. Bạn nên đảm bảo rằng mỗi thành viên trong nhóm thực sự biết khái niệm này để có thể sử dụng nó như thiết kế.


Chào Tommy, cảm ơn về hướng dẫn. Lỗi chúng tôi gặp phải là do thiếu từ khóa "ảo" trong một phương thức của lớp cơ sở. BTW, tôi đang nói làm cho tất cả các chức năng ảo (không phải ngược lại), sau đó, khi rõ ràng là không cần thiết, hãy xóa từ khóa "ảo".
MiniQuark

@MiniQuark: Tommy Hui đang nói rằng nếu bạn biến tất cả các hàm thành ảo, một lập trình viên có thể sẽ xóa từ khóa trong một lớp dẫn xuất mà không nhận ra rằng nó không có tác dụng. Bạn sẽ cần một số cách để đảm bảo rằng việc loại bỏ từ khóa ảo luôn xảy ra ở lớp cơ sở.
M. Dudley

1

Tùy thuộc vào nền tảng của bạn, chi phí của một cuộc gọi ảo có thể rất không mong muốn. Bằng cách khai báo mọi hàm ảo về cơ bản, bạn đang gọi tất cả chúng thông qua một con trỏ hàm. Ít nhất thì đây là một yêu cầu bổ sung, nhưng trên một số nền tảng PPC, nó sẽ sử dụng vi mã hoặc các hướng dẫn chậm để thực hiện điều này.

Tôi khuyên bạn nên chống lại đề xuất của bạn vì lý do này, nhưng nếu nó giúp bạn ngăn chặn lỗi thì nó có thể đáng để đánh đổi. Tôi không thể không nghĩ rằng phải có một số điểm trung gian đáng để tìm kiếm.


-1

Nó sẽ chỉ yêu cầu một vài lệnh bổ sung asm để gọi phương thức ảo.

Nhưng tôi không nghĩ rằng bạn lo lắng rằng fun (int a, int b) có một vài hướng dẫn 'push' bổ sung so với fun (). Vì vậy, đừng lo lắng về ảo quá, cho đến khi bạn ở trong tình huống đặc biệt và thấy rằng nó thực sự dẫn đến vấn đề.

Tái bút Nếu bạn có một phương thức ảo, hãy đảm bảo rằng bạn có một trình hủy ảo. Bằng cách này, bạn sẽ tránh được các vấn đề có thể xảy ra


Đáp lại các bình luận của 'xtofl' và 'Tom'. Tôi đã thực hiện các bài kiểm tra nhỏ với 3 chức năng:

  1. Ảo
  2. Bình thường
  3. Bình thường với 3 tham số int

Thử nghiệm của tôi là một phép lặp đơn giản:

for(int it = 0; it < 100000000; it ++) {
    test.Method();
}

Và đây là kết quả:

  1. 3,913 giây
  2. 3,873 giây
  3. 3,970 giây

Nó được VC ++ biên dịch ở chế độ gỡ lỗi. Tôi chỉ thực hiện 5 bài kiểm tra cho mỗi phương pháp và tính toán giá trị trung bình (vì vậy kết quả có thể khá chính xác) ... Bất kỳ cách nào, các giá trị gần như bằng nhau giả sử 100 triệu cuộc gọi. Và phương pháp có thêm 3 lần đẩy / bật chậm hơn.

Điểm chính là nếu bạn không thích sự tương tự với push / pop, hãy nghĩ thêm if / else trong mã của bạn? Bạn có nghĩ đến đường ống CPU khi bạn thêm if / else bổ sung không ;-) Ngoài ra, bạn không bao giờ biết mã sẽ chạy trên CPU nào ... Trình biên dịch thông thường có thể tạo mã tối ưu hơn cho một CPU và ít tối ưu hơn cho CPU khác ( Intel Trình biên dịch C ++ )


2
asm bổ sung có thể chỉ gây ra lỗi trang (điều này sẽ không xảy ra đối với các chức năng không phải ảo) - Tôi nghĩ bạn đơn giản hóa vấn đề quá mức.
xtofl

2
+1 cho nhận xét của xtofl. Các chức năng ảo giới thiệu hướng dẫn, giới thiệu các "bong bóng" đường ống và ảnh hưởng đến hành vi lưu vào bộ nhớ đệm.
Tom

1
Thời gian cho bất kỳ thứ gì trong chế độ gỡ lỗi là vô nghĩa. MSVC tạo mã rất chậm trong chế độ gỡ lỗi và chi phí vòng lặp có thể che giấu hầu hết sự khác biệt. Nếu bạn đang hướng tới hiệu suất cao, có, bạn nên nghĩ đến việc giảm thiểu các nhánh if / else trong đường dẫn nhanh. Xem agner.org/optimize để biết thêm về tối ưu hóa hiệu suất x86 mức thấp. (Ngoài ra một số các liên kết khác trong thẻ x86 wiki
Peter Cordes

1
@Tom: điểm mấu chốt ở đây là các hàm không phải ảo có thể nội dòng, nhưng ảo thì không thể (trừ khi trình biên dịch có thể phân phối, ví dụ: nếu bạn đã sử dụng finaltrong ghi đè của mình và bạn có một con trỏ đến kiểu dẫn xuất, thay vì kiểu cơ sở ). Thử nghiệm này gọi cùng một hàm ảo mọi lúc, vì vậy nó dự đoán hoàn hảo; không có bong bóng đường ống ngoại trừ callthông lượng hạn chế . Và gián tiếp đó callcó thể là một vài uops nữa. Dự đoán nhánh hoạt động tốt ngay cả đối với các nhánh gián tiếp, đặc biệt nếu chúng luôn đến cùng một điểm đến.
Peter Cordes 9:17

Điều này rơi vào cái bẫy phổ biến của microbenchmarks: nó có vẻ nhanh khi các yếu tố dự đoán nhánh đang nóng và không có gì khác đang diễn ra. Chi phí dự đoán sai đối với gián tiếp callcao hơn đối với trực tiếp call. (Và vâng, các calllệnh thông thường cũng cần dự đoán. Giai đoạn tìm nạp phải biết địa chỉ tiếp theo để tìm nạp trước khi khối này được giải mã, vì vậy nó phải dự đoán khối tìm nạp tiếp theo dựa trên địa chỉ khối hiện tại, thay vì địa chỉ lệnh. Cũng như vậy như dự đoán nơi trong khối này có hướng dẫn nhánh ...)
Peter Cordes
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.