Ref Local e Ref Return

Elemar Júnior

A Microsoft vem se empenhando em facilitar o desenvolvimento de código com alta-performance usando C#. Uma evidência disso é a inclusão de “value semantics” (o que é comum em C++) na linguagem.

Sem querer complicar demais as coisas, todos sabemos que, em C#, valores que ficam na stack, por padrão, são “copiados” toda vez que são atribuídos a uma nova variável, retornados por uma função ou quando passados por parâmetro. Obviamente, “copiar” tem um custo para a performance que poderia (e deveria, sempre que possível) ser evitado. No passado, isso não era possível. Agora é bem fácil.

Nesse post, examinaremos alguns cenários onde podemos aplicar alguns conceitos de value semantics em C# e como estes são suportados pelo runtime.

Referenciando valores em Stack localmente (Ref Local)

Como indicado no exemplo abaixo, agora podemos referenciar valores na stack explicitamente, sem fazer cópias.

using System;

class Program
{
    static void Main(string[] args)
    {
        var x = 42;
        ref var y = ref x;
        y++;

        Console.WriteLine(x);

    }
}

No exemplo, a variável X e a variável Y apontam para um mesmo valor na stack. Assim, quando o valor em Y é alterado, o valor em X, que é o mesmo, também é. O curioso, entretanto, é que o suporte para executar esse código é novidade apenas para C# e não para .NET.

.method private hidebysig static 
	void Main (
		string[] args
	) cil managed 
{
	// Method begins at RVA 0x2050
	// Code size 17 (0x11)
	.maxstack 3
	.entrypoint
	.locals init (
		[0] int32 x
	)

	IL_0000: ldc.i4.s 42
	IL_0002: stloc.0
	IL_0003: ldloca.s 0
	IL_0005: dup
	IL_0006: ldind.i4
	IL_0007: ldc.i4.1
	IL_0008: add
	IL_0009: stind.i4
	IL_000a: ldloc.0
	IL_000b: call void [System.Console]System.Console::WriteLine(int32)
	IL_0010: ret
} // end of method Program::Main

As instruções ldloca.s (que recupera o endereço de memória de uma variável local armazenda na stack), ldind.i4 (que carrega um inteiro, indiretamente [por sua referência]) e stind.i4 (que armazena um inteiro, indiretamente [por sua referência]) são suportados desde a versão 1.1 do .NET.

Esta capacidade, aparentemente ingênua, é bem poderosa e permite que façamos coisas incríveis.

Passando valores por referência (Ref em parâmetros)

Quando escrevemos uma função para receber um parâmetro por referência (recurso existente há muito tempo na linguagem), as mesmas instruções que vimos acima, no ref local, são empregadas.

using System;

class Program
{
    static void Main(string[] args)
    {

        var x = 42;
        Console.WriteLine(x); // 42

        Increment(x);
        Console.WriteLine(x); // 42

        IncrementRef(ref x);
        Console.WriteLine(x); // 43
    }

    static void Increment(int value)
    {
        value++;
    }

    static void IncrementRef(ref int value)
    {
        value++;
    }
}

No exemplo, uma das funções está preparada para receber uma referência para um valor em stack e a outra não. Quando a função que recebe o valor por referência o modifica, altera o valor da variável no contexto onde a função é chamada.

As instruções em IL que suportam isso são, novamente, ldloca.s (que recupera o endereço de memória de uma variável local armazenda na stack), ldind.i4 (que carrega um inteiro, indiretamente [por sua referência]) e stind.i4 (que armazena um inteiro, indiretamente [por sua referência]) são suportados desde a versão 1.1 do .NET.

.method private hidebysig static 
	void Main (
		string[] args
	) cil managed 
{
	// Method begins at RVA 0x2050
	// Code size 35 (0x23)
	.maxstack 1
	.entrypoint
	.locals init (
		[0] int32 x
	)

	IL_0000: ldc.i4.s 42
	IL_0002: stloc.0
	IL_0003: ldloc.0
	IL_0004: call void [System.Console]System.Console::WriteLine(int32)
	IL_0009: ldloc.0
	IL_000a: call void Program::Increment(int32)
	IL_000f: ldloc.0
	IL_0010: call void [System.Console]System.Console::WriteLine(int32)
	IL_0015: ldloca.s 0
	IL_0017: call void Program::IncrementRef(int32&)
	IL_001c: ldloc.0
	IL_001d: call void [System.Console]System.Console::WriteLine(int32)
	IL_0022: ret
} // end of method Program::Main

