2012-03-31 39 views
7

Tôi đã viết một ví dụ đơn giản, ước tính thời gian trung bình gọi hàm ảo, sử dụng giao diện lớp cơ sở và dynamic_cast và gọi hàm không phải ảo. Đây là địa chỉ:Tại sao gọi hàm ảo nhanh hơn dynamic_cast?

#include <iostream> 
#include <numeric> 
#include <list> 
#include <time.h> 

#define CALL_COUNTER (3000) 

__forceinline int someFunction() 
{ 
    return 5; 
} 

struct Base 
{ 
    virtual int virtualCall() = 0; 
    virtual ~Base(){}; 
}; 

struct Derived : public Base 
{ 
    Derived(){}; 
    virtual ~Derived(){}; 
    virtual int virtualCall(){ return someFunction(); }; 
    int notVirtualCall(){ return someFunction(); }; 
}; 


struct Derived2 : public Base 
{ 
    Derived2(){}; 
    virtual ~Derived2(){}; 
    virtual int virtualCall(){ return someFunction(); }; 
    int notVirtualCall(){ return someFunction(); }; 
}; 

typedef std::list<double> Timings; 

Base* createObject(int i) 
{ 
    if(i % 2 > 0) 
    return new Derived(); 
    else 
    return new Derived2(); 
} 

void callDynamiccast(Timings& stat) 
{ 
    for(unsigned i = 0; i < CALL_COUNTER; ++i) 
    { 
    Base* ptr = createObject(i); 

    clock_t startTime = clock(); 

    for(int j = 0; j < CALL_COUNTER; ++j) 
    { 
     Derived* x = (dynamic_cast<Derived*>(ptr)); 
     if(x) x->notVirtualCall(); 
    } 

    clock_t endTime = clock(); 
    double callTime = (double)(endTime - startTime)/CLOCKS_PER_SEC; 
    stat.push_back(callTime); 

    delete ptr; 
    } 
} 

void callVirtual(Timings& stat) 
{ 
    for(unsigned i = 0; i < CALL_COUNTER; ++i) 
    { 
    Base* ptr = createObject(i); 

    clock_t startTime = clock(); 

    for(int j = 0; j < CALL_COUNTER; ++j) 
     ptr->virtualCall(); 


    clock_t endTime = clock(); 
    double callTime = (double)(endTime - startTime)/CLOCKS_PER_SEC; 
    stat.push_back(callTime); 

    delete ptr; 
    } 
} 

int main() 
{ 
    double averageTime = 0; 
    Timings timings; 


    timings.clear(); 
    callDynamiccast(timings); 
    averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0); 
    averageTime /= timings.size(); 
    std::cout << "time for callDynamiccast: " << averageTime << std::endl; 

    timings.clear(); 
    callVirtual(timings); 
    averageTime = (double) std::accumulate<Timings::iterator, double>(timings.begin(), timings.end(), 0); 
    averageTime /= timings.size(); 
    std::cout << "time for callVirtual: " << averageTime << std::endl; 

    return 0; 
} 

Dường như callDynamiccast mất gần gấp hai lần.

time for callDynamiccast: 0.000240333

time for callVirtual: 0.0001401

Bất cứ ý tưởng tại sao không?

CHỈNH SỬA: tạo đối tượng được thực hiện trong chức năng tách biệt ngay bây giờ, do đó trình biên dịch không biết loại thực. Gần như cùng một kết quả.

EDITED2: tạo hai loại khác nhau của một đối tượng có nguồn gốc.

+1

Bạn có thể cần chạy nhiều lần lặp lại hơn để có được một thước đo thống kê hợp lý. Bạn có đang biên dịch ở cài đặt tối ưu hóa cao nhất không? –

+0

Tôi đã thử với nhiều lần lặp lại hơn, nhưng kết quả là như nhau. Tất cả các tối ưu hóa đều tắt. Tôi đã sử dụng MSVS2008. –

+4

Bài kiểm tra của bạn không hợp lệ, vì trình biên dịch có thể dễ dàng tối ưu hóa cả cuộc gọi ảo (thành cuộc gọi không ảo) và dynamic_cast (về cơ bản là noop), vì nó biết rằng 'ptr' thực sự trỏ đến đối tượng' Derived'. –

Trả lời

11

Cuộc gọi hàm ảo tương tự như con trỏ hàm, hoặc nếu trình biên dịch biết loại, công văn tĩnh. Đây là thời gian không đổi.

dynamic_cast hoàn toàn khác - nó sử dụng cách triển khai được xác định nghĩa là xác định loại. Nó không phải là thời gian không đổi, có thể đi qua phân cấp lớp (cũng xem xét nhiều thừa kế) và thực hiện một số lần tra cứu. Việc triển khai có thể sử dụng so sánh chuỗi. Do đó, độ phức tạp cao hơn ở hai chiều. Hệ thống thời gian thực thường tránh/không khuyến khích dynamic_cast vì những lý do này.

