RTTI đắt như thế nào?


152

Tôi hiểu rằng có một tài nguyên được sử dụng từ RTTI, nhưng nó lớn đến mức nào? Ở mọi nơi tôi đã nhìn chỉ nói rằng "RTTI là đắt tiền", nhưng không ai trong số họ thực sự đưa ra bất kỳ điểm chuẩn hoặc dữ liệu định lượng nào lấy lại bộ nhớ, thời gian xử lý hoặc tốc độ.

Vì vậy, RTTI đắt như thế nào? Tôi có thể sử dụng nó trên một hệ thống nhúng, nơi tôi chỉ có 4 MB RAM, vì vậy mỗi bit đều có giá trị.

Chỉnh sửa: Theo câu trả lời của S. Lott , sẽ tốt hơn nếu tôi bao gồm những gì tôi thực sự đang làm. Tôi đang sử dụng một lớp để truyền dữ liệu có độ dài khác nhau và có thể thực hiện các hành động khác nhau , vì vậy sẽ rất khó để làm điều này nếu chỉ sử dụng các hàm ảo. Dường như việc sử dụng một vài dynamic_castgiây có thể khắc phục vấn đề này bằng cách cho phép các lớp dẫn xuất khác nhau được chuyển qua các cấp khác nhau nhưng vẫn cho phép chúng hành động hoàn toàn khác nhau.

Từ hiểu biết của tôi, dynamic_castsử dụng RTTI, vì vậy tôi đã tự hỏi làm thế nào khả thi để sử dụng trên một hệ thống hạn chế.


1
Theo dõi từ chỉnh sửa của bạn - rất thường xuyên khi tôi thấy mình thực hiện một số diễn xuất động, tôi nhận ra rằng việc sử dụng mẫu Khách truy cập sẽ điều chỉnh lại mọi thứ. Điều đó có thể làm việc cho bạn?
philsquared

4
Tôi sẽ giải thích nó theo cách này - Tôi mới bắt đầu sử dụng dynamic_casttrong C ++, và bây giờ, 9 trên 10 lần khi tôi "phá vỡ" chương trình với trình gỡ lỗi, nó bị hỏng bên trong chức năng truyền động bên trong. Thật là chậm.
dùng541686

3
Nhân tiện, RTTI = "thông tin loại thời gian chạy".
Noumenon

Câu trả lời:


115

Bất kể trình biên dịch, bạn luôn có thể lưu vào thời gian chạy nếu bạn có đủ khả năng để làm

if (typeid(a) == typeid(b)) {
  B* ba = static_cast<B*>(&a);
  etc;
}

thay vì

B* ba = dynamic_cast<B*>(&a);
if (ba) {
  etc;
}

Cái trước chỉ liên quan đến một so sánh std::type_info; cái sau nhất thiết phải đi qua một cây thừa kế cộng với sự so sánh.

Quá khứ ... như mọi người nói, việc sử dụng tài nguyên là triển khai cụ thể.

Tôi đồng ý với ý kiến ​​của mọi người rằng người nộp nên tránh RTTI vì lý do thiết kế. Tuy nhiên, có những lý do chính đáng để sử dụng RTTI (chủ yếu là vì boost :: any). Điều đó trong tâm trí, thật hữu ích khi biết sử dụng tài nguyên thực tế của nó trong các triển khai chung.

Gần đây tôi đã thực hiện một loạt nghiên cứu về RTTI trong GCC.

tl; dr: RTTI trong GCC sử dụng không gian không đáng kể và typeid(a) == typeid(b)rất nhanh, trên nhiều nền tảng (Linux, BSD và có thể là các nền tảng nhúng, nhưng không phải mingw32). Nếu bạn biết bạn sẽ luôn ở trên một nền tảng may mắn, RTTI rất gần với miễn phí.

Chi tiết nghiệt ngã:

GCC thích sử dụng một C ++ ABI "trung lập với nhà cung cấp" cụ thể và luôn sử dụng ABI này cho các mục tiêu Linux và BSD [2]. Đối với các nền tảng hỗ trợ ABI này và liên kết yếu, typeid()trả về một đối tượng nhất quán và duy nhất cho từng loại, thậm chí qua các ranh giới liên kết động. Bạn có thể kiểm tra &typeid(a) == &typeid(b)hoặc chỉ dựa vào thực tế là kiểm tra di động typeid(a) == typeid(b)thực sự chỉ so sánh một con trỏ bên trong.

Trong ABI ưa thích của GCC, một lớp vtable luôn giữ một con trỏ tới cấu trúc RTTI mỗi loại, mặc dù nó có thể không được sử dụng. Vì vậy, typeid()bản thân một cuộc gọi chỉ nên chi phí nhiều như bất kỳ tra cứu vtable nào khác (giống như gọi một hàm thành viên ảo) và hỗ trợ RTTI không nên sử dụng bất kỳ không gian thừa nào cho mỗi đối tượng.

Từ những gì tôi có thể nhận ra, các cấu trúc RTTI được sử dụng bởi GCC (đây là tất cả các lớp con của std::type_info) chỉ giữ một vài byte cho mỗi loại, ngoài tên. Tôi không rõ liệu tên có trong mã đầu ra ngay cả với -fno-rtti. Dù bằng cách nào, sự thay đổi kích thước của nhị phân được biên dịch sẽ phản ánh sự thay đổi trong việc sử dụng bộ nhớ thời gian chạy.

Một thử nghiệm nhanh (sử dụng GCC 4.4.3 trên Ubuntu 10.04 64 bit) cho thấy -fno-rttithực sự làm tăng kích thước nhị phân của một chương trình thử nghiệm đơn giản thêm vài trăm byte. Điều này xảy ra nhất quán trên các kết hợp -g-O3. Tôi không chắc tại sao kích thước sẽ tăng lên; một khả năng là mã STL của GCC hoạt động khác đi mà không có RTTI (vì các trường hợp ngoại lệ sẽ không hoạt động).

[1] Được biết đến với cái tên Itanium C ++ ABI, được ghi lại tại http://www.codesourcery.com/public/cxx-abi/abi.html . Các tên rất khó hiểu: tên này đề cập đến kiến ​​trúc phát triển ban đầu, mặc dù đặc tả ABI hoạt động trên nhiều kiến ​​trúc bao gồm i686 / x86_64. Nhận xét về nguồn nội bộ và mã STL của GCC đề cập đến Itanium là ABI "mới" trái ngược với "cũ" mà họ đã sử dụng trước đây. Tồi tệ hơn, "mới" / Itanium ABI đề cập đến tất cả các phiên bản có sẵn thông qua -fabi-version; ABI "cũ" có trước phiên bản này. GCC đã thông qua ABI Itanium / phiên bản / "mới" trong phiên bản 3.0; ABI "cũ" đã được sử dụng trong 2,95 trở về trước, nếu tôi đọc chính xác các thay đổi của chúng.

[2] Tôi không thể tìm thấy bất kỳ std::type_infosự ổn định đối tượng liệt kê tài nguyên theo nền tảng. Đối với trình biên dịch tôi có quyền truy cập, tôi đã sử dụng như sau : echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES. Macro này điều khiển hành vi của operator==cho std::type_infotrong STL GCC, như GCC 3.0. Tôi đã thấy rằng mingw32-gcc tuân theo Windows C ++ ABI, nơi std::type_infocác đối tượng không phải là duy nhất cho một loại trên DLL; typeid(a) == typeid(b)các cuộc gọi strcmpdưới vỏ bọc. Tôi suy đoán rằng trên các mục tiêu nhúng chương trình đơn như AVR, nơi không có mã để liên kết, std::type_infocác đối tượng luôn ổn định.


6
Ngoại lệ hoạt động mà không có RTTI. (Bạn được phép ném intvà không có vtable nào trong đó :))
Billy ONeal

3
@Ded repeatator: Chưa hết, khi tôi tắt RTTI trong trình biên dịch của mình, chúng vẫn hoạt động tốt. Xin lỗi đã làm bạn thất vọng.
Billy ONeal

5
Cơ chế xử lý ngoại lệ phải có khả năng làm việc với bất kỳ kiểu điền đầy đủ một số yêu cầu cơ bản. Bạn có thể đề xuất cách xử lý việc ném và bắt ngoại lệ của loại tùy ý qua các ranh giới mô-đun mà không cần RTTI. Vui lòng xem xét rằng việc lên xuống là bắt buộc.
Ded repeatator

15
typeid (a) == typeid (b) KHÔNG giống với B * ba = Dynamic_cast <B *> (& a). Hãy thử nó trên các đối tượng có nhiều kế thừa như một mức ngẫu nhiên trên cây lớp dẫn xuất và bạn sẽ thấy typeid () == typeid () sẽ không mang lại kết quả dương. Dynamic_cast là cách duy nhất để tìm kiếm cây thừa kế thực sự. Ngừng suy nghĩ về tiết kiệm tiềm năng bằng cách vô hiệu hóa RTTI và chỉ sử dụng nó. Nếu bạn quá dung lượng thì tối ưu hóa mã phình của bạn. Cố gắng tránh sử dụng Dynamic_cast bên trong các vòng lặp bên trong hoặc bất kỳ mã quan trọng nào về hiệu suất và bạn sẽ ổn.
huyền bí

