10000 chamadas Update()

Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.
Para um desenvolvedor experiente, esse código é um pouco estranho.
1. Não está claro como exatamente esse método é chamado.
2. Não está claro em que ordem esses métodos são chamados se você tiver vários objetos em uma cena.
3. Esse estilo de código não funciona com o intellisense.
Não, o Unity não usa o System.Reflection para encontrar um método mágico toda vez que precisa chamá-lo.
Em vez disso, na primeira vez que um MonoBehaviour de um determinado tipo é acessado, o script subjacente é inspecionado por meio do tempo de execução do script (Mono ou IL2CPP) para verificar se ele tem algum método mágico definido, e essas informações são armazenadas em cache. Se um MonoBehaviour tiver um método específico, ele será adicionado a uma lista adequada; por exemplo, se um script tiver o método Update definido, ele será adicionado a uma lista de scripts que precisam ser atualizados a cada quadro.
Durante o jogo, o Unity apenas itera através dessas listas e executa métodos a partir delas - simples assim. Além disso, é por isso que não importa se o método Update é público ou privado.
A ordem é especificada por Script Execution Order Settings (menu: Editar > Configurações do projeto > Ordem de execução do script). Pode não ser a melhor maneira de definir manualmente a ordem de 1000 scripts, mas se você quiser que um script seja executado depois de todos os outros, essa maneira é aceitável. É claro que, no futuro, queremos ter uma maneira mais conveniente de especificar a ordem de execução, usando um atributo no código, por exemplo.
Todos nós usamos um IDE de algum tipo para editar nossos scripts C# no Unity, e a maioria deles não gosta de métodos mágicos para os quais não é possível descobrir onde são chamados, se é que são chamados. Isso gera avisos e dificulta a navegação no código.
Às vezes, os desenvolvedores adicionam uma classe abstrata que estende o MonoBehaviour, chamam-na de BaseMonoBehaviour ou algo semelhante e fazem com que todos os scripts do projeto estendam essa classe. Eles colocaram algumas funcionalidades básicas úteis nele, juntamente com vários métodos mágicos virtuais, como este:
Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.
Essa estrutura torna o uso de MonoBehaviours em seu código mais lógico, mas tem uma pequena falha. Aposto que você já descobriu...
Todos os seus MonoBehaviours estarão em todas as listas de atualização que o Unity usa internamente, e todos esses métodos serão chamados a cada quadro para todos os seus scripts, na maioria das vezes sem fazer nada!
Alguém pode se perguntar: por que alguém deveria se preocupar com um método vazio? O fato é que essas são as chamadas da terra do C++ nativo para a terra do C# gerenciado, e elas têm um custo. Vamos ver qual é esse custo.
Para esta postagem, criei um pequeno projeto de exemplo que está disponível no Github. Ele tem 2 cenas que podem ser alteradas tocando em um dispositivo ou pressionando qualquer tecla no editor:
(1) Na primeira cena, 10000 MonoBehaviours são criados com esse código:
Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.
(2) Na segunda cena, outros 10.000 MonoBehaviours são criados, mas, em vez de terem um Update, eles têm um método UpdateMe personalizado que é chamado por um script de gerenciador a cada quadro, da seguinte forma:
Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.
O projeto de teste foi executado em 2 dispositivos iOS compilados para Mono e IL2CPP no modo não-desenvolvimento na configuração de versão. O tempo foi medido da seguinte forma:
Configure um cronômetro na primeira atualização chamada (configurada em Script Execution Order),
Pare o cronômetro em LateUpdate,
Faça a média dos tempos em alguns minutos.
Versão Unity: 5.2.2f1
Versão iOS: 9.0

UAU! Isso é muito! Deve haver algo errado com o teste!
Na verdade, esqueci de definir a Otimização de chamada de script como Rápida, mas sem exceções, mas agora podemos ver o impacto que essa configuração específica tem sobre o desempenho... não que alguém se importe mais com o IL2CPP.

OK, assim está melhor. Vamos mudar para IL2CPP.

Aqui vemos duas coisas:
1. Essa otimização específica ainda faz sentido no IL2CPP.
2. O IL2CPP ainda pode ser aprimorado e, enquanto escrevo esta postagem, as equipes do Scripting e do IL2CPP estão trabalhando arduamente para aumentar o desempenho. Por exemplo, a última ramificação de Scripting contém otimizações que tornam a execução do teste 35% mais rápida.
Explicarei o que o Unity está fazendo nos bastidores em alguns instantes. Mas agora vamos alterar o código do nosso gerenciador para torná-lo 5 vezes mais rápido!
Se você ainda não leu esta excelente série de publicações sobre os componentes internos do IL2CPP, deve fazê-lo logo após terminar de ler esta!
Acontece que, se você quisesse iterar uma lista de 10.000 elementos a cada quadro, seria melhor usar uma matriz em vez de uma lista porque, nesse caso, o código C++ gerado é mais simples e o acesso à matriz é mais rápido.
No próximo teste, alterei List<ManagedUpdateBehavior> para ManagedUpdateBehavior[].

