[Update: Span<T>] - Evitando alocações de memória ao ler arquivos textos

12/03/2017 21:05:00 By Felipe Pessoto

Com o release de hoje do Visual Studio 15.5, fiz uma versão utilizando Span<T>, ainda não é possível ver suas vantagens pois muitas APIs ainda não aceitam o ReadOnlySpan<char> no lugar de strings, o que permitiria por exemplo chamar o int.TryParse diretamente (Update 2: Agora permitem, o codigo foi atualizado para refletir isso). Com isso o Span<T> traria o melhor de todas as soluções apresentadas, a simplicidade do Substring, com a performance dos ponteiros, sem utilizar o unsafe.

Ainda assim o Span<T> mostra que sua performance foi a melhor entre os métodos avaliados, sem alocar praticamente nenhuma memória:

[Benchmark]
private long Read_ResultSpan(bool stack)
{
    int bufferSize = GetLineSize();
    Span buffer = stack ? stackalloc char[bufferSize] : new char[bufferSize];
    long total = 0;

    TextReader sr = GetReader();
    {
        while (sr.Read(buffer) > 0)
        {
            int lastIndex = 0;

            for (int i = 0; i < positions.Length; i++)
            {
                ReadOnlySpan result = buffer.Slice(lastIndex, positions[i]);
                lastIndex += positions[i];
                total += int.Parse(result);
            }
        }
    }

    if (total != expected)
    {
        throw new Exception("Wrong result");
    }

    return total;
}

E o resultado final:

Method Mean Ratio Gen 0/1k Op Gen 1/1k Op Gen 2/1k Op Allocated Memory/Op
Minimum 1.522 ms 0.002 - - - 0 B
ReadLine_ResultSubstring 662.002 ms 1.000 78000.0000 - - 157 MB
ReadLine_ResultCharArray 649.625 ms 0.981 78000.0000 - - 157 MB
Read_ResultCharArray 443.691 ms 0.670 57000.0000 - - 114 MB
Read_ResultCharArraySpan 390.457 ms 0.590 - - - 440 B
Read_ResultUseUnsafeCharPointer 551.917 ms 0.834 - - - 1824 B
Read_ResultSpanStackAlloc 369.504 ms 0.558 - - - 0 B
Read_ResultSpanCharArray 397.005 ms 0.600 - - - 360 B
Read_ResultSpanNative 376.109 ms 0.568 - - - 0 B

 

 

O código está disponível em https://github.com/felipepessoto/AvoidingStringAllocation

O que há de novo no C# 7.0 - Pattern Matching

12/13/2016 23:40:00 By Felipe Pessoto

Pattern Matching é uma nova estrutura na linguagem que permite testar um valor contra um determinado formato enquanto atribui seu resultado.

Na versão 7.0 do C# são esperados três patterns:

  • Constant - Testa a expressão contra uma constante, isto é, literais, variável const, o valor de um enum ou typeof
  • Type - Testa se uma expressão é de um determinado tipo e em caso de sucesso faz o cast para o tipo, atribuindo o resultado em uma variável
  • Var - Esta expressão sempre é executada com sucesso, servindo apenas para atribuir o valor em uma nova variável

E mais estão por vir, no GitHub é possível ler o documento sobre Pattern Matching

Atualmente estes patterns podem ser utilizados nas expressões is e no case do bloco switch.

Is Expressions

 O principal uso nas expressões is será do Type Pattern, para atribuir uma variavel enquanto teste seu tipo. Por exemplo, quando é necessário executar um método de uma classe derivada, em vez de:

Dog dog = animal as Dog;
if(dog != null)
{
    dog.Bark();
}

//Ou

if (animal is Dog)
{
    Dog dog = (Dog)animal;
    dog.Bark();
}

É possível validar e atribuir em uma unica expressão:

if (animal is Dog dog)
{
    dog.Bark();
}

Como muitos dos recursos são apenas Syntactic Sugar, recomendo sempre avaliar qual a IL gerada, neste caso, é equivalente ao seguinte código C#:

Dog dog;
if ((dog = (animal as Dog)) != null)
{
    dog.Bark();
}

 Switch statements

 Com o switch fica ainda mais interessante ao combinar as condições, mantendo as regras separadas para cada caso e o código mais claro:

Animal animal = GetAnimal();

