Qual é o preço do RTTI?

152

Entendo que há um acerto de recurso ao usar o RTTI, mas qual é o tamanho? Em todo lugar que olhei, apenas diz que "o RTTI é caro", mas nenhum deles realmente fornece benchmarks ou dados quantitativos sobre memória, tempo do processador ou velocidade.

Então, quão caro é o RTTI? Eu posso usá-lo em um sistema incorporado onde tenho apenas 4 MB de RAM, então cada bit conta.

Edit: Conforme a resposta de S. Lott , seria melhor se eu incluísse o que realmente estou fazendo. Estou usando uma classe para transmitir dados de diferentes comprimentos e que podem executar ações diferentes , por isso seria difícil fazer isso usando apenas funções virtuais. Parece que o uso de alguns dynamic_casts poderia solucionar esse problema, permitindo que as diferentes classes derivadas passassem pelos diferentes níveis, mas ainda assim permitisse que eles agissem de maneira completamente diferente.

Pelo meu entendimento, dynamic_castusa RTTI, então eu estava pensando em como seria viável usar em um sistema limitado.

Cristián Romo
fonte
1
Após a sua edição - muitas vezes quando me vejo fazendo várias transmissões dinâmicas, percebo que o uso do padrão Visitor corrige as coisas novamente. Isso poderia funcionar para você?
philsquared 23/12/2009
4
Vou colocar desta maneira - eu apenas comecei a usar dynamic_castem C ++ e agora, 9 em 10 vezes quando "quebro" o programa com o depurador, ele quebra dentro da função interna de conversão dinâmica. É muito lento.
user541686
3
RTTI = "informações do tipo em tempo de execução", a propósito.
Novmen

Respostas:

115

Independentemente do compilador, você sempre pode economizar em tempo de execução, se puder fazer isso

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

ao invés de

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

O primeiro envolve apenas uma comparação de std::type_info; o último envolve necessariamente atravessar uma árvore de herança mais comparações.

Além disso, como todo mundo diz, o uso de recursos é específico da implementação.

Concordo com os comentários de todos os outros de que o remetente deve evitar a RTTI por motivos de design. No entanto, não são boas razões para utilizar RTTI (principalmente por causa do boost :: houver). Isso é útil para saber o uso real de recursos em implementações comuns.

Recentemente, fiz várias pesquisas sobre RTTI no GCC.

tl; dr: RTTI no GCC usa espaço desprezível e typeid(a) == typeid(b)é muito rápido, em muitas plataformas (Linux, BSD e talvez plataformas embarcadas, mas não no mingw32). Se você sabe que sempre estará em uma plataforma abençoada, o RTTI está muito perto de ser gratuito.

Detalhes corajosos:

O GCC prefere usar um C ++ ABI "neutro em relação ao fornecedor" [1] e sempre usa esse ABI para destinos Linux e BSD [2]. Para plataformas que suportam essa ABI e também com ligação fraca, typeid()retorna um objeto consistente e exclusivo para cada tipo, mesmo através dos limites de vinculação dinâmica. Você pode testar &typeid(a) == &typeid(b)ou apenas confiar no fato de que o teste portátil typeid(a) == typeid(b), na verdade, apenas compara um ponteiro internamente.

Na ABI preferida do GCC, uma tabela de classe sempre mantém um ponteiro para uma estrutura RTTI por tipo, embora possa não ser usada. Portanto, uma typeid()chamada em si só deve custar tanto quanto qualquer outra pesquisa de vtable (o mesmo que chamar uma função de membro virtual) e o suporte a RTTI não deve usar espaço extra para cada objeto.

Pelo que pude entender, as estruturas RTTI usadas pelo GCC (todas essas são as subclasses de std::type_info) contêm apenas alguns bytes para cada tipo, além do nome. Não está claro para mim se os nomes estão presentes no código de saída, mesmo com -fno-rtti. De qualquer forma, a alteração no tamanho do binário compilado deve refletir a alteração no uso da memória de tempo de execução.