3
@mcoder Đó là lý do tại sao bài viết nêu rõ điều đó the latter necessarily involves traversing an inheritance tree plus comparisons. @CoryB Bạn có thể "đủ khả năng" để làm điều đó khi bạn không cần hỗ trợ truyền từ toàn bộ cây thừa kế. Ví dụ: nếu bạn muốn tìm tất cả các mục thuộc loại X trong một bộ sưu tập, nhưng không phải các mục xuất phát từ X, thì những gì bạn nên sử dụng là mục trước. Nếu bạn cũng cần tìm tất cả các trường hợp dẫn xuất, bạn sẽ phải sử dụng trường hợp sau.
Aidiakapi

48

Có lẽ những số liệu này sẽ giúp.

Tôi đã làm một bài kiểm tra nhanh bằng cách này:

  • Đồng hồ GCC () + Trình tạo hồ sơ của XCode.
  • Lặp lại vòng lặp 100.000.000.
  • Intel Xeon lõi kép 2 x 2,66 GHz.
  • Lớp trong câu hỏi được bắt nguồn từ một lớp cơ sở duy nhất.
  • typeid (). name () trả về "N12fastdelegate13FastDelegate1IivEE"

5 trường hợp đã được thử nghiệm:

1) dynamic_cast< FireType* >( mDelegate )
2) typeid( *iDelegate ) == typeid( *mDelegate )
3) typeid( *iDelegate ).name() == typeid( *mDelegate ).name()
4) &typeid( *iDelegate ) == &typeid( *mDelegate )
5) { 
       fastdelegate::FastDelegateBase *iDelegate;
       iDelegate = new fastdelegate::FastDelegate1< t1 >;
       typeid( *iDelegate ) == typeid( *mDelegate )
   }

5 chỉ là mã thực tế của tôi, vì tôi cần tạo một đối tượng thuộc loại đó trước khi kiểm tra xem nó có giống với mã tôi đã có không.

Không tối ưu hóa

Kết quả là (Tôi đã tính trung bình một vài lần chạy):

1)  1,840,000 Ticks (~2  Seconds) - dynamic_cast
2)    870,000 Ticks (~1  Second)  - typeid()
3)    890,000 Ticks (~1  Second)  - typeid().name()
4)    615,000 Ticks (~1  Second)  - &typeid()
5) 14,261,000 Ticks (~23 Seconds) - typeid() with extra variable allocations.

Vì vậy, kết luận sẽ là:

  • Đối với các trường hợp diễn viên đơn giản mà không tối ưu hóa typeid()nhanh hơn gấp đôi dyncamic_cast.
  • Trên một chiếc máy hiện đại, sự khác biệt giữa hai loại là khoảng 1 nano giây (một phần triệu của một phần nghìn giây).

Với Tối ưu hóa (-Os)

1)  1,356,000 Ticks - dynamic_cast
2)     76,000 Ticks - typeid()
3)     76,000 Ticks - typeid().name()
4)     75,000 Ticks - &typeid()
5)     75,000 Ticks - typeid() with extra variable allocations.

Vì vậy, kết luận sẽ là:

  • Đối với các trường hợp diễn viên đơn giản với tối ưu hóa, typeid()nhanh hơn gần x20 dyncamic_cast.

Đồ thị

nhập mô tả hình ảnh ở đây

Mật mã

Theo yêu cầu trong các bình luận, mã bên dưới (hơi lộn xộn, nhưng hoạt động). 'FastDelegate.h' có sẵn từ đây .

#include <iostream>
#include "FastDelegate.h"
#include "cycle.h"
#include "time.h"

// Undefine for typeid checks
#define CAST

class ZoomManager
{
public:
    template < class Observer, class t1 >
    void Subscribe( void *aObj, void (Observer::*func )( t1 a1 ) )
    {
        mDelegate = new fastdelegate::FastDelegate1< t1 >;
        
        std::cout << "Subscribe\n";
        Fire( true );
    }
    
    template< class t1 >
    void Fire( t1 a1 )
    {
        fastdelegate::FastDelegateBase *iDelegate;
        iDelegate = new fastdelegate::FastDelegate1< t1 >;
        
        int t = 0;
        ticks start = getticks();
        
        clock_t iStart, iEnd;
        
        iStart = clock();
        
        typedef fastdelegate::FastDelegate1< t1 > FireType;
        
        for ( int i = 0; i < 100000000; i++ ) {
        
#ifdef CAST
                if ( dynamic_cast< FireType* >( mDelegate ) )
#else
                // Change this line for comparisons .name() and & comparisons
                if ( typeid( *iDelegate ) == typeid( *mDelegate ) )
#endif
                {
                    t++;
                } else {
                    t--;
                }
        }
        
        iEnd = clock();
        printf("Clock ticks: %i,\n", iEnd - iStart );
        
        std::cout << typeid( *mDelegate ).name()<<"\n";
        
        ticks end = getticks();
        double e = elapsed(start, end);
        std::cout << "Elasped: " << e;
    }
    
    template< class t1, class t2 >
    void Fire( t1 a1, t2 a2 )
    {
        std::cout << "Fire\n";
    }
    
    fastdelegate::FastDelegateBase *mDelegate;
};

class Scaler
{
public:
    Scaler( ZoomManager *aZoomManager ) :
        mZoomManager( aZoomManager ) { }
    
    void Sub()
    {
        mZoomManager->Subscribe( this, &Scaler::OnSizeChanged );
    }
    
    void OnSizeChanged( int X  )
    {
        std::cout << "Yey!\n";        
    }
private:
    ZoomManager *mZoomManager;
};

int main(int argc, const char * argv[])
{
    ZoomManager *iZoomManager = new ZoomManager();
    
    Scaler iScaler( iZoomManager );
    iScaler.Sub();
        
    delete iZoomManager;

    return 0;
}

1
Tất nhiên, dàn diễn viên năng động hơn - nó hoạt động nếu vật phẩm có nguồn gốc nhiều hơn. Ví dụ: class a {}; class b : public a {}; class c : public b {};khi mục tiêu là một thể hiện csẽ hoạt động tốt khi kiểm tra lớp bvới dynamic_cast, nhưng không phải với typeidgiải pháp. Mặc dù vậy vẫn hợp lý, +1
Billy ONeal 23/07/13

34
Điểm chuẩn này hoàn toàn không có thật với các tối ưu hóa : kiểm tra typeid là bất biến vòng lặp và được chuyển ra khỏi vòng lặp. Nó không thú vị chút nào, đó là một điểm chuẩn cơ bản không-không.
Phục hồi lại

3
@Kuba: Sau đó, điểm chuẩn là không có thật. Đó không phải là một lý do để điểm chuẩn với tối ưu hóa tắt; đó là một lý do để viết điểm chuẩn tốt hơn.
Billy ONeal

3
một lần nữa, đây là một thất bại. "Đối với các trường hợp truyền đơn giản với tối ưu hóa, typeid () nhanh hơn gần x20 so với dyncamic_cast." họ KHÔNG làm điều tương tự. Có một lý do Dynamic_cast chậm hơn.
huyền bí

1
@KubaOber: tổng +1. điều này thật kinh điển và nó phải rõ ràng từ cái nhìn của số chu kỳ mà điều này đã xảy ra.
v.oddou

38

Nó phụ thuộc vào quy mô của sự vật. Đối với hầu hết các phần, nó chỉ là một vài kiểm tra và một vài hội nghị con trỏ. Trong hầu hết các triển khai, ở đầu mọi đối tượng có hàm ảo, có một con trỏ tới vtable chứa danh sách các con trỏ tới tất cả các cài đặt của hàm ảo trên lớp đó. Tôi đoán rằng hầu hết các triển khai sẽ sử dụng điều này để lưu trữ một con trỏ khác vào cấu trúc type_info cho lớp.

Ví dụ trong pseudo-c ++:

struct Base
{
    virtual ~Base() {}
};

struct Derived
{
    virtual ~Derived() {}
};


int main()
{
    Base *d = new Derived();
    const char *name = typeid(*d).name(); // C++ way

    // faked up way (this won't actually work, but gives an idea of what might be happening in some implementations).
    const vtable *vt = reinterpret_cast<vtable *>(d);
    type_info *ti = vt->typeinfo;
    const char *name = ProcessRawName(ti->name);       
}

Nói chung, đối số thực sự chống lại RTTI là không thể xác định được việc phải sửa đổi mã ở mọi nơi mỗi khi bạn thêm một lớp dẫn xuất mới. Thay vì chuyển đổi các câu lệnh ở khắp mọi nơi, hãy đưa các yếu tố đó vào các chức năng ảo. Điều này di chuyển tất cả các mã khác nhau giữa các lớp vào chính các lớp, do đó, một dẫn xuất mới chỉ cần ghi đè tất cả các hàm ảo để trở thành một lớp hoạt động đầy đủ. Nếu bạn đã từng phải tìm kiếm một cơ sở mã lớn cho mỗi lần ai đó kiểm tra loại lớp và làm điều gì đó khác biệt, bạn sẽ nhanh chóng học cách tránh xa phong cách lập trình đó.

Nếu trình biên dịch của bạn cho phép bạn tắt hoàn toàn RTTI, thì việc tiết kiệm kích thước mã kết quả cuối cùng có thể là đáng kể, với không gian RAM nhỏ như vậy. Trình biên dịch cần tạo cấu trúc type_info cho mỗi lớp duy nhất có hàm ảo. Nếu bạn tắt RTTI, tất cả các cấu trúc này không cần được đưa vào hình ảnh thực thi.


4
+1 để thực sự giải thích lý do tại sao sử dụng RTTI được coi là một quyết định thiết kế tồi, điều đó không rõ ràng với tôi trước đây.
aguazales

6
Câu trả lời này là sự hiểu biết ở mức độ thấp về sức mạnh của C ++. "Nói chung" và "Trong hầu hết các triển khai" được sử dụng một cách tự do có nghĩa là bạn không nghĩ về cách sử dụng tốt các tính năng ngôn ngữ. Các chức năng ảo và thực hiện lại RTTI không phải là câu trả lời. RTTI là câu trả lời. Đôi khi bạn chỉ muốn biết nếu một đối tượng là một loại nhất định. Đó là lý do tại sao nó ở đó! Vì vậy, bạn mất một vài KB RAM cho một số cấu trúc type_info. Gee ...
huyền bí học

16

Vâng, hồ sơ không bao giờ nói dối.

Vì tôi có một hệ thống phân cấp khá ổn định gồm 18-20 loại không thay đổi nhiều, tôi tự hỏi liệu chỉ cần sử dụng một thành viên enum đơn giản sẽ làm được điều đó và tránh được chi phí RTTI "cao". Tôi đã hoài nghi nếu RTTI thực tế đắt hơn chỉ là iftuyên bố mà nó giới thiệu. Chàng trai ơi, là nó.

Nó chỉ ra rằng RTTI đắt tiền, nhiều hơn nữa đắt hơn tương đương iftuyên bố hoặc một đơn giản switchtrên một biến nguyên thủy trong C ++. Vì vậy, câu trả lời S. Lott là không hoàn toàn chính xác, có thêm chi phí cho RTTI, và nó không do chỉ có một iftuyên bố trong hỗn hợp. Đó là do RTTI rất đắt tiền.

Thử nghiệm này được thực hiện trên trình biên dịch Apple LLVM 5.0, với tối ưu hóa kho được bật (cài đặt chế độ phát hành mặc định).

Vì vậy, tôi có dưới 2 chức năng, mỗi chức năng chỉ ra loại cụ thể của một đối tượng thông qua 1) RTTI hoặc 2) một công tắc đơn giản. Nó làm như vậy 50.000.000 lần. Không để quảng cáo thêm, tôi trình bày cho bạn thời gian chạy tương đối cho 50.000.000 lượt chạy.

nhập mô tả hình ảnh ở đây

Đúng vậy, dynamicCastsmất 94% thời gian chạy. Trong khi regularSwitchkhối chỉ chiếm 3,3% .

Câu chuyện dài ngắn: Nếu bạn có thể đủ năng lượng để kết nối một enumloại như tôi đã làm dưới đây, có lẽ tôi khuyên bạn nên làm điều đó, nếu bạn cần làm RTTI hiệu suất là tối quan trọng. Chỉ cần thiết lập thành viên một lần (đảm bảo có được thông qua tất cả các hàm tạo ) và đảm bảo không bao giờ viết nó sau đó.

Điều đó nói rằng, làm điều này không nên làm rối loạn các thực hành OOP của bạn .. nó chỉ được sử dụng khi thông tin loại đơn giản là không có sẵn và bạn thấy mình bị dồn vào sử dụng RTTI.

#include <stdio.h>
#include <vector>
using namespace std;

enum AnimalClassTypeTag
{
  TypeAnimal=1,
  TypeCat=1<<2,TypeBigCat=1<<3,TypeDog=1<<4
} ;

struct Animal
{
  int typeTag ;// really AnimalClassTypeTag, but it will complain at the |= if
               // at the |='s if not int
  Animal() {
    typeTag=TypeAnimal; // start just base Animal.
    // subclass ctors will |= in other types
  }
  virtual ~Animal(){}//make it polymorphic too
} ;

struct Cat : public Animal
{
  Cat(){
    typeTag|=TypeCat; //bitwise OR in the type
  }
} ;

struct BigCat : public Cat
{
  BigCat(){
    typeTag|=TypeBigCat;
  }
} ;

struct Dog : public Animal
{
  Dog(){
    typeTag|=TypeDog;
  }
} ;

typedef unsigned long long ULONGLONG;

void dynamicCasts(vector<Animal*> &zoo, ULONGLONG tests)
{
  ULONGLONG animals=0,cats=0,bigcats=0,dogs=0;
  for( ULONGLONG i = 0 ; i < tests ; i++ )
  {
    for( Animal* an : zoo )
    {
      if( dynamic_cast<Dog*>( an ) )
        dogs++;
      else if( dynamic_cast<BigCat*>( an ) )
        bigcats++;
      else if( dynamic_cast<Cat*>( an ) )
        cats++;
      else //if( dynamic_cast<Animal*>( an ) )
        animals++;
    }
  }

  printf( "%lld animals, %lld cats, %lld bigcats, %lld dogs\n", animals,cats,bigcats,dogs ) ;

}

//*NOTE: I changed from switch to if/else if chain
void regularSwitch(vector<Animal*> &zoo, ULONGLONG tests)
{
  ULONGLONG animals=0,cats=0,bigcats=0,dogs=0;
  for( ULONGLONG i = 0 ; i < tests ; i++ )
  {
    for( Animal* an : zoo )
    {
      if( an->typeTag & TypeDog )
        dogs++;
      else if( an->typeTag & TypeBigCat )
        bigcats++;
      else if( an->typeTag & TypeCat )
        cats++;
      else
        animals++;
    }
  }
  printf( "%lld animals, %lld cats, %lld bigcats, %lld dogs\n", animals,cats,bigcats,dogs ) ;  

}

int main(int argc, const char * argv[])
{
  vector<Animal*> zoo ;

  zoo.push_back( new Animal ) ;
  zoo.push_back( new Cat ) ;
  zoo.push_back( new BigCat ) ;
  zoo.push_back( new Dog ) ;

  ULONGLONG tests=50000000;

  dynamicCasts( zoo, tests ) ;
  regularSwitch( zoo, tests ) ;
}

13

Cách tiêu chuẩn:

cout << (typeid(Base) == typeid(Derived)) << endl;

RTTI tiêu chuẩn rất tốn kém vì nó phụ thuộc vào việc thực hiện so sánh chuỗi cơ bản và do đó tốc độ của RTTI có thể thay đổi tùy thuộc vào độ dài tên lớp.

Lý do tại sao so sánh chuỗi được sử dụng là để làm cho nó hoạt động nhất quán trên các ranh giới thư viện / DLL. Nếu bạn xây dựng ứng dụng của mình một cách tĩnh và / hoặc bạn đang sử dụng một số trình biên dịch nhất định thì có lẽ bạn có thể sử dụng:

cout << (typeid(Base).name() == typeid(Derived).name()) << endl;

Điều này không được đảm bảo để hoạt động (sẽ không bao giờ cho kết quả dương tính giả, nhưng có thể cho kết quả âm tính giả) nhưng có thể nhanh hơn tới 15 lần. Điều này phụ thuộc vào việc triển khai typeid () để hoạt động theo một cách nhất định và tất cả những gì bạn đang làm là so sánh một con trỏ char nội bộ. Điều này đôi khi cũng tương đương với:

cout << (&typeid(Base) == &typeid(Derived)) << endl;

Bạn Tuy nhiên, có thể sử dụng kết hợp một cách an toàn, sẽ rất nhanh nếu các loại khớp và sẽ là trường hợp xấu nhất đối với các loại chưa từng có:

cout << ( typeid(Base).name() == typeid(Derived).name() || 
          typeid(Base) == typeid(Derived) ) << endl;

Để hiểu liệu bạn có cần tối ưu hóa việc này hay không, bạn cần xem bạn dành bao nhiêu thời gian để nhận gói mới, so với thời gian xử lý gói. Trong hầu hết các trường hợp, một so sánh chuỗi có thể sẽ không phải là một chi phí lớn. (tùy thuộc vào lớp hoặc không gian tên của bạn :: độ dài tên lớp)

Cách an toàn nhất để tối ưu hóa điều này là triển khai typeid của riêng bạn dưới dạng int (hoặc enum Type: int) như một phần của lớp Cơ sở của bạn và sử dụng điều đó để xác định loại của lớp, sau đó chỉ cần sử dụng static_cast <> hoặc reinterpret_cast < >

Đối với tôi, sự khác biệt là khoảng 15 lần trên MS VS 2005 C ++ SP1 không được tối ưu hóa.


2
"RTTI tiêu chuẩn rất tốn kém vì nó phụ thuộc vào việc thực hiện so sánh chuỗi cơ bản" - không, không có gì "Tiêu chuẩn" về điều này; nó chỉ là cách thực hiện của bạn typeid::operatorlàm việc s . Chẳng hạn, GCC trên nền tảng được hỗ trợ, đã sử dụng các so sánh của char *s, mà không cần chúng tôi buộc nó - gcc.gnu.org/onlinesocs/gcc-4.6.3/libstdc++/api/ . Chắc chắn, cách của bạn làm cho MSVC hoạt động tốt hơn nhiều so với mặc định trên nền tảng của bạn, vì vậy, và tôi không biết "một số mục tiêu" sử dụng con trỏ thực sự là gì ... nhưng quan điểm của tôi là hành vi của MSVC không theo bất kỳ cách nào "Tiêu chuẩn".
gạch dưới

7

