Understanding vtable And Unity’s Memory Layout — Mechanisms for Polymorphism in Unity

Rossam
7 min readJul 5, 2023

Hi everyone. Here I am, a Japanese game engineer who adores Eevee and mainly thrives on Twitter. I usually work as a lead engineer, juggling from writing code to managing tasks and schedules, taking care of team management, and hiring engineers. I sometimes receive requests from Metaverse/game companies, so I mainly work on Metaverse/game projects on the side.

Today, I would like to delve into understanding vtable and Unity’s memory layout, specifically the mechanisms for polymorphism within Unity. For this purpose, I’ll use some images from the slide I have created, available here.

https://speakerdeck.com/rossam/understanding-vtable-and-unitys-memory-layout

Alright, let’s dive right in!

About Unity’s memory

Unity manages three main types of memory.

  • Managed memory
  • C# Unmanaged memory
  • Native memory

Let’s focus on Managed Memory at this time!

There are three types of managed memory stored in RAM.

Stack

The size depends on the application’s architecture of the Build target; for example, for iOS/Android, it is 1 MB. The OS on the device side strictly manages this memory size; the Garbage Collector (GC) does not intervene here.

Heap

This is a memory area that can be used freely within the program. Heap area is used when data is referenced from many places, and the scope is unclear, when the size can only be determined at runtime, or when the required size is large. Memory allocation and deallocation are automatically performed at runtime.

Native VM memory

Contains metadata used by generics and reflection.

I will describe Stack and Heap for now :)

The developer mainly uses the Stack and Heap. The following types are saved on these stored;

Stack

  • The function is allocated for arguments and local variables in the function at the time the function is called, and the allocated portion is released when the function returns to the original function. It is also used to store the return value of the function.
  • So, the data stored in the stack has the same lifetime as the function’s scope.
  • Array types are stored in a heap, but if you use stackalloc, it can use to store them on the stack. If you want to know more details, please search for this.

Heap

  • A heap is expanded by allocating memory whenever it is needed in the program and released when it is finished being used. Therefore, it is often used when you want to retain data beyond the function scope.
  • The heap extended is basically not returned to prevent re-expansion.
  • Boxing is making a copy of the value type on the heap and passing it to a variable of type object. Whenever possible, value types should be used as they are.
  • The heap has different types, and static variables are allocated special memory for them. I won’t delve into this issue in depth this time.

Advantages and disadvantages of Stack

  • Implemented by increasing or decreasing the allocated contiguous memory and stack pointer, the memory allocation speed is much faster than heap or freestore, which are dynamic storage.
  • Since the amount of memory required for each variable type is determined at the time of compilation and the data is stored, a stack overflow will occur if a value outside the range of the variable type is entered.
  • A stack overflow also occurs when a function is recursively called or when too much large data is placed on the stack, and the stack memory is used up.

Advantages and disadvantages of Heap

  • Unity uses the Boehm GC. This open-source GC keeps portability high at the expense of some loss of functionality and performance. Thanks to this GC, there is no need to allocate/deallocate memory manually, reducing the possibility of memory errors due to human error. However, since it does not adjust the free memory created after freeing memory, only less than the initially stored data can be put into the free memory.
  • Since this GC periodically surveys memory, the larger the heap, the higher the cost per GC execution. If the heap is expanded with regular frequency, the GC processing cost will remain increased because space on the heap is not returned after use.
  • Unity has various mechanisms for dynamically allocating memory. There is also a feature called a dynamic allocator for allocating memory on the heap. It can handle any allocation size, but the process of allocating memory requires a lot of overhead. I won’t explain it in detail here because there is no end to it, but if you are interested, please search for it because it is also a very deep area.

Next, let’s review offsets to understand Unity’s basic of memory layout.

About Offset

(Memory allocation varies from compiler to compiler, so it may not be exactly as shown in the above figure. However, for the sake of explanation, I will assume that they are configured in the above order)

public class A
{
public void HogeA()
{ … }
}

public class B : A
{
public void HogeB()
{ … }
}

public class C : B
{
public void HogeC()
{ … }
}

In the case of Class C with simple inheritance, the address of a group of functions is calculated at compile time & link time. The memory allocation configuration of a Class C instance will look like the following;

When calling HogeB() implemented in class B from an object of class C, it is necessary to move the ‘this’ pointer to the object part of B to get the pointer for class B. To do so, the pointer is moved by referencing the offset for class B that class C has.

void Main()
{
var C = new C();
c.HogeB();
}

The addresses and offsets of the function groups are computed at compile-time & link-time, so the offsets exist as compile-time constants. Adding or subtracting this offset to the ‘this’ pointer will point to the information of class B inside object C.

Now it is time to understand vtable!

About vtable

Type-dependent function calls are resolved using a “virtual method table(vtable)”. This is a collection of pointers to virtual functions related to the target class. A unique vtable is created for each class if the class has at least one virtual method.

public class Base
{
public virtual int GetId()
{ … }
}

public class A : Base
{
public override int GetId()
{ … }
}

public class B : A
{
public new int GetId()
{ … }
}

Virtual function calls are indirect function calls via vtable. A class always has one vtable if it has one or more virtual functions. In addition, all objects of that class have a pointer (vpointer, vptr) pointing to the vtable of that class implicitly added at the beginning, as shown in the figure.

For example, looking at the table information for class B, the function pointer of the A-side with override GetId() and the function pointer of GetId() redefined for class B with new is stored. The function pointer of GetId(), which is redefined for class B with new, is stored in the table.

vtable’s disadvantages

Increase instance size

Instances of the virtual function holding class have a pointer to access the vtable that is implicitly added to the top of the class. Because of it, the class’s size will be slightly larger.

The increase in instance size (4 bytes or 8 bytes (depending on the processing system)) should be considered if you are developing on a platform with a large memory limitation.

Performance loss

There is a slight performance loss because virtual function calls are made via vtable. This is very small, but the overhead can quickly increase if thousands of calls are made per frame.

If performance is significantly impaired due to virtual function calls, they must be replaced with normal functions.

Profiling difficulty

Even if the use of virtual functions impairs performance, it is difficult to pinpoint the cause.

It is necessary to use them with risk in mind from the beginning.

To prevent unnecessary creation of vtable due to human error, it is also useful to explicitly prohibit inheritance by using the sealed modifier as a project code convention.

This is the end of this article. Thank you, guys, for reading this far!
Have a good development time, everyone!

References

--

--