Como é a Performance da RTTI?

Trabalhando em um determinado projeto, o primeiro caminho que tomei foi utilizar as funcionalidades da RTTI, porque realmente gosto como ela funciona. Todavia, deparei-me com a possibilidade de realizar a mesma coisa de uma forma mais simples, e sem a necessidade de nenhum processamento em runtime, visto que rodaria através do  código compilado. A primeira coisa que pensei foi: claro que vou alterar para a forma mais simples, mas qual seria o prejuízo na performance, se optasse pela RTTI? Isso foi algo que não consegui responder no primeiro momento.

Foi então que fiz um teste simples. Testaria as duas formas e mediria a performance, para saber o quanto a RTTI (por utilizar o processamento em runtime) prejudicaria a performance em rotinas diversas vezes executadas.

Segue a implementação do teste:

A diferença das duas formas de execução é obtenção do método e sua execução pela RTTI, ao contrário do cast diretamente para a interface e sua execução.

Veja que a utilização da RTTI, por sua própria natureza, utiliza muito mais código e muito mais processamento, porque o fluxo para obter o mesmo resultado é muito mais longo, embora muito mais flexível. Mas qual seria o resultado?

Realmente fiquei muito impressionado. Só pela leitura do código, imaginei que a RTTI fosse muito mais lento, mas não foi isso o que aconteceu. Ela deveras possui uma performance inferior, mas nada a se envergonhar.

  • Para 100.000 iterações, as duas rodaram em menos de 1 segundo.
  • Para 1.000.000 iterações,  pela interface foi menos de 1 segundo, enquanto pela RTTI durou próximo à 6 segundos.

A partir daí, o tempo de diferença aumenta exponencialmente, lógico.

Quero apenas chamar a atenção para o fato de que dificilmente rodamos rotinas mais de 100.000 vezes. Quando necessário, a melhor performance deve ser utilizada, mas acredito, embasado na evidência àcima, que na grande maioria dos casos, pode-se utilizar a RTTI sem prejuízo algum da performance, que seja sensível ao usuário.

Tem uma visão diferente? Comente abaixo e vamos aprender ainda mais juntos.

Run-time Type Information (RTTI)

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti

No momento da compilação do programa, o compilador reúne informações a respeito de todos os elementos do sistema, como classes, records, enumeradores, e outras estruturas, e disponibiliza essas informações para utilização em tempo de execução (run-time). Imagine que, enquanto o software estiver em execução, é possível saber quantos métodos as classes possuem, quantas propriedades, variáveis, etc. Imagine ainda que essas informações são tão detalhadas que é possível identificar todos os parâmetros de entrada e saída do método e seus tipos. Imagine tambem que é possível invocar os métodos (procedures e funções), quando necessário.

O conceito pode ser claramente entendido quando você utilizar os recursos, porque será notável o poder que essa funcionalidade disponibiliza ao desenvolvedor. Em outras linguagens de programação, o mesmo conceito possui o nome de reflection.

Algo parecido era utilizado desde o início do Delphi, para captar informações para o object inspector, por exemplo, mas não era tão avançado e documentado como as alterações que houveram na RTTI no Delphi 2010.

Junto com as alterações da RTTI, foi adicionado o recurso de atributos, que é a capacidade de adicionar suas próprias informações (metadados) às classes. Veremos isso detalhadamento depois.

Classe vs Instância

É possível obter tanto informações da classe (informações da abstração) e outras estruturas quanto informações do objeto instanciado. Existe uma diferênça significativa nisso, porque quando trabalhamos com as instâncias, temos o poder de alterar os valores atuais daquela instância.

TRTTIContext

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiContext

TRttiContext é um record fundamental de onde todas as informações podem ser obtidas. A própria documentação da embarcadero o define como o pilar da Rtti. Ele é o responsável pela exposição das informações para utilização. Uma curiosidade é que apenas uma única instância de TRttiContext é realmente criada dentro da aplicação (e liberada no término da aplicação).

O seu uso ficará mais claro quando iniciarmos os exemplos, todavia atente-se que todo o uso da RTTI deve provir dele, e você NÂO DEVE INSTANCIAR AS OUTRAS CLASSES DA RTTI DIRETAMENTE! Tudo deve vir através da TRttiContext.

TRttiType

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiType

Essa classe é responsável por todos os objetos que retornam informações da Rtti. Nunca crie uma classe da Rtti diretamente. Sempre a obtenha através da TRttiContext.

Nossa classe de exemplo

Vamos declarar uma classe de exemplo para podermos obter informações dela em tempo de execução:

TRttiField

Documentação: http://docwiki.embarcadero.com/Libraries/Berlin/en/System.Rtti.TRttiField

A classe TRttiField é a classe responsável pelas informações das variáveis (fields) de uma classe ou record. Através dela é possível identificar quais campos existem, seus tipos e valores, além de poder alterar esses valores em tempo de execução, via Rtti, dos objetos instanciados:

Obs: Também é possível obter as informações dos campos através de GetField(‘NomeDaVariavel’).

TRttiProperty

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiProperty

A classe TRttiProperty é responsável por trazer as informações das propriedades. Com ela é possível obter informações de quais propriedades existem na estrutura (classe ou record), quais seus tipos, quais seus índices, seus métodos get e set, valor de retorno (para os objetos instanciados), entre outras:

Obs: Também é possível obter as informações dos campos através de GetProperty(‘NomeDaPropriedade’).

TRttiMethods

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiMethod

A classe TRttiMethods é a responsável por trazer as informações dos métodos (procedures e funções) da estrutura (classe ou record). Uma particularidade dos métodos é a existência das informações dos parâmetros dos métodos. Através da RTTI é possível obter informações da quantidade de parâmetros, quais seus tipos, etc:

Existe uma diferenciação na chamado dos métodos entre GetMethods e GetDeclaredMethods. Uma classe pode ser parte de uma linha de herança. Para esses casos, GetMethods busca todos os métodos da classe, inclusive aqueles declarados nas classes superiores.  GetDeclaredMethods, ao contrário, retorna apenas os métodos declarados na classe que está sendo verificada, indiferentemente se ela herda de outra classe ou não.

Tente trocar essa chamada no código de exemplo e veja como o retorno é afetado.

 

TRttiMethod.MethodKind

Documentaçãohttp://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiMethod.MethodKind

Existe muitas vezes a necessidade de obtermos a informação do típo do método (função, procedure, etc), que pode ser feito através do retorno de TRttiMethod.MethodKind. As opções podem ser as abaixo (conforme enumerador TMethodKind):

mkProcedure – O método é uma procedure.
mkFunction – O método é uma função.
mkDestructor – O método é um destrutor de classe.
mkConstructor – O método é um construtor de classe.
mkClassProcedure – O método é uma class procedure (procedure estática).
mkClassFunction – O método é uma class function (função estática).
mkClassConstructor – O método é um class constructor (construtor estático).
mkOperatorOverload – O método é uma sobrecarga de um método de operações (matemáticas, casts, etc)
mkSafeProcedure – O método é uma procedure segura.
mkSafeFunction – O método é uma funçção segura.

Documentação do enumerador TMethodKind: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.TypInfo.TMethodKind

 

TRttiMethod.CallingConvention

Documentação: http://docwiki.embarcadero.com/Libraries/Seattle/en/System.Rtti.TRttiMethod.CallingConvention

É possível inclusive obter a convenção da chamada do método através de TRttiMethod.CallingConvention. Ela pode ser:

  • ccReg – O método utiliza a convenção de chamada register.
  • ccCdecl – O método utiliza a convenção de chamada cdecl.
  • ccPascal – O método utiliza a convenção de chamada pascal.
  • ccStdCall – O método utiliza a convenção de chamada stdcall
  • ccSafeCall – O método utiliza a convenção de chamada safecall.

(conforme enumerador System.TypInfo.TCallConv: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.TypInfo.TCallConv)

Quando você declara uma função ou procedure, você pode definir uma convenção de chamada, usando uma das diretivas register, pascal, cdecl, stdcall, safecall, e winapi.

As convenções de chamadas definem a ordem em que os parâmetros serão passados para o método. Eles ainda afetam a forma como os parâmetros são retirados da pilha da memória (quer saber mais sobre isso, click aqui), o uso dos registradores e a forma de tratamento das exceções. A convenção padrão (quando você não define alguma explicitamente) é a register.

Para detalhes maiores sobre as convenções das chamadas, você pode acessar diretamente a documentação oficial da Embarcadero: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Procedures_and_Functions_(Delphi)#Calling_Conventions

e

http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Calling_Convention_Options_(BCC32)

 

TRttiMethod.CodeAddress

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiMethod.CodeAddress

Caso necessário, você pode obter o ponteiro para o endereço da memória onde o método foi alocado com  TRttiMethod.CodeAddress.

Evocando dinamicamente um método através da RTTI

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiMethod.Invoke

É possível evocarmos um método de uma instância de forma simples. A partir do momento que temos nosso método identificado, podemos simplesmente chamar Metodo.Invoke:

Repare que é possível passar os parâmetros da função, assim como receber o retorno e utilizá-lo posteriormente.

