Feb 6, 2019

Evitando cópias de dados em C#

Read this post in English

Lire ce post en Francais

Você nota algum problema no trecho de código abaixo? Dica: existe pelo menos um, relacionado ao uso de Value Types & performance.

Ok, vamos tornar o problema um pouco mais óbvio introduzindo um erro, o qual, assumindo-se que o código em questão possui testes unitários, provavelmente faria com que o comportamento fosse notado imediatamente. Qual seria o resultado do programa agora ?

Se você respondeu:
M() : 0
M() : 1
M() : 2

Você provavelmente ficará surpreso ao ver:
M() : 0
M() : 0
M() : 0

Agora ficou evidente que algo está errado.

O problema com este programa é que o campo s foi declarado como readonly o que faz com que o compilador emita cópias defensivas antes da chamada de qualquer método do tipo (no nosso caso s.M()); isso significa que o método M() incrementará o campo counter em uma cópia da estrutura original (ao invés de s) o que explica o comportamento observado.

Note que na primeira versão do programa (em que a estrutura em questão não é mutável) não é fácil detectar que algo pode não estar correto. Mesmo na última versão do programa não é claro o porquê nunca observamos o incremento de s.counter.

Para entender o que está ocorrendo vamos olhar o código IL gerado para o método Main() (você pode usar o sharplab.io para experimentar com o código):
Agora ficou mais fácil observar as cópias extras que ocorrem nos offsets IL_0001 & IL_0006 e depois em IL_000f & IL_0014 e finalmente em IL_001D & IL_0022. Note que após copiar o conteúdo de s para a variável local localizada no slot 1 o endereço da mesma é carregado na pilha e a seguir o método M() é executado.

Infelizmente em C# é relativamente fácil cair em outras “armadilhas”; O exemplo abaixo apresenta mais dois casos: parâmetros in e variáveis local ref readonly:

A pergunta natural que surge é: é possível evitar estas cópias? Como? A resposta depende da versão da linguagem que você está usando:
  • < 7.3: A única maneira que conheço é remover o modificador readonly do campo (s no nosso exemplo)
  • >= 7.3: Neste caso você pode declarar sua struct como readonly de forma a garantir que a mesma não é mutável; desta forma o compilador não precisará emitir cópias defensivas (caso o código tente modificar o estado da estrutura o compilador emitirá um erro).
Resta uma questão: quão ruim estas cópias extras são? Na minha opinião a resposta depende de como as mesmas afetam seu programa; performance (i) e/ou comportamento (ii) e também dos seus requisitos quanto a performance.

No caso de comportamento incorreto acredito ser consenso que o desenvolvedor deve encontrar/corrigir estas cópias; já em cenários em que tais cópias afetam apenas a performance eu diria que o desenvolvedor deve analisar os prós/contras antes de investir tempo para encontrar/remover tais cópias (para um número de aplicativos o impacto em performance causado por estas cópias extras é desprezível). Dito isso, se você desenvolve usando o Visual Studio você pode instalar o ErrorProne.NET que irá facilitar em muito a localização de problemas deste tipo.

Finalmente, se você se interessa em entender a plataforma .Net mais a fundo recomendo acompanhar este blog

Have fun!

Adriano

No comments: