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
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 alocações desnecessárias, em .NET, fatalmente, pressionam o GC fazendo com que a performance fique deteriorada. Na época, mostramos uma revisão desse código que eliminava todas as alocações na heap.
Na ocasião demonstramos, inclusive, como 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.
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.