Đối với một kiểm tra đơn giản, RTTI có thể rẻ như một so sánh con trỏ. Để kiểm tra thừa kế, nó có thể tốn kém như strcmpmọi loại trong cây thừa kế nếu bạn làdynamic_cast từ trên xuống dưới trong một triển khai ngoài kia.

Bạn cũng có thể giảm chi phí bằng cách không sử dụng dynamic_cast và thay vào đó kiểm tra loại rõ ràng thông qua & typeid (...) == & typeid (loại). Mặc dù điều đó không nhất thiết phải hoạt động đối với các tệp dll hoặc mã được tải động khác, nhưng nó có thể khá nhanh đối với những thứ được liên kết tĩnh.

Mặc dù tại thời điểm đó, nó giống như sử dụng một câu lệnh chuyển đổi, vì vậy bạn đi.


1
Bạn có bất kỳ giới thiệu cho phiên bản strcmp? Có vẻ như rất không hiệu quả và không chính xác để sử dụng strcmp để kiểm tra loại.
JaredPar

Trong một triển khai kém có thể có nhiều đối tượng type_info cho mỗi loại, nó có thể triển khai bool type_info :: toán tử == (const type_info & x) const là "! Strcmp (name (), x.name ())"
Greg Rogers

3
Bước vào quá trình tháo gỡ của Dynamic_cast hoặc typeid (). Toán tử == cho MSVC và bạn sẽ đạt được một strcmp trong đó. Tôi giả sử nó ở đó cho trường hợp khủng khiếp mà bạn đang so sánh với một loại được biên dịch trong một. Và nó sử dụng tên được đọc sai, vì vậy ít nhất nó đúng với cùng một trình biên dịch.
MSN

1
bạn phải làm "typeid (...) == typeid (type)" và không so sánh địa chỉ
Johannes Schaub - litb

1
Quan điểm của tôi là bạn có thể làm & typeid (...) == & typeid (blah) khi ra mắt sớm và sẽ an toàn. Nó thực sự có thể không làm bất cứ điều gì hữu ích vì typeid (...) có thể được tạo trên ngăn xếp, nhưng nếu địa chỉ của chúng bằng nhau, thì loại của chúng bằng nhau.
MSN

6

Luôn luôn tốt nhất để đo lường mọi thứ. Trong đoạn mã sau, theo g ++, việc sử dụng nhận dạng kiểu mã hóa tay dường như nhanh hơn khoảng ba lần so với RTTI. Tôi chắc chắn rằng một triển khai mã hóa bằng tay thực tế hơn bằng cách sử dụng các chuỗi thay vì ký tự sẽ chậm hơn, đưa các thời gian gần nhau ..

#include <iostream>
using namespace std;

struct Base {
    virtual ~Base() {}
    virtual char Type() const = 0;
};

struct A : public Base {
    char Type() const {
        return 'A';
    }
};

struct B : public Base {;
    char Type() const {
        return 'B';
    }
};

int main() {
    Base * bp = new A;
    int n = 0;
    for ( int i = 0; i < 10000000; i++ ) {
#ifdef RTTI
        if ( A * a = dynamic_cast <A*> ( bp ) ) {
            n++;
        }
#else
        if ( bp->Type() == 'A' ) {
            A * a = static_cast <A*>(bp);
            n++;
        }
#endif
    }
    cout << n << endl;
}

1
cố gắng không làm điều đó với Dynamic_cast, nhưng với typeid. nó có thể tăng tốc hiệu suất.
Julian Schaub - litb

1
nhưng sử dụng Dynamic_cast thì thực tế hơn, ít nhất là nhìn vào mã của tôi

2
nó thực hiện một điều khác: nó cũng kiểm tra xem bp có trỏ đến một loại có nguồn gốc từ A. your == 'A' hay không kiểm tra xem nó có trỏ chính xác đến 'A' hay không. Tôi cũng nghĩ rằng thử nghiệm là không công bằng phần nào: trình biên dịch có thể dễ dàng thấy bp không thể chỉ ra bất cứ điều gì khác với A. nhưng tôi nghĩ rằng nó không tối ưu hóa ở đây.
Julian Schaub - litb

Dù sao, tôi đã kiểm tra mã của bạn. và nó mang lại cho tôi "0,016s" cho RTTI và "0,044s" cho các cuộc gọi chức năng ảo. (sử dụng -O2)
Johannes Schaub - litb

mặc dù việc thay đổi nó thành sử dụng typeid không tạo ra bất kỳ sự khác biệt nào ở đây (vẫn là 0,016s)
Johannes Schaub - litb

4