Chi tiết khác có sẵn in this document.

+0

Vì vậy, nếu tôi mở rộng lớp học, thời gian dynamic_cast sẽ không còn nhiều hơn nữa? –

+4

@Xác minh cách triển khai được xác định bởi việc triển khai của bạn, nhưng như một sự khái quát hóa có, điều đó đúng. sự phức tạp của thừa kế (số lượng cơ sở và nếu nhiều thừa kế được sử dụng) thường là nơi mà chi phí được giới thiệu. nếu các lớp không liên quan, việc triển khai của bạn có thể có tối ưu hóa tốt cho trường hợp đó. cũng lưu ý rằng có một vài trường hợp cạnh làm tăng độ phức tạp - 'dynamic_cast' có thể thất bại khi không thể xác định được một cơ sở số ít vì hai cơ sở chung tồn tại. do đó, toàn bộ hệ thống phân cấp phải được kiểm tra trước khi trở về. – justin

+0

Trang 31 của tài liệu liên kết có các chi tiết thích hợp về một cuộc gọi ảo so với dynamic_cast khác nhau đối với một số trình biên dịch (mặc dù tôi không thể tìm thấy nơi nó cho bạn biết trình biên dịch nào được sử dụng). – paxos1977

3

Bạn chỉ cần đo chi phí dynamic_cast<>. Nó được thực hiện với RTTI, đó là tùy chọn trong bất kỳ trình biên dịch C++ nào. Dự án + Thuộc tính, C/C++, Ngôn ngữ, Bật cài đặt Thông tin loại thời gian chạy. Thay đổi nó thành số

Bây giờ bạn sẽ nhận được một lời nhắc không rõ ràng rằng dynamic_cast<> không thể thực hiện công việc thích hợp nữa. Tự ý thay đổi nó thành static_cast<> để nhận các kết quả khác nhau đáng kể. Điểm mấu chốt ở đây là nếu bạn biết rằng một upcast luôn an toàn thì static_cast<> sẽ mua cho bạn hiệu suất bạn đang tìm kiếm. Nếu bạn không biết cho một thực tế là upcast là an toàn sau đó dynamic_cast<> giúp bạn thoát khỏi rắc rối. Đó là loại rắc rối khó chẩn đoán. Chế độ thất bại chung là tham nhũng đống, bạn chỉ nhận được GPF ngay lập tức nếu bạn thực sự may mắn.

4

Cần lưu ý rằng toàn bộ mục đích của các chức năng ảo là không phải bỏ qua biểu đồ thừa kế. Các hàm ảo tồn tại để bạn có thể sử dụng một cá thể lớp dẫn xuất như thể nó là một lớp cơ sở. Do đó, việc triển khai thực hiện các chức năng chuyên biệt hơn có thể được gọi từ mã ban đầu được gọi là các phiên bản lớp cơ sở.

Nếu chức năng ảo chậm hơn so với truyền an toàn cho cuộc gọi hàm lớp dẫn xuất, thì trình biên dịch C++ sẽ chỉ thực hiện các cuộc gọi hàm ảo theo cách đó.

Vì vậy, không có lý do gì để mong đợi dynamic_cast + gọi nhanh hơn.

0

Sự khác biệt là, bạn có thể gọi hàm ảo trên bất kỳ cá thể nào có nguồn gốc từ Base.Thành viên notVirtualCall() không tồn tại trong phạm vi Base và không thể được gọi mà không xác định chính xác loại động chính xác của đối tượng.

Hậu quả của sự khác biệt này là, vtable của lớp cơ sở bao gồm một khe cho virtualCall(), có chứa một con trỏ hàm đến hàm chính xác để gọi. Vì vậy, các cuộc gọi ảo chỉ đơn giản là đuổi theo con trỏ vtable bao gồm thành viên đầu tiên (vô hình) của tất cả các đối tượng thuộc loại Base, tải con trỏ từ khe tương ứng với virtualCall() và gọi hàm đằng sau con trỏ đó.

Khi bạn thực hiện dynamic_cast<>, ngược lại, lớp Base không biết thời gian biên dịch những gì các lớp khác cuối cùng sẽ xuất phát từ nó. Do đó, nó không thể bao gồm thông tin trong vtable của nó làm giảm độ phân giải của dynamic_cast<>. Đó là sự thiếu thông tin khiến cho việc thực hiện dynamic_cast<> tốn kém hơn một cuộc gọi hàm ảo. Các dynamic_cast<> đã thực sự tìm kiếm thông qua cây thừa kế của đối tượng thực tế để kiểm tra xem loại đích của diễn viên được tìm thấy trong số các căn cứ của nó. Đó là công việc mà cuộc gọi ảo tránh.

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