Why does this look wrong? (Self-taught coder - Optimizing question)

I’m trying to learn how to code better.
This is basically a function that will run inside a Coroutine.
It looks like I’m constantly destroying and recreating a new Vector3.

    IEnumerator UpdatePath()
    {
            pathfinder.SetDestination(new Vector3(target.position.x, 0, target.position.z));
            yield return new WaitForSeconds(0.1);
    }

If I’m destroying and recreating a new object every time, isn’t it inefficient?
I need to ask this cause I think I’m supposed to be really careful what I put into a code that will constantly loop.
I understand creating a Vector3 over and over again isn’t performance intensive, but if ever I need to update something that IS performance intensive inside either update() or coroutine, it will essential that I know how to do this the right way.

Use cod tags:

1 Like

Vector3 is a struct, it is therefore not a referenced object and doesn’t have the sort of overhead you may be thinking of.

Structs cost something to be created, but it’s not really a whole lot.

The expense in this code is the fact you’ll be creating a new IEnumerator every time you call UpdatePath, as well as a WaitForSeconds. Those things cost a lot more than create a Vector3.

Heck… any time you access the ‘position’ property of a transform, a new Vector3 is created. You technically create 3 Vector3’s in your code above.

4 Likes

Vector3 is a struct, which means (among other things) that it is typically allocated on the stack. That’s a cheap operation. Realistically, creating an object on the heap is typically an inexpensive operation unless you are dealing with a really under-powered piece of hardware. Allocating and deallocating on the stack is just adding or subtracting a number to or from a register. So it’s super cheap.

Generally, don’t worry about performance problems that might be. Worry about the ones you actually have.

1 Like

The biggest problem with the code snippet is that it doesn’t do anything interesting?

When you start the coroutine, you immediately set a destination, then you wait before you don’t do anything. Is there code missing, or do you believe that that WaitForSeconds call has an effect?

1 Like

This is how it works…

You have reference types, and value types.

Reference types are most often classes and interfaces.

Value types are most often things like structs, primitives like int/float/bool, enums, etc.

Value types are said to usually exist “on the stack”… but that’s not exactly accurate. Value types exist in the memory scope that they are needed.

If it’s a variable in a function/method, then it exists on the stack (since the stack is where function/method memory exists). When a function/method is called, the stack expands with the necessary memory for said method. It’s like the first thing a method does, the stack has memory locations pushed onto the stack for every needed variable. Lets look at the IL code for Mathf.Min decompiled with Dotpeek:

    /// <summary>
    ///   <para>Returns the smallest of two or more values.</para>
    /// </summary>
    /// <param name="a"></param>
    /// <param name="b"></param>
    /// <param name="values"></param>
    public static float Min(float a, float b)
    {
      return (double) a >= (double) b ? b : a;
    }

//the IL for the above:
  .method public hidebysig static float32 
    Min(
      float32 a, 
      float32 b
    ) cil managed 
  {
    .maxstack 2
    .locals init (
      [0] float32 V_0
    )

    IL_0000: nop         

    // [226 7 - 226 47]
    IL_0001: ldarg.0      // a
    IL_0002: ldarg.1      // b
    IL_0003: bge.un       IL_000e

    IL_0008: ldarg.0      // a
    IL_0009: br           IL_000f
    IL_000e: ldarg.1      // b
    IL_000f: stloc.0      // V_0
    IL_0010: br           IL_0015
    IL_0015: ldloc.0      // V_0
    IL_0016: ret         

  } // end of method Mathf::Min

Note how the first thing called by the method is:

.maxstack 2
.locals init(
    [0]float32 V_0
)

This essentially is telling the JIT compiler that the max size the stack needs is 2 words (32-bit) of memory.

This chunk of memory is what will be used by the method to operate during its life. Once the method is complete, that memory will be purged from the stack (by moving the stack pointer back maxsize words).