switch (animal)
{
    case Dog dog when dog.IsShortHaired:
        Console.WriteLine("Short Haired");
        break;
    case Dog dog:
        Console.WriteLine("Not Short Haired");
        break;
    case Cat cat:
        Console.WriteLine("Cat");
        break;
    case null:
        throw new ArgumentNullException();
    default:
        Console.WriteLine("Animal");
        break;
}

 O escopo de cada variavel atribuida no case é restrita ao bloco em que foi declarado e pode ser utilizado para validar outras regras, como no exemplo o IsShortHaired. Assim caso alguma validação não seja feita com sucesso, o bloco não é executado e o próximo case é avaliado.

Outra mudança importante é que agora o ordem dos case's é validada na compilação, impedindo que se use um case que inutilize os que estão abaixo dele, por exemplo:

E o case default será sempre executado por último, não importando o local em que foi declarado.

Evitando alocações de memória ao ler arquivos textos

12/01/2016 21:01:00 By Felipe Pessoto

Esta é uma tarefa comum, alguns processos de importação e exportação de dados usam colunas fixas, como alguns usados por bancos, por exemplos o CNAB.

O processo de leitura parece simples e realmente é. Só precisamos tomar alguns cuidados, como não ter ler todo o arquivo para a memória para então processar e guardar seu resultado, o que poderia causar um OutOfMemoryException.

De forma geral, bastaria algo como:

int[] positions = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

string[] result = new string[positions.Length];

using (StreamReader sr = new StreamReader("Data.txt"))
{
    string line;
    while ((line = sr.ReadLine()) != null)
    {
        int lastIndex = 0;

        for (int i = 0; i < positions.Length; i++)
        {
            result[i] = line.Substring(lastIndex, positions[i]);
            lastIndex += positions[i];
        }
    }
}

 

Porém desta forma são alocadas diversas string para cada linha. Cada chamada ao método Substring, cria uma nova string.

Assim, ao ler 1.000.000 de linhas, são alocados cerca de 1121MB levando 1208ms.

Ao realizar o profiling da aplicação é possível ver que a praticamente todo o tempo é utilizado nos métodos ReadLine e Substring. (Não foi possível aguardar o fim da execução, com o profiler ativo ela fica muito mais lenta)

 

Assim podemos tentar otimizar estes dois pontos, o primeiro deles, o uso de Substring.

Neste exemplo criei duas soluções, utilizando char[], no caso como são diversas colunas, char[][]. A segunda opção com ponteiros.

Ambas tem resultados semelhantes, cada uma com seus pontos fortes e fracos. Ao utilizar ponteiro você obrigatoriamente utiliza código marcado como unsafe. E ao utilizar char[] não é possível utilizar as classes do .NET para fazer o Parse para int por exemplo, apesar de ser possível criar seu próprio método, como o método privado do .NET: https://referencesource.microsoft.com/#mscorlib/system/number.cs,04291cc3a0b10032,references

Ao executar o teste sem SubString diminuimos para 412MB alocados e 915ms de execução:

public void ReadLine_ResultCharArray()
{
    char[][] result = new char[positions.Length][];

    //Allocate char array of right size
    for (int i = 0; i < positions.Length; i++)
    {
        result[i] = new char[positions[i]];
    }

    using (TextReader sr = GetReader())
    {
        string line;
        while ((line = sr.ReadLine()) != null)
        {
            int lastIndex = 0;

            for (int i = 0; i < positions.Length; i++)
            {
                int fieldLength = positions[i];

                for (int j = 0; j < fieldLength; j++)
                {
                    result[i][j] = line[lastIndex + j];
                }

                lastIndex += fieldLength;
            }
        }
    }
}

 

 Desta vez o profiling é executado quase que de forma instantânea:

 

 

Agora nosso foco é o método ReadLine.

A classe TextReader possui um método Read que permite que seja utilizado um buffer de char[], assim podemos usar este método para evitar alocações, criando um buffer do tamanho de uma linha e reutilizando até o final do processamento:

public void Read_ResultCharArray()
{
    int bufferSize = GetLineSize();
    char[] buffer = new char[bufferSize];
    char[][] result = new char[positions.Length][];

    //Allocate char array of right size
    for (int i = 0; i < positions.Length; i++)
    {
        result[i] = new char[positions[i]];
    }

    using (TextReader sr = GetReader())
    {
        while (sr.Read(buffer, 0, bufferSize) > 0)
        {
            int lastIndex = 0;

            for (int i = 0; i < positions.Length; i++)
            {
                int fieldLength = positions[i];

                for (int j = 0; j < fieldLength; j++)
                {
                    result[i][j] = buffer[lastIndex + j];
                }

                lastIndex += fieldLength;
            }
        }
    }
}

 

 Diminuindo a alocação de memória para valores irrisórios:

 

