O operador stackalloc
permite que aloquemos arrays na stack ao invés da heap. Dessa forma, esses arrays são descartados automaticamente quando a execução de um método se encerra (no momento do retorno), não gerando pressão para o garbage collector , podendo melhorar a performance de forma muito siginificativa.
Veja também
- SÉRIE: Fundamentos para performance (em .NET)
- SÉRIE: Como o GC afeta a performance em .NET
- Entendendo a “Stack” em sua forma mais primitiva (em Assembly)
- Como variáveis locais são suportadas em Assembly
Evolução do operador stackalloc ao longo dos anos
No passado, a utilização desse operador implicava que o trecho de código onde ele aparecia fosse marcado como unsafe – o que restringia bastante seu uso.
public static unsafe string StackAllocReverseString(string input) { var reversedCharArray = stackalloc char[input.Length]; for ( int i = input.Length - 1, destIndex = 0; i >= 0; i--, destIndex++ ) { reversedCharArray[destIndex] = input[i]; } return new string(reversedCharArray); }
Entretanto, a partir do C# 7.2, com o advento dos tipos System.Span<T> e ReadOnlySpan<T> isso não é mais necessário. No C# 8, o operador pode ser utilizado, inclusive, em expressões aninhadas.
int length = 1000; Span<byte> buffer = length <= 1024 ? stackalloc byte[length] : new byte[length];
O trecho de código acima mostra o nível de sofisticação atual dessa feature alocando de forma condicional o array na stack, se ele tiver menos do que 1024 posições, ou na heap.
Como stackalloc pode melhorar a performance do código
No passado, demonstramos como implementações ingênuas, que ignoram o impacto do GC, podem ter prejuízos significativos de performance. No código que segue, que valida CPFs, por exemplo, há muitas alocações desnecessárias.
public static bool ValidarCPF(string sourceCPF) { if (String.IsNullOrWhiteSpace(sourceCPF)) return false; string clearCPF; clearCPF = sourceCPF.Trim(); clearCPF = clearCPF.Replace("-", ""); clearCPF = clearCPF.Replace(".", ""); if (clearCPF.Length != 11) { return false; } int[] cpfArray; int totalDigitoI = 0; int totalDigitoII = 0; int modI; int modII; if (clearCPF.Equals("00000000000") || clearCPF.Equals("11111111111") || clearCPF.Equals("22222222222") || clearCPF.Equals("33333333333") || clearCPF.Equals("44444444444") || clearCPF.Equals("55555555555") || clearCPF.Equals("66666666666") || clearCPF.Equals("77777777777") || clearCPF.Equals("88888888888") || clearCPF.Equals("99999999999")) { return false; } foreach (char c in clearCPF) { if (!char.IsNumber(c)) { return false; } } cpfArray = new int[11]; for (int i = 0; i < clearCPF.Length; i++) { cpfArray[i] = int.Parse(clearCPF[i].ToString()); } for (int posicao = 0; posicao < cpfArray.Length - 2; posicao++) { totalDigitoI += cpfArray[posicao] * (10 - posicao); totalDigitoII += cpfArray[posicao] * (11 - posicao); } modI = totalDigitoI % 11; if (modI < 2) { modI = 0; } else { modI = 11 - modI; } if (cpfArray[9] != modI) { return false; } totalDigitoII += modI * 2; modII = totalDigitoII % 11; if (modII < 2) { modII = 0; } else { modII = 11 - modII; } if (cpfArray[10] != modII) { return false; } // CPF Válido! return true; }
Essas [tweet]alocações desnecessárias, em .NET, fatalmente, pressionam o GC fazendo com que a performance fique deteriorada.[/tweet] Na época, mostramos uma revisão desse código que eliminava todas as alocações na heap.
Na ocasião demonstramos, inclusive, como [tweet]código aparentemente menos eficiente, que realiza muito mais processamento, muitas vezes é compensado pela utilização mais racional da memória – sendo menos ofensivo ao GC.[/tweet]
public struct Cpf { private readonly string _value; public readonly bool EhValido; private Cpf(string value) { _value = value; if (value == null) { EhValido = false; return; } var posicao = 0; var totalDigito1 = 0; var totalDigito2 = 0; var dv1 = 0; var dv2 = 0; bool digitosIdenticos = true; var ultimoDigito = -1; foreach (var c in value) { if (char.IsDigit(c)) { var digito = c - '0'; if (posicao != 0 && ultimoDigito != digito) { digitosIdenticos = false; } ultimoDigito = digito; if (posicao < 9) { totalDigito1 += digito * (10 - posicao); totalDigito2 += digito * (11 - posicao); } else if (posicao == 9) { dv1 = digito; } else if (posicao == 10) { dv2 = digito; } posicao++; } } if (posicao > 11) { EhValido = false; return; } if (digitosIdenticos) { EhValido = false; return; } var digito1 = totalDigito1 % 11; digito1 = digito1 < 2 ? 0 : 11 - digito1; if (dv1 != digito1) { EhValido = false; return; } totalDigito2 += digito1 * 2; var digito2 = totalDigito2 % 11; digito2 = digito2 < 2 ? 0 : 11 - digito2; EhValido = dv2 == digito2; } public static implicit operator Cpf(string value) => new Cpf(value); public override string ToString() => _value; }
O problema da implementação que recomendamos, de qualquer forma, é que ela é muito diferente daquela que faz alocações desncessárias. Além disso, sob diversas perspectivas, é também muito mais difícil de entender.
A utilização do operador stackalloc
abre a possibilidade para que escrevamos uma versão, também sem alocações, porém muito mais próxima da que tínhamos inicialmente.
public static bool ValidarCPF(string sourceCPF) { static bool VerificaTodosValoresSaoIguais(ref Span<int> input) { for (var i = 1; i < 11; i++) { if (input[i] != input[0]) { return false; } } return true; } if (string.IsNullOrWhiteSpace(sourceCPF)) return false; Span<int> cpfArray = stackalloc int[11]; var count = 0; foreach (var c in sourceCPF) { if (char.IsDigit(c)) { if (count > 10) { return false; } cpfArray[count] = c - '0'; count++; } } if (count != 11) return false; if (VerificaTodosValoresSaoIguais(ref cpfArray)) return false; var totalDigitoI = 0; var totalDigitoII = 0; int modI; int modII; for (var posicao = 0; posicao < cpfArray.Length - 2; posicao++) { totalDigitoI += cpfArray[posicao] * (10 - posicao); totalDigitoII += cpfArray[posicao] * (11 - posicao); } modI = totalDigitoI % 11; if (modI < 2) { modI = 0; } else { modI = 11 - modI; } if (cpfArray[9] != modI) { return false; } totalDigitoII += modI * 2; modII = totalDigitoII % 11; if (modII < 2) { modII = 0; } else { modII = 11 - modII; } return cpfArray[10] == modII; }
Aqui, também não ocorrem alocações e, consequentemente, não há pressão sobre o GC.
A nova versão, usando stackalloc
, além de mais simples, é mais performática do que aquela com otimizações que apresentamos anteriormente.
Em tempo, as heurísticas de acionamento do GC melhoraram visivelmente nos últimos tempos fazendo com que o tempo de execução da implementação ingênua caísse pela metade (comparação com os resultados obtidos no post onde a experiência foi feita originalmente).
Muito obrigado pelo post. Sempre agregando algo de valor.