.method private hidebysig static 
	void Increment (
		int32 'value'
	) cil managed 
{
	// Method begins at RVA 0x207f
	// Code size 6 (0x6)
	.maxstack 8

	IL_0000: ldarg.0
	IL_0001: ldc.i4.1
	IL_0002: add
	IL_0003: starg.s 'value'
	IL_0005: ret
} // end of method Program::Increment

.method private hidebysig static 
	void IncrementRef (
		int32& 'value'
	) cil managed 
{
	// Method begins at RVA 0x2086
	// Code size 7 (0x7)
	.maxstack 8

	IL_0000: ldarg.0
	IL_0001: ldarg.0
	IL_0002: ldind.i4
	IL_0003: ldc.i4.1
	IL_0004: add
	IL_0005: stind.i4
	IL_0006: ret
} // end of method Program::IncrementRef

Retornando valores por referência (ref returns)

Começamos agora a tratar de cenários k de utilização de referências para valores na stack fazem a diferença.

Para entender ref return, vamos recorrer a um exemplo simples.

using System;

class Program
{
    static void Main()
    {
        var numbers = new[] {2, 8, 12, 7, 20 };

        MakeItEven(numbers, IndexOfFirstOdd(numbers));

        Console.WriteLine(string.Join(", ", numbers)); // 2, 8, 12, 8, 20
    }

    public static int IndexOfFirstOdd(int[] numbers)
    {
        for (var i = 0; i < numbers.Length; i++)
        {
            if (numbers[i] % 2 != 0)
            {
                return i;
            }
        }
        return -1;

    }

    public static void MakeItEven(int[] numbers, int indexOfFirstOdd)
    {
        if (indexOfFirstOdd == -1)
        {
            return;
        }

        numbers[indexOfFirstOdd]++;
    }
}

O código acima procura, em um array, por um número ímpar. Em seguida, uma função pega esse número, no array, e o torna par (incrementando em um).

C# não possuia formas de retornar uma referência para a posição do array forçando-nos a mover o “array” como parâmetro, inclusive para a função MakeItEven. 

Agora, podemos escrever esse mesmo código usando apenas referências para os valores apontados pelo array.

using System;

class Program
{
    static void Main()
    {
        var numbers = new[] {2, 8, 12, 7, 20 };

        MakeItEven(ref FirstOdd(numbers));

        Console.WriteLine(string.Join(", ", numbers)); // 2, 8, 12, 8, 20
    }

    public static ref int FirstOdd(int[] numbers)
    {
        for (var i = 0; i < numbers.Length; i++)
        {
            if (numbers[i] % 2 != 0)
            {
                return ref numbers[i];
            }
        }
        return ref numbers[0];

    }

    public static void MakeItEven(ref int oddNumber)
    {
        if (oddNumber % 2 != 0)
        {
            oddNumber++;
        }
    }
}

A função FirstOdd agora retorna uma referência para o primeir número ímpar (não mais o índice). Repare que, como trata-se um um value type não é possível retornar null quando não encontro um valor que atenda o critério.

Observando o código em IL, vimos que o valor retornado é, efetivamente, o endereço correspondente ao valor no array (instrução ldelema)