Desta forma porém nosso resultado é um array de char e não string, impedindo o uso de alguns métodos como int.Parse. Para continuarmos com o resultado em string, é possível utilizando ponteiros, como no exemplo:

public void Read_ResultUseUnsafeCharPointer()
{
    int bufferSize = GetLineSize();
    char[] buffer = new char[bufferSize];
    string[] result = new string[positions.Length];

    //Allocate empty strings of right size
    for (int i = 0; i < positions.Length; i++)
    {
        result[i] = new string(' ', positions[i]);
    }

    using (TextReader sr = GetReader())
    {
        while (sr.Read(buffer, 0, bufferSize) > 0)
        {
            int lastIndex = 0;

            for (int i = 0; i < positions.Length; i++)
            {
                int fieldLength = positions[i];

                unsafe
                {
                    fixed (char* chars = result[i])
                    {
                        for (int j = 0; j < fieldLength; j++)
                        {
                            chars[j] = buffer[lastIndex + j];
                        }
                    }
                }

                lastIndex += fieldLength;
            }
        }
    }
}

 

O uso de memória e tempo de processamento é semelhante ao usar char[][].

Abaixo um sumário com todos os testes realizados com 100 mil linhas:

 

Method Mean StdErr StdDev Median Scaled Scaled-StdDev Gen 0 Bytes Allocated/Op
GoStraightReadLine 68.5477 ms 0.4298 ms 0.7444 ms 68.7090 ms 0.49 0.00 2,268.00 101,400,456.33
GoStraightRead 34.3965 ms 0.3196 ms 0.5535 ms 34.4998 ms 0.25 0.00 - 19,869.67
ReadLine_ResultSubstring 140.3299 ms 0.0818 ms 0.1416 ms 140.3629 ms 1.00 0.00 6,440.00 284,255,469.00
ReadLine_ResultCharArray 101.1819 ms 0.6428 ms 1.1134 ms 100.8166 ms 0.72 0.01 2,275.00 101,735,538.92
ReadLine_ResultUseUnsafeCharPointer 106.5311 ms 0.2403 ms 0.4162 ms 106.3192 ms 0.76 0.00 2,275.00 101,738,164.83
Read_ResultNewString 109.9989 ms 0.0806 ms 0.1395 ms 110.0615 ms 0.78 0.00 4,165.00 182,531,333.17
Read_ResultUseUnsafeCharPointer 80.7485 ms 0.2625 ms 0.4546 ms 80.7316 ms 0.58 0.00 - 24,451.50
Read_ResultCharArray 77.3640 ms 1.0972 ms 1.9004 ms 76.4365 ms 0.55 0.01 - 24,806.92

 

As duas primeiras linhas representam a leitura do arquivo, sem nenhum processamento. A terceira é o primeiro exemplo e a última o último exemplo

O que há de novo no C# 7.0 - Literais binários

09/06/2016 23:11:00 By Felipe Pessoto

Além dos números decimais e hexadecimais, no C# 7.0 é possível escrever usando notação binária:

class BinaryLiteral
{
    public static void Example()
    {
        int myValue = 0b1010;
    }
}

Facilitando muito a criação de Flags Enum:

public enum MyFlagsEnumCSharp60
{
    None = 0,
    Sunday = 1,
    Monday = 2,
    Tuesday = 4,
    Wednesday = 8,
    Thursday = 16,
    Friday = 32,
    Saturday = 64
}

public enum MyFlagsEnum2CSharp60
{
    None = 0,
    Sunday = 1,             // 1
    Monday = 1 << 1,        // 2
    Tuesday = 1 << 2,       // 4
    Wednesday = 1 << 3,     // 8
    Thursday = 1 << 4,      // 16
    Friday = 1 << 5,        // 32
    Saturday = 1 << 6,      // 64
}

public enum MyFlagsEnumCSharp70
{
    None =      0b00000000,
    Sunday =    0b00000001,
    Monday =    0b00000010,
    Tuesday =   0b00000100,
    Wednesday = 0b00001000,
    Thursday =  0b00010000,
    Friday =    0b00100000,
    Saturday =  0b01000000
}

O que há de novo no C# 7.0 - Separador de dígitos

09/06/2016 23:04:00 By Felipe Pessoto

Uma pequena mas util nova funcionalidade é a possibilidade de utilizar separadores entre os dígitos, facilitando a leitura. Por exemplo:

class DigitSeparator
{
    public const int MyValue = 123_456;
    public const int MyHexValue = 0xAB_CD_EF;

    public static void Example()
    {
        Console.WriteLine(MyValue);
        Console.WriteLine(MyHexValue);
    }
}

Não há nenhuma diferença no código compilado com ou sem os separadores.