Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Would it be more scalable and have better performance if ndarray.tobytes() returned a Span<byte>? #64

Open
lintao185 opened this issue May 25, 2024 · 7 comments

Comments

@lintao185
Copy link

    public static IList GetList<T>(ndarray ndarray) where T : struct
    {
        var values = MemoryMarshal.Cast<byte, T>(ndarray.tobytes());
        var list = GetList(values, 0, values.Length, ndarray.shape.iDims);
        return list;
    }
      private static IList GetList<T>(Span<T> values, int start, int end, long[] shapeIDims)
    {
        if (shapeIDims.Length == 1)
        {
            var listr=new List<T>(end-start);
            listr.AddRange(values[start..end]);
            return listr;
        }
        var genericType = typeof(List<>);
        var argType = typeof(T);
        for (int i = 0; i < shapeIDims.Length; i++)
        {
            argType = genericType.MakeGenericType(argType);
        }

        var list = (IList)Activator.CreateInstance(argType);
        var valueLength = end - start;
        var length = (int)(valueLength / shapeIDims[0]);
        for (int i = 0; i < shapeIDims[0]; i++)
        {
            var newStart = start + (i * length);
            var newEnd  = start + ((i + 1) * length);
            newEnd = newEnd >= values.Length ? values.Length : newEnd;
            list.Add(GetList(values, newStart, newEnd, shapeIDims[1..]));
        }

        return list;
    }
  var gn = np.ones((5, 10000, 10000)).astype(np.Float32);
  var gnLs = GetList<float>(gn);

In this example, the conversion is particularly slow.

@KevinBaselinesw
Copy link
Collaborator

I don't have time right now, but I will experiment with this when I get a chance.

Question: is this code MemoryMarshal.Cast<byte, T>(ndarray.tobytes()); even valid in a .NET standard application? In other words , will it work on .net versions for linux and mac too?

I welcome assistance. If you want branch the code and try to turn this into a generic API for different data types, please fell free. I will review it and possibly accept it into the main branch.

@KevinBaselinesw
Copy link
Collaborator

I tried to add a unit test to experiment with your sample. I does not compile cleanly for me. I have not used Span before so it may be user error on my part.

These lines do not compile cleanly:

values[start..end];
shapeIDims[1..]

Can you help?

    [TestMethod]
        public void test_lintao185_1()
        {
            var gn = np.ones((5, 10000, 10000)).astype(np.Float32);
            var gnLs = GetList<float>(gn);


        }

        public static IList GetList<T>(ndarray ndarray) where T : struct
        {
            var values = MemoryMarshal.Cast<byte, T>(ndarray.tobytes());
            var list = GetList(values, 0, values.Length, ndarray.shape.iDims);
            return list;
        }
        private static IList GetList<T>(Span<T> values, int start, int end, long[] shapeIDims)
        {
            if (shapeIDims.Length == 1)
            {
                var listr = new List<T>(end - start);
                listr.AddRange(values[start..end]);
                return listr;
            }
            var genericType = typeof(List<>);
            var argType = typeof(T);
            for (int i = 0; i < shapeIDims.Length; i++)
            {
                argType = genericType.MakeGenericType(argType);
            }

            var list = (IList)Activator.CreateInstance(argType);
            var valueLength = end - start;
            var length = (int)(valueLength / shapeIDims[0]);
            for (int i = 0; i < shapeIDims[0]; i++)
            {
                var newStart = start + (i * length);
                var newEnd = start + ((i + 1) * length);
                newEnd = newEnd >= values.Length ? values.Length : newEnd;
                list.Add(GetList(values, newStart, newEnd, shapeIDims[1..]));
            }

            return list;
        }

@xela-trawets
Copy link

Hi Kevin,

I just happened to click on this and am interested... so please pardon me for butting in.

The two lines that you mention
values[start..end];
shapeIDims[1..]

The compile error is not specific to Spans,
Those lines use a feature from c# version 8 called "range",
(https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/ranges-indexes)

    list.Add(GetList(values, newStart, newEnd, shapeIDims[1..]));

can be read as
list.Add(GetList(values, newStart, newEnd, shapeIDims.Skip(1).ToArray()));// Allocates new array

    listr.AddRange(values[start..end]);

can be read as
listr.AddRange(values.Slice(start,end - start)); //slice() itself does not allocate

With regards to your other question..
Spans and MemoryMarshal.Cast are ".Net standard 2.0" onward and work fine on Linux and all "dotnet" (core). (The implementation of Spans on ".NET Framework 4.8" was slightly different, but it works there too).

A mini primer on Spans:

  1. Spans are basically pointers with a length field.
  2. Spans are (typed) references stored in a "ref struct" - ( not an object ).
  3. Spans refer to memory already allocated - in an Array, stackalloc, native allocation, mapped file etc.
  4. Spans cannot be safely stored in objects fields or properties
    -specifically no List and no Array Span[] can exist.
  5. Spans cannot be used in code with await (due to the fourth law).

Memory is more amenable to storing and async code, Memory is also a typed reference with a length, but is an object and not a struct.

if you have some time, (and dont mind watching videos) the following recent presentation does a reasonable job at explaining how revolutionary the introduction of Span is:
https://learn.microsoft.com/en-us/shows/on-net/a-complete-dotnet-developers-guide-to-span-with-stephen-toub#time=31m51s

Alex

@KevinBaselinesw
Copy link
Collaborator

I appreciate you butting in :) Thanks for the tips. I appreciate you pointing the info.

@KevinBaselinesw
Copy link
Collaborator

As I interpret your question, the answer is that YES, converting an existing array to a Span via MemoryMarshal.Cast is much faster than using the existing "tobytes" method. In this unit test below, it took 7743 ms to convert using tobytes and only 205ms using the MemoryMarshal.Cast method.

  [TestMethod]
        public void test_lintao185_1()
        {
            var gn = np.ones((100000000)).astype(np.Float32);

            System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch();
            sw1.Restart();

            var bytes1 = gn.tobytes();
            var gn1 = np.array(bytes1);

            sw1.Stop();

            //var gnLs = GetList<float>(gn);

            System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch();
            sw2.Restart();

            var bytes2 = MemoryMarshal.Cast<float, byte>(gn.AsFloatArray());
            var gn2 = np.array(bytes2.ToArray());

            sw2.Stop();

            Console.WriteLine(sw1.ElapsedMilliseconds);  <--7743
            Console.WriteLine(sw2.ElapsedMilliseconds);  <--205


        }

@KevinBaselinesw
Copy link
Collaborator

KevinBaselinesw commented May 27, 2024

In the unit test below, the call to tobytes returns a new array of bytes, copied from the source array. If you modify the bytes1 array it does not affect the original. If you use the MemoryMarshall.Cast method, you get a view into the original array and any modifications made to the bytes2 will be reflected in the original array. So, they are very different.

    [TestMethod]
        public void test_lintao185_2()
        {
            var gn = np.ones((10, 10, 10)).astype(np.Int32);

            var bytes1 = gn.tobytes();
            bytes1[0] = 99;


            var gn2 = np.ones((10, 10, 10)).astype(np.Int32);
            var bytes2 = MemoryMarshal.Cast<int, byte>(gn2.AsInt32Array());
            bytes2[0] = 99;

            return;


        }

@KevinBaselinesw
Copy link
Collaborator

Unless I misunderstood the question, I think the existing implementation of .tobytes() is closer to the python version (although that actually returns a string instead of a byte[]).

But this was a great exercise to show that anyone who wants to map an ndarray object to a different data type can do so much faster with MemoryMarshall.Cast as long as they don't want a copy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants