Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fastest (portable) way to split an array in C#

I'm writing a fully managed Mercurial library (to be used in a fully managed Mercurial Server for Windows, coming soon), and one of the most severe performance problems I'm coming across is, strangely enough, splitting an array in parts.

The idea is as follows: there's a byte array with size ranging from several hundred bytes to up to a megabyte and all I need to do with it is to split it in parts delimited by, in my specific case, \n characters.

Now what dotTrace shows me is that my "optimized" version of Split (the code is correct, here's the naive version I began with) takes up 11 seconds for 2,300 calls (there's an obvious performance hit introduced by the dotTrace itself, but everything's up to scale).

Here are the numbers:

  • unsafe version: 11 297 ms for 2 312 calls
  • managed ("naive") version: 20 001 ms for 2 312 calls

So here goes: what will be the fastest (preferably portable, meaning supporting both x86 and x64) way to split an array in C#.

like image 473
Anton Gogolev Avatar asked Aug 26 '12 19:08

Anton Gogolev


2 Answers

I believe the problem is, that you are doing a lot of complex operations in loop. This code removes all the operations except single addition and comparison inside a loop. Other complex stuff happens only when split is detected or at end of an array.

Also, it is hard to tell what kind of data you run your tests with, so this is only guesswork.

public static unsafe Segment[] Split2(byte[] _src, byte value)
{
    var _ln = _src.Length;

    if (_ln == 0) return new Segment[] { };

    fixed (byte* src = _src)
    {
        var segments = new LinkedList<Segment>(); // Segment[c];

        byte* last = src;
        byte* end = src + _ln - 1;
        byte lastValue = *end;
        *end = value; // value-termination

        var cur = src;
        while (true)
        {
            if (*cur == value)
            {
                int begin = (int) (last - src);
                int length = (int) (cur - last + 1);
                segments.AddLast(new Segment(_src, begin, length));

                last = cur + 1;

                if (cur == end)
                {
                    if (lastValue != value)
                    {
                        *end = lastValue;
                    }
                    break;
                }
            }
            cur++;
        }

        return segments.ToArray();
    }
}

Edit: Fixed code, so it returns correct results.

like image 179
Euphoric Avatar answered Sep 28 '22 10:09

Euphoric


For Split, handling ulong on 32-bit machine is really slow, so definitely reduce to uint. If you really want ulong, implement two versions, one for 32-bit, one for 64-bit.

You should also measure whether handling byte at a time is faster.

Need to profile the cost of memory allocation. If it's bigger enough, try to reuse memory across multiple calls.

Other:

ToString: it's faster to use "(" + Offset.ToString() + ", " + Length.ToString() + ")";

GetHashCode: try fixed(byte * b = & buffer[offset])


This version should be really fast, if used multiple times. Key point: no new memory allocation after the internal array has expanded to the right size, minimal data copy.

class ArraySplitter
{
    private byte[] m_data;
    private int    m_count;
    private int[]  m_stops;

    private void AddRange(int start, int stop)
    {
        // Skip empty range
        if (start > stop)
        {
            return;
        }

        // Grow array if needed
        if ((m_stops == null) || (m_stops.Length < (m_count + 2)))
        {
            int[] old = m_stops;

            m_stops = new int[m_count * 3 / 2 + 4];

            if (old != null)
            {
                old.CopyTo(m_stops, 0);
            }
        }

        m_stops[m_count++] = start;
        m_stops[m_count++] = stop;
    }

    public int Split(byte[] data, byte sep)
    {
        m_data  = data;
        m_count = 0;      // reuse m_stops

        int last = 0;

        for (int i = 0; i < data.Length; i ++)
        {
            if (data[i] == sep)
            {
                AddRange(last, i - 1);
                last = i + 1;
            }
        }

        AddRange(last, data.Length - 1);

        return m_count / 2;
    }

    public ArraySegment<byte> this[int index]
    {
        get
        {
            index *= 2;
            int start = m_stops[index];

            return new ArraySegment<byte>(m_data, start, m_stops[index + 1] - start + 1);
        }
    }
}

Test program:

    static void Main(string[] args)
    {
        int count = 1000 * 1000;

        byte[] data = new byte[count];

        for (int i = 0; i < count; i++)
        {
            data[i] = (byte) i;
        }

        Stopwatch watch = new Stopwatch();

        for (int r = 0; r < 10; r++)
        {
            watch.Reset();
            watch.Start();

            int len = 0;

            foreach (var seg in data.MySplit(13))
            {
                len += seg.Count;
            }

            watch.Stop();

            Console.WriteLine("MySplit      : {0} {1,8:N3} ms", len, watch.Elapsed.TotalMilliseconds);

            watch.Reset();
            watch.Start();

            ArraySplitter splitter = new ArraySplitter();

            int parts = splitter.Split(data, 13);

            len = 0;

            for (int i = 0; i < parts; i++)
            {
                len += splitter[i].Count;
            }

            watch.Stop();
            Console.WriteLine("ArraySplitter: {0} {1,8:N3} ms", len, watch.Elapsed.TotalMilliseconds);
        }
    }

Result:

MySplit      : 996093    9.514 ms
ArraySplitter: 996093    4.754 ms
MySplit      : 996093    7.760 ms
ArraySplitter: 996093    2.710 ms
MySplit      : 996093    8.391 ms
ArraySplitter: 996093    3.510 ms
MySplit      : 996093    9.677 ms
ArraySplitter: 996093    3.468 ms
MySplit      : 996093    9.685 ms
ArraySplitter: 996093    3.370 ms
MySplit      : 996093    9.700 ms
ArraySplitter: 996093    3.425 ms
MySplit      : 996093    9.669 ms
ArraySplitter: 996093    3.519 ms
MySplit      : 996093    9.844 ms
ArraySplitter: 996093    3.416 ms
MySplit      : 996093    9.721 ms
ArraySplitter: 996093    3.685 ms
MySplit      : 996093    9.703 ms
ArraySplitter: 996093    3.470 ms
like image 34
Feng Yuan Avatar answered Sep 28 '22 08:09

Feng Yuan