Cách đây một thời gian, tôi đã đo chi phí thời gian cho RTTI trong các trường hợp cụ thể của MSVC và GCC cho PowerPC 3ghz. Trong các thử nghiệm tôi đã chạy (một ứng dụng C ++ khá lớn với cây lớp sâu), mỗi ứng dụng có dynamic_cast<>giá từ 0,8 đến 2 giây, tùy thuộc vào việc nó bị trúng hay bị trượt.


2

Vì vậy, RTTI đắt như thế nào?

Điều đó phụ thuộc hoàn toàn vào trình biên dịch bạn đang sử dụng. Tôi hiểu rằng một số sử dụng so sánh chuỗi và những người khác sử dụng thuật toán thực sự.

Hy vọng duy nhất của bạn là viết một chương trình mẫu và xem trình biên dịch của bạn làm gì (hoặc ít nhất là xác định thời gian cần thiết để thực hiện một triệu dynamic_castshoặc một triệu typeidgiây).


1

RTTI có thể rẻ và không nhất thiết cần một strcmp. Trình biên dịch giới hạn kiểm tra để thực hiện phân cấp thực tế, theo thứ tự ngược lại. Vì vậy, nếu bạn có một lớp C là con của lớp B là con của lớp A, thì Dynamic_cast từ A * ptr đến C * ptr chỉ ngụ ý một so sánh con trỏ chứ không phải hai (BTW, chỉ con trỏ bảng vptr là so). Bài kiểm tra giống như "if (vptr_of_obj == vptr_of_C) return (C *) obj"

Một ví dụ khác, nếu chúng ta cố gắng động_cast từ A * đến B *. Trong trường hợp đó, trình biên dịch sẽ kiểm tra cả hai trường hợp (obj là C và obj là B) lần lượt. Điều này cũng có thể được đơn giản hóa thành một thử nghiệm duy nhất (hầu hết các lần), vì bảng chức năng ảo được thực hiện dưới dạng tổng hợp, do đó, thử nghiệm tiếp tục thành "if (offset_of (vptr_of_obj, B) == vptr_of_B)" với

offset_of = return sizeof (vptr_table)> = sizeof (vptr_of_B)? vptr_of_new_methods_in_B: 0

Bố cục bộ nhớ của

vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ]

Làm thế nào để trình biên dịch biết để tối ưu hóa điều này tại thời gian biên dịch?

Tại thời gian biên dịch, trình biên dịch biết hệ thống phân cấp hiện tại của các đối tượng, vì vậy nó từ chối biên dịch kiểu phân cấp động khác nhau Dynamic_casting. Sau đó, nó chỉ phải xử lý độ sâu phân cấp và thêm số lượng thử nghiệm đảo ngược để phù hợp với độ sâu như vậy.

Ví dụ: điều này không biên dịch:

void * something = [...]; 
// Compile time error: Can't convert from something to MyClass, no hierarchy relation
MyClass * c = dynamic_cast<MyClass*>(something);  

-5

RTTI có thể "đắt" vì bạn đã thêm một câu lệnh if mỗi lần bạn thực hiện so sánh RTTI. Trong các lần lặp lồng nhau sâu, điều này có thể tốn kém. Trong một cái gì đó không bao giờ được thực hiện trong một vòng lặp, về cơ bản nó là miễn phí.

Sự lựa chọn là sử dụng thiết kế đa hình thích hợp, loại bỏ câu lệnh if. Trong các vòng lặp lồng nhau sâu, điều này rất cần thiết cho hiệu suất. Mặt khác, nó không quan trọng lắm.

RTTI cũng đắt tiền vì nó có thể che khuất hệ thống phân cấp lớp con (nếu thậm chí có một). Nó có thể có tác dụng phụ trong việc loại bỏ "hướng đối tượng" khỏi "lập trình hướng đối tượng".


2
Không nhất thiết - Tôi sẽ sử dụng nó một cách gián tiếp thông qua Dynamic_cast và giữ nguyên cấu trúc phân cấp, bởi vì tôi cần hạ thấp vì mỗi kiểu con cần có dữ liệu khác nhau (có kích thước thay đổi) phải được áp dụng khác nhau, do đó Dynamic_cast.
Cristián Romo

1
@ Cristián Romo: Vui lòng cập nhật câu hỏi của bạn với những sự thật mới này. Dynamic_cast là một (đôi khi) cái ác cần thiết trong C ++. Hỏi về hiệu suất RTTI khi bạn bị buộc phải làm điều đó không có ý nghĩa gì nhiều.
S.Lott

@ S.Lott: Đã cập nhật. Xin lỗi về sự nhầm lẫn.
Cristián Romo

1
tôi đã làm một thử nghiệm về điều này ngay bây giờ - hóa ra RTTI đắt hơn đáng kể so với iftuyên bố bạn giới thiệu khi bạn kiểm tra thông tin loại thời gian chạy theo cách này.
bobobobo
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.