Uma experiência rápida (usando o GCC 4.4.3 no Ubuntu 10.04 de 64 bits) mostra que, -fno-rttina verdade, aumenta o tamanho binário de um programa de teste simples em algumas centenas de bytes. Isso acontece consistentemente nas combinações de -ge -O3. Não sei por que o tamanho aumentaria; uma possibilidade é que o código STL do GCC se comporte de maneira diferente sem o RTTI (já que as exceções não funcionarão).

[1] Conhecido como Itanium C ++ ABI, documentado em http://www.codesourcery.com/public/cxx-abi/abi.html . Os nomes são terrivelmente confusos: o nome se refere à arquitetura de desenvolvimento original, embora a especificação ABI funcione em várias arquiteturas, incluindo i686 / x86_64. Os comentários na fonte interna e no código STL do GCC referem-se ao Itanium como a "nova" ABI, em contraste com a "antiga" usada anteriormente. Pior, o "novo" / Itanium ABI refere-se a todas as versões disponíveis no -fabi-version; a ABI "antiga" antecedeu esse controle de versão. O GCC adotou a ITB Itanium / versioned / "new" na versão 3.0; a ABI "antiga" foi usada na versão 2.95 e anterior, se eu estiver lendo os registros de alterações corretamente.

[2] Não consegui encontrar nenhuma std::type_infoestabilidade de objeto da lista de recursos por plataforma. Para compiladores I teve acesso, eu usei o seguinte: echo "#include <typeinfo>" | gcc -E -dM -x c++ -c - | grep GXX_MERGED_TYPEINFO_NAMES. Essa macro controla o comportamento de operator==for std::type_infono STL do GCC, a partir do GCC 3.0. Eu descobri que o mingw32-gcc obedece à ABI do Windows C ++, onde os std::type_infoobjetos não são exclusivos para um tipo nas DLLs; typeid(a) == typeid(b)chamadas strcmpdebaixo das cobertas. Especulo que em destinos incorporados de programa único como o AVR, onde não há código para vincular, os std::type_infoobjetos são sempre estáveis.

sbrudenell
fonte
6
Exceções funcionam sem RTTI. (Você está autorizado a lançar uma inte não há vtable em que :))
Billy ONeal
3
@ Deduplicator: E, no entanto, quando eu desligo o RTTI no meu compilador, eles funcionam muito bem. Desculpe por desapontá-lo.
Billy ONeal
5
O mecanismo de tratamento de exceções deve poder trabalhar com qualquer tipo, preenchendo alguns requisitos básicos. Você pode sugerir como lidar com exceções de tipo arbitrário de lançamento e captura através dos limites do módulo sem o RTTI. Por favor, considere que a conversão para cima e para baixo é necessária.
Deduplicator
15
typeid (a) == typeid (b) NÃO é o mesmo que B * ba = dynamic_cast <B *> (& a). Experimente em objetos com herança múltipla como um nível aleatório na árvore de classes derivada e você encontrará que typeid () == typeid () não produzirá um resultado positivo. dynamic_cast é a única maneira de pesquisar na árvore de herança de verdade. Pare de pensar em possíveis economias desativando o RTTI e use-o. Se você tiver excesso de capacidade, otimize o inchaço do código. Tente evitar o uso de dynamic_cast dentro de loops internos ou qualquer outro código crítico de desempenho e você ficará bem.
Mysticcoder
3
@ codificador É por isso que o artigo afirma explicitamente isso the latter necessarily involves traversing an inheritance tree plus comparisons. @CoryB Você pode "se dar ao luxo" de fazê-lo quando não precisar dar suporte à conversão de toda a árvore de herança. Por exemplo, se você deseja encontrar todos os itens do tipo X em uma coleção, mas não aqueles que derivam de X, o que você deve usar é o primeiro. Se você também precisar encontrar todas as instâncias derivadas, precisará usá-las.
Aidiakapi
48