.method public hidebysig static 
	int32& FirstOdd (
		int32[] numbers
	) cil managed 
{
	// Method begins at RVA 0x208c
	// Code size 37 (0x25)
	.maxstack 2
	.locals init (
		[0] int32 i
	)

	IL_0000: ldc.i4.0
	IL_0001: stloc.0
	IL_0002: br.s IL_0017
	// loop start (head: IL_0017)
		IL_0004: ldarg.0
		IL_0005: ldloc.0
		IL_0006: ldelem.i4
		IL_0007: ldc.i4.2
		IL_0008: rem
		IL_0009: brfalse.s IL_0013

		IL_000b: ldarg.0
		IL_000c: ldloc.0
		IL_000d: ldelema [System.Runtime]System.Int32
		IL_0012: ret

		IL_0013: ldloc.0
		IL_0014: ldc.i4.1
		IL_0015: add
		IL_0016: stloc.0

		IL_0017: ldloc.0
		IL_0018: ldarg.0
		IL_0019: ldlen
		IL_001a: conv.i4
		IL_001b: blt.s IL_0004
	// end loop

	IL_001d: ldarg.0
	IL_001e: ldc.i4.0
	IL_001f: ldelema [System.Runtime]System.Int32
	IL_0024: ret
} // end of method Program::FirstOdd

Em termos práticos, essa capacidade permite, por exemplo, a utilização de structs (que são value types) em cenários mais avançados. O código que segue autoriza o valor atributo X da struct (o que não era possível até agora). Afinal, o valor da struct não está sendo copiado ao retornar a propriedade.

using System;

class Program
{
    static void Main()
    {
        var thing = new Thing();
        ref var position = ref thing.Position;
        position.X = 10;
        Console.WriteLine(thing.Position);
    }
}

public class Thing
{
    Point3 _position = new Point3 {X = 0, Y = 0, Z = 0};
    public ref Point3 Position => ref _position;
}

public struct Point3
{
    public int X, Y, Z;
    public override string ToString() => $"{X}, {Y}, {Z}";
}

Eu sei que structs  deveriam ser imutáveis e que este talvez não seja o melhor exemplo possivel. Mas, perceba que, antes, mesmo que desejássemos, não seria possível modificar o valor da struct com a posição porque estaríamos trabalhando um uma cópia.

Concluindo … por enquanto

A possibilidade de referênciar estruturas de dados na stack, mais baratas, abre espaço para escrita de códigos consideravelmente mais performáticos sem sacrificar a expressividade.

A utilização de structs fica autorizada para cenários mais complexos e torna seu entendimento ainda mais relevante.

Cada vez é menos necessário recorrer a linguagens como C++ para obter código, ao mesmo tempo, fácil de entender e de manter.

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
0 Comentários
Inline Feedbacks
View all comments

AUTOR

Elemar Júnior
Fundador e CEO da EximiaCo atua como tech trusted advisor ajudando empresas e profissionais a gerar mais resultados através da tecnologia.

NOVOS HORIZONTES PARA O SEU NEGÓCIO

Nosso time está preparado para superar junto com você grandes desafios tecnológicos.

Entre em contato e vamos juntos utilizar a tecnologia do jeito certo para gerar mais resultados.

Insights EximiaCo

Confira os conteúdos de negócios e tecnologia desenvolvidos pelos nossos consultores:

Arquitetura de Dados

Insights de um DBA na análise de um plano de execução

Especialista em performance de Bancos de Dados de larga escala
Arquitetura de Software

Estratégias para modernização do legado

Desenvolvedor .NET/NodeJs e especialista em Kafka com experiência em startups e grandes empresas
Infraestrutura e Nuvem

Migração para a nuvem, mais do que mudança tecnológica, implica em mudança da cultura organizacional

Engenheiro de nuvem, arquiteto de software e especialista em Containers e Devops

Acesse nossos canais

Simplificamos, potencializamos e aceleramos resultados usando a tecnologia do jeito certo

EximiaCo 2022 – Todos os direitos reservados

0
Queremos saber a sua opinião, deixe seu comentáriox
()
x

Ref Local e Ref Return

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Condição especial de pré-venda: R$ 14.000,00 - contratando a mentoria até até 31/01/2023 e R$ 15.000,00 - contratando a mentoria a partir de 01/02/2023, em até 12x com taxas.

Tenho interesse nessa capacitação

Para solicitar mais informações sobre essa capacitação para a sua empresa, preencha o formulário a seguir:

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse nessa solução

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse neste serviço

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

× Precisa de ajuda?