-2

I'm looking for a way to replace some of my List objects with arrays in my project to boost performance. The reasoning is that the List I would replace do not change size often (sometimes only once), so it would make sense to swap them out with arrays. Also, I would like to prevent allocations on the heap as much as possible.

So my idea is to create a struct (ArrayStruct) with two members "Count" and "Array". The struct would have some functions to add/remove/get/set/ect... elements in the array. The count would keep count of the elements that are usable in the array, and the array would only increase in size, never decrease. Note that this is for a very particular case because I know the array size won't be very big.

The main problem I have is when I pass the reference to an already existing ArrayStruct object (named a in the example) to another (named b in the example) - I can edit the array in it, but once I resize it in a, I get an out of range exception in b. Is this because of the way I'm updating the reference via System.Array.Resize, so that the object b somehow does not attribute this change?

The only thing I could come up with as an idea was trying to override the assignment operator so that it would create a new array on assignment... but obviously that does not work.

My ArrayStruct

    public struct ArrayStruct<T>
    {
        [SerializeField] private int m_Count;
        [SerializeField] private T[] m_Array;

        public int Count => m_Count;

        public void Initialize(int startSize)
        {
            m_Count = 0;
            m_Array = new T[startSize];
        }

        public T GetAt(int index)
        {
            return m_Array[index];
        }

        public void SetAt(int index, T newValue)
        {
            m_Array[index] = newValue;
        }

        public void Add(T newElement)
        {
            if (m_Array == null) {
                m_Array = new T[1];
            } else if (m_Array.Length == m_Count) {
                System.Array.Resize(ref m_Array, m_Count + 1);
            }

            m_Array[m_Count] = newElement;
            m_Count++;
        }

        public void Remove(T element)
        {
            for (int i = 0; i < m_Count; ++i) {
                if (!element.Equals(m_Array[i])) {
                    continue;
                }

                for (int j = index; j < m_Count - 1; ++j) {
                    m_Array[j] = m_Array[j + 1];
                }
                m_Count--;
                m_Array[m_Count] = default(T);
                break;
            }
        }

        //Trying to overload the = operating by creating a auto cast, this gives a compile error
        /*public static implicit operator ArrayStruct<T>(ArrayStruct<T> otherArray)
        {
            var newArray = new ArrayStruct<T>();
            newArray.m_Count = otherArray.Count;
            newArray.m_Array = new T[otherArray.Length];
            otherArray.m_Array.CopyTo(newArray.m_Array);
            return newArray;
        }*/

An example showcasing the problem

var a = new ArrayStruct<string>()
a.Add("hello");
var b = a;
print (b.GetAt(0)); //"hello"

a.SetAt(0, "new value for a");

print(b.GetAt(0));//"new value for a"  , changing a changed b because the array is the same in both, this is bad

a.Add("resizing array");
print (b.GetAt(1)); //"Out of range exception" ,  because the array was resized for a but not for b therefore the connection broke

So do any of you have an idea of how I could make a new copy the array when I assign the struct to another variable? Of course, I know I could use a function to do something like so b = new ArrayStruct(a); But I want it to be implicit. Or if there was a way to prevent assignments that would also work for me. var a = new ArrayStruct();//let this work var b = a; //Prevent this from happening

If you have any other solutions for replacing Lists with arrays conveniently please let me know

SpiritBob
  • 2,355
  • 3
  • 24
  • 62
Santiago14
  • 65
  • 4
  • 8
    `I'm looking for a way to replace some of my List objects with arrays in my project to boost performance.` I haven't seen your codebase. But I will tell you this. I've _never_ seen a codebase where doing this kind of work would have made a material performance difference. Once you solve your current issue, the memory cost of copying the array every time you copy the struct is likely to offset any performance gain. – mjwills Aug 15 '19 at 07:14
  • 5
    Based on what you've shown, you're just reimplementing `List`. – madreflection Aug 15 '19 at 07:14
  • 6
    @Santiago14 Just to clarify, you do understand that a `List` is basically a thin wrapper over an array already? As madreflection states - this sounds remarkably similar to what you are trying to build (although `List` is a class not a struct). – mjwills Aug 15 '19 at 07:16
  • The fact that you're using a struct has some interesting side-effects... For example, if you pass it somewhere (not using `ref`) and it resizes, the change doesn't make it back to the caller. `List` won't fail you there. – madreflection Aug 15 '19 at 07:17
  • What performance benefit do you expect to see from this over using `List`? How will that benefit be achieved? – ProgrammingLlama Aug 15 '19 at 07:17
  • 5
    `Also, I would like to prevent allocations on the heap as much as possible.` `The only thing I could come up with as an idea was trying to override the assignment operator so that it would create a new array on assignment.` These two requirements are in conflict. You want to prevent allocations on the heap. But you also want to create new arrays on assignment (which will allocate on the heap). You can't have both. – mjwills Aug 15 '19 at 07:20
  • 4
    I had a similar thing in one of my projects. I was thinking about replacing some small-ish classes with `readonly structs` and then pass them around using `in`. I did a micro benchmark first and saw that perf wise, the structs ended up being 2.5 slower but allocated 3 times less. The allocations were all collected in Gen0 so the allocations didn't really matter. I threw that idea out of the window. You should measure it first. – JohanP Aug 15 '19 at 07:22
  • 3
    You should read this: [Which is faster](https://ericlippert.com/2012/12/17/performance-rant/) – Liam Aug 15 '19 at 07:30
  • 2
    Thanks for all the comments. I guess I'll have to rethink whether it is even worth changing the lists into arrays. The reason I wanted to use a struct and not a class was to not write in the heap, but as @mjwills pointed out if I copy the array I am still writing to the heap...I'll have to make some benchmarks to see how worth it it would be – Santiago14 Aug 15 '19 at 09:13
  • 4
    Your last statement is the most important thing you've said. **You have to make some benchmarks to see whether the optimization is worth it**, but that is actually the *second* step. The first step is *decide ahead of time what acceptable performance is, and then measure your performance to see if it is already acceptable*. Don't waste time and energy on getting a 1% optimization for code that is already fast enough! – Eric Lippert Aug 15 '19 at 16:56

2 Answers2

0

So do any of you have an idea of how I could make a new copy the array when I assign the struct to another variable? Of course, I know I could use a function to do something like so b = new ArrayStruct(a); But I want it to be implicit.

You can't do that. If a is a struct and you execute var b = a, then it will always just set b to a shallow copy of a; there's no way to change that behavior.

Doing something like b = new ArrayStruct(a) is the best you can do.

Tanner Swett
  • 3,241
  • 1
  • 26
  • 32
0

You're not understanding what the "=" operator is doing there. Objects are defined by reference, meaning when you write var b = a; you are actually asking for what you wish to avoid to happen, that is - object "b", pointing to the same object, which is "a". You're passing the object "b" to point to the reference of object "a", which object holds the address to the object in question you defined, using the new keyword var a = new ArrayStruct<string>()

Your operator overloading does not work because you can't use it for conversions of the same type as the class you're writing the operator in. You'd either have to make a separate object (class), which would defeat the point you're trying to fix, or pass the m_array instead: public static implicit operator ArrayStruct<T>(T[] array)

You can also do something like this:

        public static ArrayStruct<T> CopyFrom (ArrayStruct<T> array)
    {
        var newArray = new ArrayStruct<T>();
        newArray.m_Array = new T[array.Count + 1];
        newArray.Count = array.Count;
        Array.Copy(array.m_Array, 0, newArray.m_Array, 0, array.m_Array.Length);
        return newArray;
    }

Now it should work exactly as you wanted.

    var a = new ArrayStruct<string>();
    a.Add("hello");
    var b = ArrayStruct<string>.CopyFrom(a);

    a.SetAt(0, "new value for a");


    a.Add("resizing array");

    Console.WriteLine(b.Count); // b returns 1
    Console.WriteLine(a.Count); // a returns 2

As to why your index was going out of bounds, i.e it did not preserve the reference found in "a", this is all in correlation with references, yet again.

In the following code System.Array.Resize(ref m_Array, Count + 1); you're changing the reference found for object m_array in "a", when you're adding the new element with the line a.Add("resizing array");. That essentially means that your "a" object's m_array (which is another object of type array[]) is now pointing to a new object (of type array[]), which is of a new size. Okay, but object "a" is now using the new reference, whilst object "b" is still using the old one. m_array in object "b" only has a single element, while the new m_array object in "a" has the newly added element "resizing array", along with any previously added ones.

I believe that's how System.Array.Resize works? Edit me if I'm wrong.

Below is a resource explaining how to do a deep cloning of an object, if that's what you truly wish to do (i.e create a completely new copy of an object, as well as any other objects inside)


Deep cloning of objects

SpiritBob
  • 2,355
  • 3
  • 24
  • 62