Talvez esses números ajudem.

Eu estava fazendo um teste rápido usando isso:

  • Clock do GCC () + perfilador do XCode.
  • 100.000.000 de iterações de loop.
  • Intel Xeon de núcleo duplo de 2 x 2,66 GHz.
  • A classe em questão é derivada de uma única classe base.
  • typeid (). name () retorna "N12fastdelegate13FastDelegate1IivEE"

5 casos foram testados:

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 é apenas o meu código real, pois eu precisava criar um objeto desse tipo antes de verificar se é semelhante a um que eu já tenho.

Sem otimização

Para os quais foram os resultados (calculei a média de algumas execuções):

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.

Portanto, a conclusão seria:

  • Para casos de transmissão simples, sem otimização, typeid()é mais do que duas vezes mais rápido que dyncamic_cast.
  • Em uma máquina moderna, a diferença entre os dois é de cerca de 1 nanossegundo (um milionésimo de milissegundo).

Com otimização (-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.

Portanto, a conclusão seria:

  • Para casos de transmissão simples com otimização, typeid()é quase 20 vezes mais rápido que dyncamic_cast.

Gráfico

insira a descrição da imagem aqui

O código

Conforme solicitado nos comentários, o código está abaixo (um pouco confuso, mas funciona). 'FastDelegate.h' está disponível aqui .

#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;
}
Izhaki
fonte
1
Obviamente, o elenco dinâmico é mais geral - funciona se o item for mais derivado. Por exemplo, class a {}; class b : public a {}; class c : public b {};quando o destino é uma instância de c, funcionará bem ao testar a classe bcom dynamic_cast, mas não com a typeidsolução. Ainda razoável, porém, +1
Billy ONeal
34
Esse benchmark é totalmente falso com otimizações : a verificação de tipo é invariável e é removida do loop. Não é nada interessante, é um benchmarking básico, não-não.
Restabeleça Monica
3
@ Kuba: Então a referência é falsa. Essa não é uma razão para fazer benchmarks com otimizações desativadas; essa é uma razão para escrever melhores benchmarks.
Billy ONeal
3
mais uma vez, isso é um fracasso. "Para casos simples de conversão com otimização, typeid () é quase 20 vezes mais rápido que dyncamic_cast." eles não fazem a mesma coisa. Há uma razão pela qual dynamic_cast é mais lento.
Mysticcoder
1
@KubaOber: total de +1. isso é tão clássico. e deve parecer óbvio pela aparência do número de ciclos que isso aconteceu.
precisa saber é o seguinte
38

Depende da escala das coisas. Na maioria das vezes, são apenas algumas verificações e algumas referências de ponteiro. Na maioria das implementações, na parte superior de cada objeto que possui funções virtuais, há um ponteiro para uma vtable que contém uma lista de ponteiros para todas as implementações da função virtual nessa classe. Eu acho que a maioria das implementações usaria isso para armazenar outro ponteiro para a estrutura type_info da classe.

Por exemplo, no 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);       
}

Em geral, o argumento real contra o RTTI é a impossibilidade de manter o código em todos os lugares sempre que você adiciona uma nova classe derivada. Em vez de alternar instruções em todos os lugares, leve-as em funções virtuais. Isso move todo o código que é diferente entre as classes para as próprias classes, de modo que uma nova derivação só precisa substituir todas as funções virtuais para se tornar uma classe totalmente funcional. Se você já teve que procurar em uma grande base de códigos toda vez que alguém verifica o tipo de uma classe e faz algo diferente, rapidamente aprenderá a ficar longe desse estilo de programação.

Se o seu compilador permitir que você desative totalmente o RTTI, a economia final de tamanho de código resultante poderá ser significativa, com um espaço de RAM tão pequeno. O compilador precisa gerar uma estrutura type_info para cada classe com uma função virtual. Se você desativar o RTTI, todas essas estruturas não precisarão ser incluídas na imagem executável.

