Passagem por valor ou por referência - O que é mais rápido?
Passagem por valor ou por referência - O que é mais rápido?
?? Apenas como uma isenção de responsabilidade: esteja ciente de que as diferenças de desempenho mostradas são super pequenas. Dito isso, não execute seu código e altere tudo para uma estrutura. A Microsoft recomenda classes por padrão por um motivo. Se você tiver um atalho supercrítico, isso pode ser justificado.
Quando estamos passando objetos, podemos fazer isso por referência ou por valor. Qual desses dois métodos é mais rápido?
Para responder a essa pergunta, precisamos mergulhar um pouco no que acontece exatamente quando você passa algo e como o outro lado receberá isso.
Passando por valor
Um tipo de valor como int é passado para um método por valor. Isso significa que uma cópia desse objeto é feita e passada para o chamador. Como? Acabamos de colocar a nova cópia no quadro de pilha que a função de chamada pode ver.
Melhor explicado com este exemplo simples
int a = 0;
Change(a); // We pass a copy of a to the function
Console.Write(a); // Prints still 0
void Change(int a) => a = 5; // The function changes the copy to 5
Passando por referência
Tecnicamente falando, o passe por referência não existe realmente. Tudo é passado por valor. Quando dizemos passar por referência, queremos dizer passar o valor da referência para o chamado. Se dermos uma olhada no exemplo anterior, encontraremos uma pequena mudança no resultado:
int a = 0;
Change(ref a); // We pass the reference of a to the function
Console.Write(a); // Prints 5
void Change(ref int a) => a = 5; // The function changes the "original a" to 5
Acho que essa parte é bem conhecida. Mas também não é toda a verdade. Para entender o que e por que algo é mais rápido, temos que ir um pouco mais fundo.
Desreferenciamento
Agora, apenas passar uma referência a uma função é apenas uma parte. Também temos que desreferenciar esse valor para poder lê-lo. Assim, podemos ver que há duas operações envolvidas quando se trata de passar por referência. Lembre-se que uma referência é basicamente um endereço de memória. Isso é tudo!
- Coloque a referência no quadro de pilha para que o receptor possa acessá-lo
- Desreferencie e leia o conteúdo real
A parte interessante é (2), porque a desreferenciação acontece toda vez que acessamos essa variável dentro do nosso código:
void DoSomething(ref MyStruct myStruct)
{
int x = myStruct.Prop1; // Dereferencing and reading 4 bytes
int y = myStruct.Prop2; // Dereferencing and reading 4 bytes
Tamanho de uma referência
Eu disse anteriormente que estamos copiando o valor do endereço de memória de um tipo de referência para o receptor. Em C/C++ (assim como em C#) essas referências a endereços de memória são chamadas de ponteiros. Os ponteiros têm (geralmente) 4 bytes de largura em um processo de 32 bits ou 8 bytes de largura em um processo de 64 bits. Isso não é coincidência. Isso significa que copiar este valor pode ser feito dentro de um único registro em sua CPU. Importante saber é que isso é super rápido e basicamente o melhor caso.
estrutura(s) e seu tamanho
Agora talvez você tenha uma ideia de onde tudo isso vai dar. Vimos anteriormente que o tamanho é um fator muito, muito importante. (Alerta de spoiler: também o layout desempenha um papel, mais sobre isso depois). Então, quando passamos uma estrutura pelo seu valor, o tamanho é a fábrica de chaves que determina se ela é mais rápida ou não. Lembre-se de que temos menos trabalho para passar por valor do que por referência. Só se tivermos que copiar muito mais coisas seremos mais lentos. Dê uma olhada nesta estrutura:
public struct Point2D
{
public int X { get; set; }
public int Y { get; set; }
}
Esta é uma estrutura que tem dois int's. Cada int tem 32 bits, respectivamente, 4 bytes de largura. Isso significa que a estrutura tem exatamente 64 bits de largura. Portanto, exatamente o mesmo tamanho do nosso ponteiro quando passamos referências. Mas, ao contrário de uma referência, depois de copiar, terminamos. O chamado não precisa desreferenciar nada.
Referência
Aqui está uma pequena referência para mostrar isso na natureza.
public class PassByBenchmark
{
private readonly PointClass pointClass = new();
private readonly PointStruct pointStruct = new();
[Benchmark(Baseline = true)]
public int GetSumViaClassReference()
{
var sum = 0;
for(var i = 0; i < 20_000; i++)
sum += GetSumClass(pointClass);
return sum;
}
[Benchmark]
public int GetSumViaStruct()
{
var sum = 0;
for(var i = 0; i < 20_000; i++)
sum += GetSumStruct(pointStruct);
return sum;
}
[Benchmark]
public int GetSumViaStructReference()
{
{
var sum = 0;
for(var i = 0; i < 20_000; i++)
sum += GetSumRefStruct(in pointStruct);
return sum;
}
}
private int GetSumClass(PointClass c) => c.X + c.Y;
private int GetSumStruct(PointStruct c) => c.X + c.Y;
private int GetSumRefStruct(in PointStruct c) => c.X + c.Y;
}
public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
}
public struct PointStruct
{
public int X { get; set; }
public int Y { get; set; }
}
Resultados:
| Método | Quer dizer | Erro | StdDev | Razão | RatioSD |
|------------------------|-----------:| --------:| --------:| -----:|--------:|
|GetSumViaClassReference | 12.94 us | 0.259 us | 0.318 us | 1.00 | 0.00 |
| GetSumViaStruct | 12.05 us | 0.229 us | 0.214 us | 0.92 | 0.03 |
|GetSumViaStructReference| 12.82 us | 0.244 us | 0.365 us | 0.99 | 0.04 |
Passar uma classe por referência ou uma estrutura por referência não faz diferença alguma. Isso é esperado, pois o mesmo mecanismo está em vigor. Mas podemos ver que nosso estrutura Point é o método mais rápido pelo único motivo de ser pequeno e copiar essa estrutura tem o mesmo impacto que copiar o ponteiro para nosso callee.
Estrutura de gordura
gora, isso parecerá muito diferente quando nossa estrutura for um pouco mais ampla. Vamos ter a mesma configuração, mas "refatoramos" nossa estrutura e classe da seguinte forma:
public class PointClass
{
public int A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z;
}
public struct PointStruct
{
public int A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z;
}
Nossa estrutura agora tem 4 bytes * 26 letras = 104 bytes de largura. Nossa referência ainda é de apenas 8 bytes (em um processo de 64 bits).
| Método | Quer dizer | Erro | StdDev | Razão | RatioSD |
|--------------------------|-----------:| --------:| --------:| -----:|--------:|
| GetSumViaClassReference | 13.17 us | 0.140 us | 0.131 us | 1.00 | 0.00 |
| GetSumViaStruct | 109.50 us | 2.161 us | 4.058 us | 8.28 | 0.40 |
| GetSumViaStructReference | 13.22 us | 0.253 us | 0.260 us | 1.00 | 0.02 |
Podemos ver que o tempo de execução de nossas referências ainda é o mesmo esperado. Mas nossa estrutura é 8x mais lenta. A única coisa que mudou foi a cópia maior que temos que fazer.
Layout da estrutura
Uma última parte que pode ser interessante é o layout de uma estrutura. Já estamos na toca do coelho, então por que não ir um pouco mais fundo ??. Dê uma olhada nessas duas estruturas:
public struct Struct1
{
public int Number1;
public bool HasNumber1;
public int Number2;
public bool HasNumber2;
}
public struct Struct2
{
public int Number1;
public int Number2;
public bool HasNumber1;
public bool HasNumber2;
}
Eles são os mesmos? Sim e não. Claro que eles têm as mesmas propriedades. Mas o layout é bem diferente:
Tirado daqui
Apesar de termos a mesma quantidade de propriedades, o layout da estrutura é diferente. O que acontece aqui é que uma estrutura será preenchido em blocos de 4 bytes (esse valor depende da sua plataforma). Isso significa que se começarmos com um bool, teremos apenas 3 bytes restantes, portanto, um int não pode mais caber e 3 bytes devem ser preenchidos para preencher o bloco. Isso pode ter implicações de desempenho, mas mais importante que pode levar a grandes problemas se você tiver interoperabilidade com outras linguagens (nativas) como C/C++. Neste artigo, não vou me aprofundar nesse assunto, pois pode ficar bastante complicado. Talvez algo para o futuro ??
Conclusão
Espero que você tenha uma melhor compreensão do que acontece quando você passa algo por referência e por valor. E por que estruturas podem ser mais rápidos que classes ou estruturas de referência.