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


Comments (0)