Eclipse
fonte
4
+1 por realmente explicar por que o uso do RTTI é considerado uma má decisão de design, que antes não estava clara para mim.
Aguazales
6
Esta resposta é uma compreensão de baixo nível do poder do C ++. "Em geral" e "Na maioria das implementações" usadas liberalmente significa que você não está pensando em como usar bem os recursos de idiomas. Funções virtuais e reimplementação de RTTI não são a resposta. RTTI é a resposta. Às vezes, você só quer saber se um objeto é de um determinado tipo. É por isso que está lá! Então você perde alguns KB de RAM em algumas estruturas type_info. Gee ...
mysticcoder
16

Bem, o criador de perfil nunca mente.

Como tenho uma hierarquia bastante estável de 18 a 20 tipos que não está mudando muito, perguntei-me se apenas o uso de um membro enum'd simples faria o truque e evitaria o suposto "alto" custo do RTTI. Fiquei cético se o RTTI fosse de fato mais caro do que apenas a ifdeclaração que ele introduz. Garoto, oh garoto, é isso?

Acontece que o RTTI é caro, muito mais caro que uma ifinstrução equivalente ou um simples switchem uma variável primitiva em C ++. Então, a resposta de S. Lott não é completamente correto, não é custo extra para RTTI, e é não devido a apenas ter uma ifdeclaração na mistura. É porque o RTTI é muito caro.

Esse teste foi realizado no compilador Apple LLVM 5.0, com as otimizações de estoque ativadas (configurações padrão do modo de liberação).

Então, eu tenho abaixo de 2 funções, cada uma das quais descobre o tipo concreto de um objeto via 1) RTTI ou 2) uma opção simples. Faz isso 50.000.000 de vezes. Sem mais delongas, apresento os tempos de execução relativos de 50.000.000 de execuções.

insira a descrição da imagem aqui

É isso mesmo, o que dynamicCastslevou 94% do tempo de execução. Enquanto o regularSwitchbloco levou apenas 3,3% .

Para encurtar a história: se você puder pagar a energia necessária para conectar um enumtipo como eu fiz abaixo, provavelmente o recomendaria, se você precisar fazer RTTI e o desempenho for primordial. É necessário definir o membro apenas uma vez (certifique-se de obtê-lo através de todos os construtores ) e nunca escrevê-lo posteriormente.

Dito isto, isso não deve atrapalhar suas práticas de POO. Ele deve ser usado apenas quando as informações de tipo simplesmente não estão disponíveis e você se vê envolvido no uso de 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 ) ;
}
bobobobo
fonte
13

A maneira padrão:

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

O RTTI padrão é caro, pois depende de uma comparação de sequência subjacente e, portanto, a velocidade do RTTI pode variar dependendo do tamanho do nome da classe.

O motivo pelo qual as comparações de strings são usadas é fazê-lo funcionar de maneira consistente nos limites da biblioteca / DLL. Se você criar seu aplicativo estaticamente e / ou estiver usando determinados compiladores, provavelmente poderá usar:

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

O que não garante que funcione (nunca dará um falso positivo, mas pode dar falsos negativos), mas pode ser até 15 vezes mais rápido. Isso depende da implementação de typeid () para funcionar de uma certa maneira e tudo o que você está fazendo é comparar um ponteiro interno de char. Às vezes, isso também é equivalente a:

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

No entanto, você pode usar um híbrido com segurança, que será muito rápido se os tipos corresponderem e será o pior caso para tipos sem correspondência:

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

Para entender se você precisa otimizar isso, precisa ver quanto tempo gasta recebendo um novo pacote, em comparação com o tempo necessário para processar o pacote. Na maioria dos casos, uma comparação de cadeias provavelmente não será uma sobrecarga grande. (dependendo da sua classe ou namespace :: tamanho do nome da classe)

