Parsing large files is a recurring and challenging task. Right? It is too easy to write slow code that consumes a lot of memory.
As an example, let’s consider the following CSV file sample (the size of the real one is ~500MB)

userId,movieId,rating,timestamp
1,2,3.5,1112486027
1,29,3.5,1112484676
1,32,3.5,1112484819
1,47,3.5,1112484727
1,50,3.5,1112484580
1,112,3.5,1094785740
1,151,4.0,1094785734
1,223,4.0,1112485573
1,253,4.0,1112484940

Let’s assume that you need to compute the average rating for the ‘Braveheart’ movie (movieId=110). How would you implement it? Probably you would start with something like this:

var lines = File.ReadAllLines(filePath);
var sum = 0d;
var count = 0;

foreach (var line in lines)
{
    var parts = line.Split(',');

    if (parts[1] == "110")
    {
        sum += double.Parse(parts[2], CultureInfo.InvariantCulture);
        count++;
    }
}

Console.WriteLine($"Average rate for Braveheart is {sum/count} ({count} votes).");

The previous code is easy to read (that is good), but it is slow (took more than 6 seconds to run on my machine) and it consumes a lot of RAM (more than 2GB allocated processing a 500MB file).

The problem is we are loading all the data to the memory, putting a lot of pressure on the garbage collector. There is no need for doing that.

var sum = 0d;
var count = 0;
string line;

using (var fs = File.OpenRead(filePath))
using (var reader = new StreamReader(fs))
while ((line = reader.ReadLine()) != null)
{
    var parts = line.Split(',');

    if (parts[1] == "110")
    {
        sum += double.Parse(parts[2], CultureInfo.InvariantCulture);
        count++;
    }
}

Console.WriteLine($"Average rate for Braveheart is {sum / count} ({count} votes).");

This time we are loading data as we need and discarding it. This code is ~30% faster than the previous one, demands less memory (no more than 13MB for processing a 500MB file) and puts less pressure on the Garbage Collector (no more big objects nor objects that survive to the gen#0 collections).

Let’s try something different.

var sum = 0d;
var count = 0;
string line;

// Braveheart id movie id as span;
var lookingFor = "110".AsSpan();

using (var fs = File.OpenRead(filePath))
using (var reader = new StreamReader(fs))
while ((line = reader.ReadLine()) != null)
{
    // ignoring the voter id
    var span = line.AsSpan(line.IndexOf(',') + 1);

    // movieId
    var firstCommaPos = span.IndexOf(',');
    var movieId = span.Slice(0, firstCommaPos);
    if (!movieId.SequenceEqual(lookingFor)) continue;

    // rating
    span = span.Slice(firstCommaPos + 1);
    firstCommaPos = span.IndexOf(',');
    var rating = double.Parse(span.Slice(0, firstCommaPos), provider: CultureInfo.InvariantCulture);

    sum += rating;
    count++;
}

The primary goal of the previous code was to allocate fewer objects, to reduce the pressure on the garbage collector, getting better performance. Success! This code is 4x faster than the original one, consumes only 6MB and demands ~50% less garbage collector activations (Congrats, Microsoft!).

We are still allocating a string object for each line in the line. Let’s change it.

var sum = 0d;
var count = 0;

var lookingFor = Encoding.UTF8.GetBytes("110").AsSpan();
var rawBuffer =  new byte[1024*1024];
using (var fs = File.OpenRead(filePath))
{
    var bytesBuffered = 0;
    var bytesConsumed = 0;

    while (true)
    {
        var bytesRead = fs.Read(rawBuffer, bytesBuffered, rawBuffer.Length - bytesBuffered);

        if (bytesRead == 0) break;
        bytesBuffered += bytesRead;

        int linePosition;

        do
        {
            linePosition = Array.IndexOf(rawBuffer, (byte) 'n', bytesConsumed,
                bytesBuffered - bytesConsumed);

            if (linePosition >= 0)
            {
                var lineLength = linePosition - bytesConsumed;
                var line = new Span<byte>(rawBuffer, bytesConsumed, lineLength);
                bytesConsumed += lineLength + 1;


                // ignoring the voter id
                var span = line.Slice(line.IndexOf((byte)',') + 1);

                // movieId
                var firstCommaPos = span.IndexOf((byte)',');
                var movieId = span.Slice(0, firstCommaPos);
                if (!movieId.SequenceEqual(lookingFor)) continue;

                // rating
                span = span.Slice(firstCommaPos + 1);
                firstCommaPos = span.IndexOf((byte)',');
                var rating = double.Parse(Encoding.UTF8.GetString(span.Slice(0, firstCommaPos)), provider: CultureInfo.InvariantCulture);

                sum += rating;
                count++;
            }

        } while (linePosition >= 0 );

        Array.Copy(rawBuffer, bytesConsumed, rawBuffer, 0, (bytesBuffered - bytesConsumed));
        bytesBuffered -= bytesConsumed;
        bytesConsumed = 0;
    }
}

Console.WriteLine($"Average rate for Braveheart is {sum / count} ({count} votes).");

This time, we are loading the data in chunks of 1MB. The code seems a bit more complex (and it is). But, it runs almost 10x faster than the original one. Also, there are not enough allocations to activate the GC.

What do you think? How would you implement it? Share your thoughts in the comments.

Compartilhe este insight:

Comentários

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

Subscribe
Notify of
guest
4 Comentários
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Renato
Renato
4 anos atrás

Solucao final é tao procedural e imperativa q c# deixou de fazer sentido.

Bruno Henrique Pontes Jares
Bruno Henrique Pontes Jares
2 anos atrás

Solução final retornou NaN na minha máquina.

erro.PNG
DDD
DDD
1 ano atrás

there is a slight bug (or display issue) in this line

linePosition = Array.IndexOf(rawBuffer, (byte) 'n', bytesConsumed,
                bytesBuffered - bytesConsumed);

it is copy pasting as (byte)’n’ (the character n) instead of (byte)’\n’ (CR)

DDD
DDD
1 ano atrás

Also there is a a problem when a file is greater than rawBuffer size esp. if the line is split across the outer while (true) iteration.
Additionally, I think the byte count trackers should be cleared on the while(true) iteration. Otherwise the 2nd iteration doesn’t read anything from the stream (try setting the 1024 * 1024 to say 64 * 1 and a run where the data you’re interested in is past the 64 bytes will cause issues.

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:

Arquivo

Pós-pandemia, trabalho remoto e a retenção dos profissionais de TI

CTO Consulting e Especialista em Execução em TI
EximiaCo 2024 - Todos os direitos reservados
4
0
Queremos saber a sua opinião, deixe seu comentáriox
()
x

Muito obrigado!

Deu tudo certo com seu envio!
Logo entraremos em contato

Parsing large files

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

Parsing large files

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?