5
\$\begingroup\$

I am experimenting with Span<> and Memory<> and checking how to use it for example for reading UTF8 text from a NetworkStream using Read(Span<Byte>). I created a fake method that simulates the byte read, and although this code works for the test, I am wondering if this is the right way of doing the most with these new structures in terms of efficiency and memory.

static void Main(string[] args)
{
    Memory<byte> byteMemory = new byte[3];
    Memory<char> charMemory = new char[1024];

    var byteMemoryPos = 0;
    var byteMemoryReaded = 0;
    var charMemoryPos = 0;
    var readed = -1;

    var decoder = Encoding.UTF8.GetDecoder();

    do
    {
        // pass the sliced part of the memory where I want to write as span
        readed = FakeNetworkStreamRead(byteMemory.Slice(byteMemoryPos + byteMemoryReaded).Span);
        Console.WriteLine($"Readed {readed} bytes");

        byteMemoryReaded += readed;

        // pass the sliced part of the memory I want to parse, and the part 
        // of the char buffer where I want to write
        decoder.Convert(byteMemory.Slice(byteMemoryPos, byteMemoryReaded).Span,
                        charMemory.Slice(charMemoryPos).Span,
                        false, out int bytesUsed, out int charsUsed, out bool completed);

        // update offsets and counts
        byteMemoryPos -= bytesUsed;
        charMemoryPos += charsUsed;
        byteMemoryReaded -= bytesUsed;

        // reset offset if nothing to read
        if (byteMemoryReaded == 0)
            byteMemoryPos = 0;
    }
    while (readed != 0);

    Console.WriteLine(new string(charMemory.Slice(0, charMemoryPos).Span));
}

static int _position = 0;
static byte[] _data = Encoding.UTF8.GetBytes("Hi this is test!!");

/// <summary>
/// Pretends to be NetworkStream.Read, that only manages to read 5 bytes each time
/// </summary>
/// <param name="span"></param>
/// <returns></returns>
static int FakeNetworkStreamRead(Span<byte> span)
{
    var pending = Math.Min(_data.Length - _position, span.Length);
    pending = Math.Min(pending, 5);
    for (int i = _position, j = 0; i < _position + pending; j++, i++)
    {
        span[j] = _data[i];
    }
    _position += pending;
    return pending;
}
\$\endgroup\$
2
\$\begingroup\$

I have no experience with Span<T> or Memory<T> so the following is just some general considerations on coding etc.


In FakeNetworkStreamRead(...): why have a internal limit of 5 bytes? Why not let the length of the Span<T> argument determine the maximum bytes to read?


Why are you using a Memory<T> as buffer, when reading from the stream?:

Memory<byte> byteMemory = new byte[3];

Why not just a Span, because you only use it as a such and not holding or collecting any data in it:

Span<byte> byteSpan = new byte[3];

IMO you should change the do {} while(...) loop to a while (...) loop, because, if the stream is empty you do a "lot" of work in the first round trip for no reason:

  int read = 0;

  while ((read = FakeNetworkStreamRead(byteSpan)) > 0)
  {
    ...
  }

With the above in mind I've tried to refactor your code to:

static int FakeNetworkStreamRead(Span<byte> span)
{
  int stop = Math.Min(_position + span.Length, _data.Length);
  int i = 0;

  for (; _position < stop; i++, _position++)
  {
    span[i] = _data[_position];
  }

  return i;
}



static void Main(string[] args)
{
  Span<byte> byteSpan = new byte[3];
  Memory<char> charMemory = new char[1024];

  var charMemoryPos = 0;
  var decoder = Encoding.UTF8.GetDecoder();
  int read = 0;

  while ((read = FakeNetworkStreamRead(byteSpan)) > 0)
  {
    Console.WriteLine($"Read {read} bytes");

    // pass the sliced part of the memory I want to parse, and the part 
    // of the char buffer where I want to write
    decoder.Convert(byteSpan.Slice(0, read),
                    charMemory.Slice(charMemoryPos).Span,
                    false, out int bytesUsed, out int charsUsed, out bool completed);

    // update offsets and counts
    charMemoryPos += charsUsed;
  }

  Console.WriteLine(new string(charMemory.Slice(0, charMemoryPos).Span));
}

In order to flush the decoder state, the do {} while(...) loop is useful anyway:

static void Main(string[] args)
{
  int bufferLength = 3;
  Span<byte> byteSpan = new byte[bufferLength];
  Memory<char> charMemory = new char[1024];

  var charMemoryPos = 0;
  var decoder = Encoding.UTF8.GetDecoder();
  int read = 0;

  do 
  {
    read = FakeNetworkStreamRead(byteSpan);

    decoder.Convert(byteSpan.Slice(0, read),
                    charMemory.Slice(charMemoryPos).Span,
                    read == 0, out int bytesUsed, out int charsUsed, out bool completed);

    Console.WriteLine($"Read {read} bytes -> Used: {bytesUsed} -> Completed: {completed} -> CharUsed: {charsUsed} -> {decoder.FallbackBuffer.Remaining}");

    charMemoryPos += charsUsed;
  } while (read > 0) ;

  Console.WriteLine(new string(charMemory.Slice(0, charMemoryPos).Span);
}

Here the read == 0 argument to decoder.Convert(...) becomes true at the end of the stream, which will flush the state and clears the internal buffer of decoder

\$\endgroup\$
  • \$\begingroup\$ Hi, thanks for that! The limits are artificial for the sake of the test. The UTF8 decoder is not always able to transform all the bytes into chars, since some chars have up to 4 bytes, and maybe not all are present in the buffer, that is why bytesUsed is very important. \$\endgroup\$ – vtortola Sep 29 '18 at 12:57
  • \$\begingroup\$ @vtortola: You are certainly right. I was blundering, and my answer isn't that good at all. \$\endgroup\$ – Henrik Hansen Sep 29 '18 at 13:36
  • 1
    \$\begingroup\$ @vtortola: And then again, I think the decoder maintains the state internally, so I think your math is redundant when it comes to byteMemoryPos and byteMemoryReaded \$\endgroup\$ – Henrik Hansen Sep 29 '18 at 15:43
  • 1
    \$\begingroup\$ You are actually right. That out var is useful for when the target char array cannot hold all the input array, which is not the case. Thanks! \$\endgroup\$ – vtortola Oct 1 '18 at 20:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.