Programação Orientada a Dados

A Programação Orientada a Dados é uma abordagem poderosa de programação que aproveita ao máximo o desempenho.

Programação Orientada a Dados é uma abordagem poderosa de programação que resulta em grandes melhorias de desempenho. Ela foca em tratar dados como o elemento central, tudo o mais é organizado em torno dele, para acessar ou modificar esses dados. Essa abordagem também é muito amigável para multiplayer, pois torna os dados que precisam ser sincronizados entre jogadores mais fáceis e rápidos de acessar.

Programação Orientada a Dados encoraja você a pensar em tudo na sua cena como dados que precisam ser copiados e mutados através dos vários sistemas. O principal benefício dessa abordagem está em otimizar a velocidade com que os dados podem ser lidos da memória, o que frequentemente é o gargalo principal ao executar aplicações e jogos modernos.

Devido a essa notável melhoria de desempenho, grande parte da indústria de criação de jogos vem se movendo para adotar essa abordagem nos últimos anos.

O SDK da Decentraland executa cenas em JavaScript, e uma limitação dessa linguagem é que ela não oferece controle sobre alocação de memória. O engine que roda a Decentraland, porém, usa C#, que se beneficia muito ao seguir princípios orientados a dados. O SDK e o engine estão constantemente trocando mensagens entre si. Para tornar essa comunicação o mais eficiente possível, faz sentido manter as estruturas de dados em ambos os lados o mais semelhantes possível, para evitar ter que reorganizar esses dados constantemente.

Como é na prática

Programação Orientada a Dados é diferente de Programação Orientada a Objetos, uma abordagem com a qual muitos desenvolvedores estão familiarizados atualmente. Na Programação Orientada a Objetos, o código é estruturado seguindo abstrações que tentam replicar construções do mundo real: objetos. Cada um desses objetos pode conter tanto dados quanto funcionalidade. Aplicações que usam essa abordagem geralmente são fáceis de planejar conceitualmente, mas também mais ineficientes na execução.

Na Programação Orientada a Dados, os dados não são estruturados em torno de objetos, são estruturados para otimizar sua facilidade de acesso. As construções do mundo real que os dados representam não participam dos diferentes fluxos que mutam esses dados.

O modelo Entity Component System (ECS) sobre o qual o SDK da Decentraland é construído é muito compatível com a abordagem de Programação Orientada a Dados. Cada componente faz parte de uma coleção estruturada de dados. Components pertencem a uma entity por referência, mas os dados não são estruturados ao redor da entity; os dados são estruturados como uma coleção de components similares. Por exemplo, todos os Transform components em uma cena são iguais. Um desses transforms pode pertencer ao modelo do seu edifício principal, outro a um copo de vidro que é filho de uma mesa. Os systems na cena então processam a lista de Transform components um por um, sem fazer distinções. Todos os Transform components têm os mesmos campos e passam pelas mesmas verificações.



Imagine uma cena que tem uma dúzia de portas que podem estar abertas ou fechadas. Você pode representar o estado de todas essas portas como um simples componente "isOpen" que armazena um valor booleano. Se "isOpen" for true a porta deve estar aberta; se for false a porta deve estar fechada. Se um jogador clica em uma porta, ela deve alternar o estado, e outros jogadores também devem ver essa alternância. Enquanto sua cena está processando uma mudança no estado de uma porta e sincronizando-a com outros jogadores, a cena realmente não se importa com o que "isOpen" representa. Todo o conjunto de components é apenas uma coleção de booleanos que precisam ser sincronizados com outros jogadores. Um system separado na sua cena pode então cuidar de ajustar regularmente o estado de cada "isOpen" à rotação correspondente da porta.

Programação Orientada a Dados não é necessariamente mais difícil, mas é uma abordagem diferente que precisa ser aprendida e adotada. Desenvolvedores que não estão acostumados com essa abordagem podem precisar de algum tempo para se familiarizar; encorajamos que você explore e brinque com exemplos para adquirir melhor entendimento.

Por que funciona

Para entender por que a programação orientada a dados faz tanta diferença, precisamos dar uma olhada no hardware.

Quando o processador precisa buscar dados na memória, ele busca um bloco inteiro de dados no cache, incluindo dados que por acaso estão armazenados na memória ao lado do valor que queríamos. Quanto mais seu código puder fazer uso de dados que já estão no cache, mais rápido seu código irá rodar.

Imagine que a memória da máquina é literalmente um armazém, onde os dados são guardados em muitas caixas empilhadas. Sempre que precisamos de um determinado dado, precisamos chamar um empilhadeira para buscar a caixa onde esse dado está localizado e trazê-la até a mesa de atendimento para inspeção. Essa mesa só comporta algumas caixas por vez, então você não pode manter muitos dados por perto.

Leva muito tempo para a empilhadeira ir até o fundo do armazém e nos trazer uma caixa. Se os dados que você quer dessas caixas estiverem espalhados, isso significará pedir que a empilhadeira faça muitas viagens. Na maior parte do tempo, você ficará esperando ao lado da mesa de atendimento, aguardando a próxima caixa solicitada chegar.

Você pode evitar muito desse tempo desperdiçado se empilhar essas caixas de forma inteligente e organizar os dados para que as coisas que provavelmente você precisará ao mesmo tempo fiquem majoritariamente agrupadas. Se você agrupar os dados de forma esperta, frequentemente descobrirá que a próxima coisa que precisa já está em uma das caixas na mesa de atendimento. Você poderá começar a trabalhar nela sem incomodar o operador da empilhadeira.

Por exemplo, se você tem uma cena que precisa buscar o estado de uma porta para checar se ela está aberta ou fechada, o hardware não está apenas buscando o valor do booleano particular que descreve o estado daquela porta, ele está buscando uma porção grande de outros dados que podem ou não ser relevantes.

Suponha que sua cena tenha um system que precisa atualizar o estado de abertura/fechamento de uma dúzia de portas, uma vez a cada frame. Se seu código estiver organizado seguindo uma abordagem de Programação Orientada a Objetos, não há como prever como os diferentes pedaços relevantes de informação podem estar agrupados. Talvez uma "caixa" do nosso armazém contenha o estado "isOpen" da porta A e também contenha a textura da porta A e o áudio que ela reproduz ao abrir. Você pode ter que fazer uma viagem para buscar uma nova "caixa" de dados para cada uma das doze portas. E claro, todo esse processo precisa acontecer de novo a cada frame. Então são 360 (30 x 12) viagens metafóricas ao fundo do armazém por segundo, mesmo quando nenhuma das portas mudou de estado.

Se seu código segue uma abordagem Orientada a Dados, por outro lado, é muito provável que todos esses 12 booleanos estejam na mesma caixa. Isso ocorre porque todos esses booleanos fazem parte de um único array que foi gravado na memória de uma vez. Você nunca está organizando explicitamente como esses dados se encaixam na memória, então no pior cenário o array pode acabar dividido em duas caixas. Mas mesmo nesse pior cenário, 2 viagens é muito melhor do que 12.

Verificar o estado de cada porta a cada frame parece muito trabalho, mas é na verdade super rápido se todos os dados já estiverem no cache de memória. Se seus dados estiverem bem organizados, sua cena pode executar processos como esse sobre muitas entities e ainda assim rodar muito rápido.

Atualizado