Hi. I tested some performance comparison when accessing to array element.
It tested 7 types of method:
- Using UnsafeUtility.ReadArrayElement()
- Using UnsafeUtility.ReadArrayElement() + inline
- Using UnsafeUtility.ArrayElementAsRef()
- Using UnsafeUtility.ArrayElementAsRef() + inline
- Using unsafe pointer dereference
- Using unsafe pointer dereference + inline
- Using unsafe pointer + C#'s ref keyword.
Full Test code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Runtime.InteropServices;
using Unity.Collections.LowLevel.Unsafe;
using Unity.VisualScripting;
using UnityEngine;
public class PerformanceTest : MonoBehaviour
{
private unsafe void Start()
{
var size = 5000;
var loopCount = 5000;
var buffer = UnsafeUtility.Malloc(UnsafeUtility.SizeOf<BigStruct>() * size, UnsafeUtility.AlignOf<BigStruct>(), Unity.Collections.Allocator.Temp);
for (int i = 0; i < size; i++)
{
UnsafeUtility.WriteArrayElement<BigStruct>(buffer, i, new BigStruct { A = i });
}
var stopwatch = new System.Diagnostics.Stopwatch();
var sum = 0;
// Warmup
TestByReadElement(buffer, size, loopCount);
TestByReadElementInline(buffer, size, loopCount);
TestByElementAsRef(buffer, size, loopCount);
TestByElementAsRefInline(buffer, size, loopCount);
TestByPtr(buffer, size, loopCount);
TestByPtrInline(buffer, size, loopCount);
TestByPtrRef(buffer, size, loopCount);
// Test
stopwatch.Restart();
sum = TestByReadElement(buffer, size, loopCount);
Debug.Log($"{nameof(TestByReadElement),-32} : sum={sum}, time={stopwatch.Elapsed.ToString("c")}");
stopwatch.Restart();
sum = TestByReadElementInline(buffer, size, loopCount);
Debug.Log($"{nameof(TestByReadElementInline),-32} : sum={sum}, time={stopwatch.Elapsed.ToString("c")}");
stopwatch.Restart();
sum = TestByElementAsRef(buffer, size, loopCount);
Debug.Log($"{nameof(TestByElementAsRef),-32} : sum={sum}, time={stopwatch.Elapsed.ToString("c")}");
stopwatch.Restart();
sum = TestByElementAsRefInline(buffer, size, loopCount);
Debug.Log($"{nameof(TestByElementAsRefInline),-32} : sum={sum}, time={stopwatch.Elapsed.ToString("c")}");
stopwatch.Restart();
sum = TestByPtr(buffer, size, loopCount);
Debug.Log($"{nameof(TestByPtr),-32} : sum={sum}, time={stopwatch.Elapsed.ToString("c")}");
stopwatch.Restart();
sum = TestByPtrInline(buffer, size, loopCount);
Debug.Log($"{nameof(TestByPtrInline),-32} : sum={sum}, time={stopwatch.Elapsed.ToString("c")}");
stopwatch.Restart();
sum = TestByPtrRef(buffer, size, loopCount);
Debug.Log($"{nameof(TestByPtrRef),-32} : sum={sum}, time={stopwatch.Elapsed.ToString("c")}");
}
private unsafe int TestByReadElement(void* buffer, int size, int loopCount)
{
var sum = 0;
for (int loopIndex = 0; loopIndex < loopCount; loopIndex++)
{
for (int i = 0; i < size; i++)
{
var value = UnsafeUtility.ReadArrayElement<BigStruct>(buffer, i);
sum += value.A;
}
}
return sum;
}
private unsafe int TestByReadElementInline(void* buffer, int size, int loopCount)
{
var sum = 0;
for (int loopIndex = 0; loopIndex < loopCount; loopIndex++)
{
for (int i = 0; i < size; i++)
{
sum += UnsafeUtility.ReadArrayElement<BigStruct>(buffer, i).A;
}
}
return sum;
}
private unsafe int TestByElementAsRef(void* buffer, int size, int loopCount)
{
var sum = 0;
for (int loopIndex = 0; loopIndex < loopCount; loopIndex++)
{
for (int i = 0; i < size; i++)
{
ref var value = ref UnsafeUtility.ArrayElementAsRef<BigStruct>(buffer, i);
sum += value.A;
}
}
return sum;
}
private unsafe int TestByElementAsRefInline(void* buffer, int size, int loopCount)
{
var sum = 0;
for (int loopIndex = 0; loopIndex < loopCount; loopIndex++)
{
for (int i = 0; i < size; i++)
{
sum += UnsafeUtility.ArrayElementAsRef<BigStruct>(buffer, i).A;
}
}
return sum;
}
private unsafe int TestByPtr(void* buffer, int size, int loopCount)
{
var sum = 0;
for (int loopIndex = 0; loopIndex < loopCount; loopIndex++)
{
for (int i = 0; i < size; i++)
{
var value = (((BigStruct*)buffer) + i);
sum += value->A;
}
}
return sum;
}
private unsafe int TestByPtrInline(void* buffer, int size, int loopCount)
{
var sum = 0;
for (int loopIndex = 0; loopIndex < loopCount; loopIndex++)
{
for (int i = 0; i < size; i++)
{
sum += (((BigStruct*)buffer) + i)->A;
}
}
return sum;
}
private unsafe int TestByPtrRef(void* buffer, int size, int loopCount)
{
var sum = 0;
for (int loopIndex = 0; loopIndex < loopCount; loopIndex++)
{
for (int i = 0; i < size; i++)
{
ref var value = ref (*(((BigStruct*)buffer) + i));
sum += value.A;
}
}
return sum;
}
[StructLayout(LayoutKind.Sequential)]
struct BigStruct
{
public int A;
public long B0;
public double B1;
public double B2;
public double B3;
public double B4;
public double B5;
public double B6;
public double B7;
public double B8;
public double B9;
}
}
And result in IL2CPP Windows64 release build:
Result is surprising. Accessing via pointer+ref (method 7, TestByPtrRef) is extremely faster than others, including pure pointer dereference too. But I can’t figure out what make this difference. Can you explain about this?
Thanks.
Note: When I changed execution order for tests, result is same. Even without warm-up, TestByPtrRef is always fastest.