A maneira mais segura de otimizar isso é implementar seu próprio typeid como um int (ou um enum Type: int) como parte de sua classe Base e usá-lo para determinar o tipo da classe e, em seguida, basta usar static_cast <> ou reinterpret_cast < >

Para mim, a diferença é aproximadamente 15 vezes no MS VS 2005 C ++ SP1 não otimizado.

Marius
fonte
2
"O RTTI padrão é caro, porque depende de fazer uma comparação de string subjacente" - não, não há nada de "padrão" nisso; é exatamente como a sua implementação typeid::operatorfunciona . O GCC em uma plataforma suportada, por exemplo, já usa comparações de char *s, sem a necessidade de forçá-lo - gcc.gnu.org/onlinedocs/gcc-4.6.3/libstdc++/api/… . Claro, seu jeito faz com que o MSVC se comporte muito melhor do que o padrão em sua plataforma, então parabéns, e eu não sei quais são os "alguns destinos" que usam ponteiros nativamente ... mas meu ponto é que o comportamento do MSVC não é de forma alguma "Padrão".
underscore_d
7

Para uma verificação simples, o RTTI pode ser tão barato quanto uma comparação de ponteiro. Para verificação de herança, pode ser tão caro quanto strcmppara todos os tipos em uma árvore de herança, se você estiver dynamic_castindo de cima para baixo em uma implementação disponível.

Você também pode reduzir a sobrecarga não usando dynamic_caste, em vez disso, verificando o tipo explicitamente via & typeid (...) == & typeid (type). Embora isso não funcione necessariamente para .dlls ou outro código carregado dinamicamente, pode ser bastante rápido para itens vinculados estaticamente.

Embora, nesse ponto, seja como usar uma instrução switch, lá está você.

MSN
fonte
1
Você tem alguma referência para a versão strcmp? Parece extremamente ineficiente e impreciso usar o strcmp para uma verificação de tipo.
JaredPar 24/02/09
Em uma implementação ruim que poderia ter vários objetos type_info por tipo, ele poderia implementar o tipo bool type_info :: operator == (const type_info & x) const como "! Strcmp (name (), x.name ())"
Greg Rogers
3
Entre na desmontagem de dynamic_cast ou typeid (). Operator == para MSVC e você atingirá um strcmp lá. Presumo que esteja lá no caso horrível em que você está comparando com um tipo compilado em outra .dll. E ele usa o nome mutilado, pelo menos está correto, considerando o mesmo compilador.
MSN
1
você deveria fazer "typeid (...) == typeid (type)" e não comparar o endereço
Johannes Schaub - litb 24/02
1
O que quero dizer é que você pode fazer & typeid (...) == & typeid (blá) como uma saída precoce e estará seguro. Na verdade, pode não ser útil, pois typeid (...) pode ser gerado na pilha, mas se os endereços forem iguais, então os tipos serão iguais.
MSN
6

É sempre melhor medir as coisas. No código a seguir, em g ++, o uso da identificação de tipo codificado manualmente parece ser cerca de três vezes mais rápido que o RTTI. Tenho certeza de que uma implementação codificada à mão mais realista, usando strings em vez de caracteres, seria mais lenta, aproximando os tempos.

#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;
}

fonte
1
tente não fazer isso com dynamic_cast, mas com typeid. isso poderia acelerar o desempenho.
Johannes Schaub - litb 24/02
1
mas usando dynamic_cast é mais realista, pelo menos, olhar para o meu código
2
faz algo diferente: verifica também se bp aponta para um tipo derivado de A. seu == 'A' verifica se aponta exatamente para um 'A'. Eu também acho que o teste é injusto: o compilador pode ver facilmente o bp não pode apontar para algo diferente de A. mas acho que não otimiza aqui.
Johannes Schaub - litb 24/02/09
de qualquer maneira, testei seu código. e fornece "0,016s" para RTTI e "0,044s" para as chamadas de função virtual. (usando -O2)
Johannes Schaub - litb 24/02
embora alterá-lo para uso typeid não faz qualquer diferença aqui (ainda 0.016s)
Johannes Schaub - litb
4

