Otimizações do IL2CPP: Desvirtualização

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jul 26, 2016|5 Min
Otimizações do IL2CPP: Desvirtualização
Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.

A equipe da máquina virtual de scripts da Unity está sempre procurando maneiras de acelerar a execução do seu código. Esta é a primeira postagem de uma minissérie de três partes sobre algumas micro-otimizações realizadas pelo compilador IL2CPP AOT e como o senhor pode tirar proveito delas. Embora nada aqui faça com que o código seja executado duas ou três vezes mais rápido, essas pequenas otimizações podem ajudar em partes importantes de um jogo, e esperamos que elas forneçam aos senhores algumas informações sobre como o código está sendo executado.

Desvirtualização

Não há outra maneira de dizer isso: as chamadas de métodos virtuais são sempre mais caras do que as chamadas de métodos diretos. Estamos trabalhando em algumas melhorias de desempenho na biblioteca de tempo de execução libil2cpp para reduzir a sobrecarga das chamadas de métodos virtuais (falaremos mais sobre isso na próxima publicação), mas elas ainda exigem algum tipo de pesquisa em tempo de execução. O compilador não pode saber qual método será chamado em tempo de execução - ou pode?

A desvirtualização é uma tática comum de otimização do compilador que transforma uma chamada de método virtual em uma chamada de método direto. Um compilador pode aplicar essa tática quando puder provar exatamente qual método real será chamado no momento da compilação. Infelizmente, muitas vezes pode ser difícil provar esse fato, pois o compilador nem sempre vê toda a base de código. Mas quando isso é possível, pode tornar as chamadas de métodos virtuais muito mais rápidas.

O exemplo canônico

Quando era um jovem desenvolvedor, aprendi sobre métodos virtuais com um exemplo de animal bastante artificial. Esse código também pode ser familiar para o senhor:

public abstract class Animal {
  public abstract string Speak();
}

public class Cow : Animal {
   public override string Speak() {
       return "Moo";
   }
}

public class Pig : Animal {
    public override string Speak() {
        return "Oink";
   }
}

Então, no Unity (versão 5.3.5), podemos usar essas classes para criar uma pequena fazenda:

public class Farm: MonoBehaviour {
   void Start () {
       Animal[] animals = new Animal[] {new Cow(), new Pig()};
       foreach (var animal in animals)
           Debug.LogFormat("Some animal says '{0}'", animal.Speak());

       var cow = new Cow();
       Debug.LogFormat("The cow says '{0}'", cow.Speak());
   }
}

Aqui, cada chamada para o Speak é uma chamada de método virtual. Vamos ver se conseguimos convencer o IL2CPP a desvirtualizar qualquer uma dessas chamadas de método para melhorar seu desempenho.

O código C++ gerado não é tão ruim

Um dos recursos do IL2CPP de que gosto é que ele gera código C++ em vez de código assembly. É claro que esse código não se parece com o código C++ que o senhor escreveria à mão, mas é muito mais fácil de entender do que o código assembly. Vejamos o código gerado para o corpo desse loop for each:

// Set up a local variable to point to the animal array
AnimalU5BU5D_t2837741914* L_5 = V_2;
int32_t L_6 = V_3;
int32_t L_7 = L_6;

// Get the current animal from the array
V_1 = ((L_5)->GetAt(static_cast<il2cpp_array_size_t>(L_7)));
Animal_t3277885659 * L_9 = V_1;

// Call the Speak method
String_t* L_10 = VirtFuncInvoker0< String_t* >::Invoke(4 /* System.String AssemblyCSharp.Animal::Speak() */, L_9);

Removi um pouco do código gerado para simplificar as coisas. Está vendo aquela chamada feia para Invoke? Ele procurará o método virtual adequado na vtable e o chamará. Essa pesquisa na vtable será mais lenta do que uma chamada de função direta, mas isso é compreensível. O animal pode ser uma vaca ou um porco, ou algum outro tipo derivado.

Vejamos o código gerado para a segunda chamada a Debug.LogFormat, que é mais parecida com uma chamada de método direta:

// Create a new cow
Cow_t1312235562 * L_14 = (Cow_t1312235562 *)il2cpp_codegen_object_new(Cow_t1312235562_il2cpp_TypeInfo_var);
Cow__ctor_m2285919473(L_14, /*hidden argument*/NULL);
V_4 = L_14;
Cow_t1312235562 * L_16 = V_4;

// Call the Speak method
String_t* L_17 = VirtFuncInvoker0< String_t* >::Invoke(4 /* System.String AssemblyCSharp.Cow::Speak() */, L_16);

Mesmo nesse caso, ainda estamos fazendo a chamada do método virtual! O IL2CPP é bastante conservador com as otimizações, preferindo garantir a correção na maioria dos casos. Como ele não faz uma análise suficiente do programa inteiro para ter certeza de que essa pode ser uma chamada direta, ele opta pela chamada de método virtual mais segura (e mais lenta).

Suponha que saibamos que não há outros tipos de vacas em nossa fazenda, portanto, nenhum tipo derivará de Cow. Se tornarmos esse conhecimento explícito para o compilador, poderemos obter um resultado melhor. Vamos alterar a classe para que seja definida assim:

public sealed class Cow : Animal {
   public override string Speak() {
       return "Moo";
   }
}

A palavra-chave sealed informa ao compilador que ninguém pode derivar de Cow (sealed também pode ser usado diretamente no método Speak). Agora, o IL2CPP terá a confiança necessária para fazer uma chamada de método direta:

// Create a new cow
Cow_t1312235562 * L_14 = (Cow_t1312235562 *)il2cpp_codegen_object_new(Cow_t1312235562_il2cpp_TypeInfo_var);
Cow__ctor_m2285919473(L_14, /*hidden argument*/NULL);
V_4 = L_14;
Cow_t1312235562 * L_16 = V_4;

// Look ma, no virtual call!
String_t* L_17 = Cow_Speak_m1607867742(L_16, /*hidden argument*/NULL);

A chamada para o Speak aqui não será desnecessariamente lenta, pois fomos muito explícitos com o compilador e permitimos que ele otimizasse com confiança.

Esse tipo de otimização não tornará seu jogo incrivelmente mais rápido, mas é uma boa prática expressar quaisquer suposições que o senhor tenha sobre o código no código, tanto para futuros leitores humanos desse código quanto para compiladores. Se o senhor estiver compilando com o IL2CPP, recomendo que examine o código C++ gerado em seu projeto e veja o que mais pode encontrar!

Na próxima vez, discutiremos por que as chamadas de métodos virtuais são caras e o que estamos fazendo para torná-las mais rápidas.