TRttiArrayType

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiArrayType

Você também pode obter informações de arrays através da classe TRttiArrayType:

TRttiRecordType

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiRecordType

É possível obter informações de records, assim como das classes. Vamos primeiramente declarar um record:

Agora vamos obter as informações dele através da RTTI:

Como pode ser observado, é muito parecido com o que já acontece nas classes.

TRttiPackage

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiPackage

Você pode obter inclusive informações das packages do delphi através da classe TRttiPackage. Essas informação serão apenas das packages de run-time atualmente carregadas na aplicação. Por padrão, ela retorna a própria aplicação:

 

TRttiOrdinalType

Documentação: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TRttiOrdinalType

É possível obter informações dos tipos ordinais, como Integer, Byte e Word, por exemplo:

Atributos

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Attributes_and_RTTI

Talvez esse seja o recurso mais conhecido da RTTI, porque através dele é possível criar seus próprios metadados das suas estruturas e recuperar isso posteriormente em tempo de execução. Os atributos não alteram a funcionalidade da classe, e sim criam informações extras que podem ser utilizadas:

Diretiva de compilação {$M} ou {$TYPEINFO}

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Run-Time_Type_Information_(Delphi)

A diretiva de compilaçãao {$M} ou {$TYPEINFO} controla a geração das informações de metadados para a utilização da RTTI. Basicamente ela habilita ({$M+} ou {$TYPEINFO ON}) ou não ({$M-} ou {$TYPEINFO OFF}) a geração das informações de metadados. As informações de metadados são as informações da Rtti, que são utilizadas em todas as classes da Rtti. Sem elas, TRttiMethods.GetDeclaredMethods não retornaria nada, por exemplo.

Obs: Quando declaramos um método público ou published, o compilador automaticamente habilita a geração dessas informações para utilização pela RTTI.

Diretiva de compilação {$METHODINFO}

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/METHODINFO_directive_(Delphi)

A diretiva de compilação {$METHODINFO} funciona apenas quando a diretiva {$TYPEINFO} estiver habilitada ({$TYPEINFO ON}).

Ela pode ser:

  • {$METHODINFO ON} – Quando está habilitada.
  • {$METHODINFO OFF} – Quando está desabilitada.

Quando está habilitada, ela produz informações adicionais para os métodos de uma interface. Provavelmente foi desenvolvida para ser utilizada em camadas mais baixas das ferramentas de redes da Embarcadero, como o DataSnap, por exemplo, visto que a documentação não diz muito a respeito, embora recomende “raramente” utilizá-la.

Diretiva de compilação {$RTTI}

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/RTTI_directive_(Delphi)

A ideia por tras da directiva {RTTI} é habilitar ou desabilitar as funcionalidades extendidas da RTTI.

A princípio, os métodos obtidos através de GetMethods não trazem as informações dos métodos protected e private. Ao declarar a directiva {RTTI}, estamos determinando que o compilador também adicione essa e outras informações aos metadados do fonte.

A RTTI pode ser definida como:

onde:

  • INHERIT – especifica que as configurações da RTTI serão iguais às das suas classes base:
  • EXPLICIT – especifica que as configurações da RTTI serão sobrepostos (override) nos métodos herdados.
  • Visibility-Clause – especifica se serão os métodos, propriedades e ou campos que serão herdados/sobrepostos. Ainda é possível determinar para eles, quais serão afetados através de suas visibilidades: [vcPrivate],[vcProtected], [vcPublic], [vcPublished].

Você pode herdar apenas alguns métodos ou propriedades, através de:

Conforme documentação do produto

Caso você queira sobrepor as configurações de algum método herdado, é possível:

Conforme documentação do produto

Isso fará com que as configurações da RTTI da classe base sejam substituídas pelas explicitamente definidas na directiva. No caso acima, somente os métodos e propriedades públicas terão as funcionalidades da RTTI extendidas.

É possível ainda desabilitar todas as funcionalidades extendidas herdadas da classe base:

Conforme documentação do produto

Nesse caso, estamos sobrepondo as configurações da herança e determinando que nenhuma informação extendida da RTTI será emitida para essa classe.

Todas essas opções da directiva dificilmente serão utilizadas, mas elas existem para dar flexibilidade nos casos onde são necessários.

Diretiva de compilação {$WEAKLINKRTTI }

Documentação: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/WEAKLINKRTTI_directive_(Delphi)

Por padrão, as informações necessárias sobre os métodos para a utilização da RTTI são geradas para dentro da aplicação (claro, dependendo das diretivas, conforme vimos anteriormente). Quando a funcionalidade de invocação de métodos da RTTI é utilizada, é necessário que essas informações de métodos estejam compiladas dentro do binário, para seu correto funcionamento. Todavia, existem casos onde a aplicação não faz uso das chamadas dinâmicas de métodos pela RTTI. Assim, você pode utilizar {$WEAKLINKRTTI  ON} para determinar que essas informações de métodos não sejam gerados para dentro da aplicação, impedindo que essas informações “encham” o executável. Mas lembre-se que isso somente é válido para os casos onde não exista a chamada dinâmica dos métodos (invoke).

Escopo da diretivas

Todas as diretivas acima possuem escopo local, ou seja, produzem efeitos apenas onde são declaradas (para as classes, por exemplo). Existe, todavia, uma forma de habilitar a diretiva para todo o projeto. Basta colocá-la logo antes da cláusula uses no DPR. Assim, o escopo da diretiva será para todo o projeto. O mesmo pode ser feito para as units, que terão escopo dentro daquela unit.

Tratamento de excessões com RTTI

A RTTI adicionou alguns tipos de exceções que podem ocorrer quando trabalhamos com suas classes:

EInsufficientRtti

EInsufficientRtti é disparado quando a RTTI precisa de mais informações geradas para poder retornar as solicitações de informações, mas por alguma razão (provavelmente pelo uso das diretivas) essas informações não foram geradas pelo compilador.

ENonPublicType

ENonPublicType é disparado quando as informações “não-públicas” são solicitadas mas a RTTI não pode fornecer ou não possui essas informações privadas.

EInvocationError

EInvocationError é disparado quando ocorre um erro ao invocar um método através da RTTI. Isso geralmente ocorre devido a passagem de parâmetros, seja pela quatidade, seja pelo tipo passado.

EArgumentException

Embora não seja exclusivo da RTTI, EArgumentException é disparado caso haja algum problema na criação (instanciação) do atributo.

O problema dos metadados da RTTI

Embora todas as informações produzidas para a utilização da RTTI sejam úteis em muitos casos (principalmente no desenvolvimento de vários frameworks), a sua geração produz um excesso de informação que “incha” o executável. Assim, quanto mais informações produzidas pela RTTI, maior o tamanho do executável. Atente-se para o fato de que as informações serão geradas (conforme a determinação das diretivas, é claro) mesmo se as informações não forem utilizadas, por isso, o recomendado é desabilitar essas informações se você não fizer uso. Mas fique atento porque, mesmo que você não faça uso, caso algum framework que você utilize o faça, ele passará a não funcionar caso a geração dessas informações sejam desabilitadas.

Outras funcionalidades

A API da RTTI é muito extensa, e não caberia demonstrar aqui todas suas possibilidades. Os pontos aqui demonstrados são uma boa base para conhecer a extensão das funcionalidades da RTTI. Assim você pode explorá-la mais profundamente sempre que necessário para obter as informações necessária para o seu projeto.

Conclusão

Como você observou nos exemplos acima, A RTTI foi muito bem estruturada para obter as informações necessárias das estruturas do código fonte. Tudo segue a mesma padronização, o que torna a curva de aprendizado muito rápido. Através dessa funcionalidade, você pode produzir software mais versáteis e menos acoplados (mais independentes de estruturas fixas), melhorando significativamente sua capacidade de desenvolvimento.

Um pouco sobre o TValue

Desde os primórdios do Delphi, existe um tipo de dados conhecido como “Variant” que é utilizado para armazenar uma grande variedade de tipos (char, string, integer, date, ponteiro, etc, etc, etc) e foi sempre utilizado para armazenar esses valores em situações onde o tipo do conteúdo poderia variar.

Com o Delphi 2010, e as novas alterações da RTTI, foi introduzido um novo tipo de dados, o TValue, que trabalha semelhante ao tipo variant, embora tenha algumas características diferentes. Uma delas é que, diferente do tipo variant, não é possível alterar o tipo de dados do TValue depois que ele foi informado pela primeira vez, ou seja, se ele foi definido como inteiro, será inteiro até o final. O objetivo do tipo não é sofrer alterações dos tipos dos valores, mas simplesmente receber um valor qualquer e armazenar esse valor até o momento de ser utilizado novamente.

Segue a declaração do TValue (Documentação oficial: http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Rtti.TValue):

Veja que ele  possui uma forma de checar os valores como difetentes tipos de dados, inclusive como interfaces.

Embora ele não possa se tornar um outro tipo de dado depois de definido na primeira utilização, ele é capaz de realizar casts com o seu conteúdo:

É isso. Lembre-se que ele veio junto com as novas funcionalidades da RTTI, por isso, pode ser muito mais explorado ainda. Experimente!