Há um tempo atrás, medi os custos de tempo para RTTI nos casos específicos de MSVC e GCC para um PowerPC de 3ghz. Nos testes que eu executei (um aplicativo C ++ bastante grande, com uma árvore de classes profunda), cada um dynamic_cast<>custa entre 0,8 μs e 2 μs, dependendo se ocorreu ou não.

Crashworks
fonte
2

Então, quão caro é o RTTI?

Isso depende inteiramente do compilador que você está usando. Entendo que alguns usam comparações de strings e outros usam algoritmos reais.

Sua única esperança é escrever um programa de exemplo e ver o que seu compilador faz (ou pelo menos determinar quanto tempo leva para executar um milhão dynamic_castsou um milhão de typeids).

Max Lybbert
fonte
1

O RTTI pode ser barato e não precisa necessariamente de um strcmp. O compilador limita o teste para executar a hierarquia real, em ordem inversa. Portanto, se você tiver uma classe C filha da classe B, filha da classe A, dynamic_cast de A * ptr para C * ptr implica apenas uma comparação de ponteiro e não duas (BTW, apenas o ponteiro da tabela vptr é comparado). O teste é como "se (vptr_of_obj == vptr_of_C) retorna (C *) obj"

Outro exemplo, se tentarmos transmitir dynamic_cast de A * para B *. Nesse caso, o compilador verificará os dois casos (obj sendo um C e obj sendo um B) alternadamente. Isso também pode ser simplificado para um único teste (na maioria das vezes), como a tabela de funções virtuais é feita como uma agregação, de modo que o teste é retomado para "if (offset_of (vptr_of_obj, B) == vptr_of_B)" com

offset_of = tamanho do retorno (tabela vptr)> = tamanho (vptr_of_B)? vptr_of_new_methods_in_B: 0

O layout da memória de

vptr_of_C = [ vptr_of_A | vptr_of_new_methods_in_B | vptr_of_new_methods_in_C ]

Como o compilador sabe otimizar isso em tempo de compilação?

No momento da compilação, o compilador conhece a hierarquia atual dos objetos e, portanto, se recusa a compilar hierarquia de tipos diferentes dynamic_casting. Depois, basta lidar com a profundidade da hierarquia e adicionar a quantidade invertida de testes para corresponder a essa profundidade.

Por exemplo, isso não compila:

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

O RTTI pode ser "caro" porque você adicionou uma instrução if toda vez que faz a comparação do RTTI. Em iterações profundamente aninhadas, isso pode ser caro. Em algo que nunca é executado em um loop, é essencialmente gratuito.

A escolha é usar o design polimórfico adequado, eliminando a instrução if. Em loops profundamente aninhados, isso é essencial para o desempenho. Caso contrário, não importa muito.

O RTTI também é caro porque pode obscurecer a hierarquia da subclasse (se houver mesmo). Pode ter o efeito colateral de remover o "orientado a objetos" da "programação orientada a objetos".

S.Lott
fonte
2
Não necessariamente - eu ia usá-lo indiretamente via dynamic_cast e manter a hierarquia no lugar, porque preciso fazer o downcast porque cada subtipo precisa ter dados diferentes (de tamanho variável) que devem ser aplicados de maneira diferente, daí o dynamic_cast.
Cristián Romo
1
@ Cristián Romo: Atualize sua pergunta com esses novos fatos. dynamic_cast é um mal (às vezes) necessário em C ++. Perguntar sobre o desempenho do RTTI quando você é forçado a fazer isso não faz muito sentido.
245/09 S.Lott
@ S.Lott: Atualizado. Desculpe a confusão.
Cristián Romo
1
Eu fiz um experimento sobre isso agora - verifica-se que o RTTI é significativamente mais caro do que a ifinstrução que você introduz quando verifica as informações sobre o tipo de tempo de execução dessa maneira.
bobobobo