Any value types that exist as local variables (the 2 floats in the case of Min) will exist in this portion of memory. And this is very efficiently dealt with memory. There’s no garbage collection on it, it exists for the life of the method, and immediately is freed on completion. The overhead of creating structs like a Vector3 is if you happen to use a constructor method for said struct… which is itself another method call, which allocates on the stack as well before it returns (though most JIT compilers optimize these to some extent… but that’s getting into even nittier grittier junk you don’t have to concern yourself with).

THING IS, this doesn’t mean value types ALWAYS exist on the stack. Far from.

Take for instance this class:

public class Foo
{
    public float value;
    public Vector3 position;
}

Because it’s a class, it’s a reference type. And reference types are allocated on the heap. How this works is that the size of the object is determined, and that amount of memory is selected from the heap, and that is where the object will now exist. Any variable that references that object holds a pointer, the location in memory that the object exists (the pointer is smart though, since the object can move in the heap when the heap is resized/cleaned, the pointer may be updated behind the scenes… you don’t have to concern yourself with this).

The cost of this comes from how we remove objects from the heap.

Basically every once in a while the garbage collector sifts through the entire heap and checks if any references exist for an object. If no reference/pointer to an object exists, then it’s considered dead, and starts its deallocation process where it gets the deconstructor called (if one exists) and it is thrown out.

This process is expensive, especially in the version of Mono used by Unity.

Thing is… what exactly does this memory hold?

Well… in the case of our class Foo. It holds the fields/member variables that make up the class. In this case it’s a float and a Vector3. And since a Vector3 is just 3 floats… really the memory taken up is 4 32-bit words all identified as floats.

float|float|float|float

Along with the identifying nature of the object… which again is mostly invisible to you.

What this means is that YES, a value type or struct can exist on the heap. It just exists as the members of a class.

Another time it might happen is if you “box” your value type. This can happen if you say something like:

object x = 0.1f;

Or if say you have a struct that implements an interface. And you then reference that struct via its interface:

public struct Bar : IEnumerator
{
    public float A;
    public float B;

    //implement IEnumerator to enumerator A and B
}

IEnumerator e = new Bar() {
    A = 1f,
    B = 5f
};

both x and e in these examples are boxed an on the heap.

So in your initial code:

IEnumerator UpdatePath()
{
    pathfinder.SetDestination(new Vector3(target.position.x, 0, target.position.z));
    yield return new WaitForSeconds(0.1);
}

Well every time you call UpdatePath, a ‘new IEnumerator’ is returned (this is called an iterator function and is syntax sugar for a enumerator that steps through your code… this can be very complicated under the hood). You also create a ‘new WaitForSeconds’ object.

But as for the variables… they exist on the sta… wait, no they don’t.

So, an iterator function really is syntax sugar for an anonymous object.

Really, the compiler turns this code into a class. All the variables in the function instead of being stack members like I described in the IL above, they are fields/member variables of this anonymous class. The UpdatePath function now returns a ‘new’ object that is the type of this anonymous class. And that object exists on the heap.

This object implements the IEnumerator interface, and the MoveNext method is implemented to move through the code to each ‘yield’ statement. It unravels to look like an IL equivalent of a select statement, and a state field that tracks at what point in the code you’re at.

It’s not the Vector3 that costs anything… it’s the creating this object that costs stuff. And that cost would be there regardless of the Vector3 or not.

6 Likes

He has a point. If that is supposed to run in a loop you will need to wrap the code in a while() statement. But then that is non sense, you can simply make SetDestination an IEnumerator.

IEnumerator UpdatePath()
{
while(true) {
    pathfinder.SetDestination(new Vector3(target.position.x, 0, target.position.z));
    yield return new WaitForSeconds(0.1);
}
}

But if that method is all that happens then you simply return null. There is no need for WaitFor Seconds.
But then this is non sense, you can simply make SetDestination() an IEnumerator.

IEnumerator UpdatePath()
{
pathfinder.SetDestination(new Vector3(target.position.x, 0, target.position.z));
yield return null;
}