Isso parece muito melhor!
Atualizar: Executei o teste com o array no Mono e obtive 0,23 ms.
Descobrimos que chamar funções de C++ para C# não é rápido, mas vamos descobrir o que o Unity está realmente fazendo ao chamar Updates em todos esses objetos. A maneira mais fácil de fazer isso é usar o Time Profiler da Apple Instruments.
Observe que não se trata de uma disputa entre mono e mono. Teste IL2CPP - a maioria das coisas descritas a seguir também é verdadeira para uma compilação Mono iOS.
Iniciei o teste no iPhone 6 com o Time Profiler, gravei alguns minutos de dados e selecionei um intervalo de um minuto para inspecionar. Estamos interessados em tudo que começa a partir dessa linha:
Tipo de bloco desconhecido "codeBlock", especifique um serializador para ele na propriedade `serializers.types`.
Se você nunca usou o Instruments antes, à direita você verá as funções classificadas por tempo de execução e outras funções que elas chamam. A coluna mais à esquerda é o tempo de CPU em ms e a porcentagem dessas funções e das funções que elas chamam combinadas; a segunda coluna à esquerda é o tempo de execução automática da função. Observe que, como a CPU não foi totalmente usada pelo Unity durante esse experimento, vemos 10 segundos de tempo de CPU gastos em nossos Updates em um intervalo de 60 segundos. Obviamente, estamos interessados nas funções que levam mais tempo para serem executadas.
Usei minhas habilidades loucas no Photoshop e codifiquei algumas áreas por cores para que você entenda melhor o que está acontecendo.

No meio, você vê nosso método Update ou como o IL2CPP o chama - UpdateBehavior_Update_m18. Mas antes de chegar lá, o Unity faz muitas outras coisas.
O Unity examina todos os Behaviours para atualizá-los. A classe especial de iterador, SafeIterator, garante que nada será interrompido se alguém decidir excluir o próximo item da lista. Apenas a iteração de todos os comportamentos registrados leva 1517ms de um total de 9979ms.
Em seguida, o Unity faz várias verificações para se certificar de que está chamando um método válido existente em um GameObject ativo que foi inicializado e seu método Start foi chamado. Você não quer que seu jogo trave se você destruir um GameObject durante o Update, quer? Essas verificações levam mais 2188ms de um total de 9979ms.
O Unity cria uma instância de ScriptingInvocationNoArgs (que representa uma chamada do lado nativo para o lado gerenciado) juntamente com ScriptingArguments e solicita que a máquina virtual IL2CPP invoque o método (função scripting_method_invoke). Essa etapa leva 2061ms de um total de 9979ms.
A função scripting_method_invoke verifica se os argumentos passados são válidos (900 ms) e, em seguida, chama o método Runtime::Invoke da máquina virtual IL2CPP (1520 ms). Primeiro, o Runtime::Invoke verifica se esse método existe (1018 ms). Em seguida, ele chama uma função RuntimeInvoker gerada para a assinatura do método (283 ms). Por sua vez, ele chama nossa função Update que, de acordo com o Time Profiler, leva 42 ms para ser executada.
E uma bela mesa colorida.

Agora vamos usar o Time Profiler com o teste do gerenciador. Você pode ver na captura de tela que existem os mesmos métodos (alguns deles levam menos de 1 ms no total, portanto, nem são mostrados), mas a maior parte do tempo de execução vai para a função UpdateMe (ou como o IL2CPP a chama - ManagedUpdateBehavior_UpdateMe_m14). Além disso, há uma verificação de nulidade inserida pelo IL2CPP para garantir que a matriz sobre a qual estamos iterando não seja nula.
A próxima imagem usa as mesmas cores.

Então, o que você acha agora? Devemos nos preocupar com uma pequena chamada de método?
Para ser sincero, esse teste não é totalmente justo. O Unity faz um ótimo trabalho ao proteger você e seu jogo de comportamentos não intencionais e falhas: Esse GameObject está ativo? Ele não foi destruído durante esse ciclo de atualização? O método Update existe no objeto? O que fazer com um MonoBehaviour criado durante esse loop de atualização? - Meu script de gerenciador não lida com nada disso, ele apenas itera por uma lista de objetos a serem atualizados.
No mundo real, o script do gerenciador provavelmente teria sido mais complicado e de execução mais lenta. Mas, nesse caso, eu sou o desenvolvedor - sei o que meu código deve fazer e arquiteto minha classe de gerente sabendo qual comportamento é possível e qual não é no meu jogo. Infelizmente, o Unity não possui esse conhecimento.
É claro que tudo depende do seu projeto, mas no campo não é raro ver um jogo usando um grande número de GameObjects na cena, cada um executando alguma lógica a cada quadro. Normalmente, é um pequeno trecho de código que não parece afetar nada, mas quando o número aumenta muito, a sobrecarga de chamar milhares de métodos de atualização começa a ser perceptível. Nesse ponto, talvez já seja tarde demais para alterar a arquitetura do jogo e refatorar todos esses objetos no padrão de gerenciador.
Você tem os dados agora, pense nisso no início de seu próximo projeto.
