Hello all,
Recently I have been spending a lot of time on improving my skills on theory of programming so I can better structure my code. During this research on the theory of programming I came across Clean Code (I’m a self-thought developer, so it took a little bit time to get there…) Anyway, the refactoring the code and extracting the methods until they get to a point about max.10-20 lines part is really surprised me since my previous knowledge on this matter is how much you rely on method calls will affect your performance since the process on cpu will make more calls to the address points related to these actions.
To test this argument by myself, I wrote a JavaScript application and run it on node.js,
const ITERATION_COUNT = 5000;
const SHOW_NUMBER_VERIFICATION = false;
class TestCallToClassMethod
{
constructor(num1, num2)
{
this.num = this.multiply(num1, num2);
}
multiply(num1, num2)
{
return num1 * num2;
}
}
function _TestCallToClassMethod()
{
var start = Date.now();
var num = 0;
for (var i = 0; i < ITERATION_COUNT; i++)
{
for (var j = 0; j < ITERATION_COUNT; j++)
{
var test = new TestCallToClassMethod(i, j);
num += test.num;
}
}
var end = Date.now();
var time = end-start;
if (SHOW_NUMBER_VERIFICATION)
console.log("Test Call To Class Method Number: " + num);
console.log("Test Call To Class Method Time: " + time);
}
_TestCallToClassMethod();
class TestCallToStaticClassMethod
{
constructor(num1, num2)
{
this.num = Math.Multiply(num1, num2);
}
}
class Math
{
static Multiply(num1, num2)
{
return num1 * num2;
}
}
function _TestCallToStaticClassMethod()
{
var start = Date.now();
var num = 0;
for (var i = 0; i < ITERATION_COUNT; i++)
{
for (var j = 0; j < ITERATION_COUNT; j++)
{
var test = new TestCallToStaticClassMethod(i, j);
num += test.num;
}
}
var end = Date.now();
var time = end-start;
if (SHOW_NUMBER_VERIFICATION)
console.log("Test Call To Static Class Method Number: " + num);
console.log("Test Call To Static Class Method Time: " + time);
}
_TestCallToStaticClassMethod();
class TestCallToConcreteClass
{
constructor(num1, num2)
{
var multiply = new Multiply(num1, num2);
this.num = multiply.num;
}
}
class Multiply
{
constructor(num1, num2)
{
this.num = num1 * num2;
}
}
function _TestCallToConcreteClass()
{
var start = Date.now();
var num = 0;
for (var i = 0; i < ITERATION_COUNT; i++)
{
for (var j = 0; j < ITERATION_COUNT; j++)
{
var test = new TestCallToConcreteClass(i, j);
num += test.num;
}
}
var end = Date.now();
var time = end-start;
if (SHOW_NUMBER_VERIFICATION)
console.log("Tell Call To Concrete Number: " + num);
console.log("Tell Call To Concrete Class: " + time);
}
_TestCallToConcreteClass();
function _TestInlineAction()
{
var start = Date.now();
var num = 0;
for (var i = 0; i < ITERATION_COUNT; i++)
{
for (var j = 0; j < ITERATION_COUNT; j++)
{
num += i * j;
}
}
var end = Date.now();
var time = end-start;
if (SHOW_NUMBER_VERIFICATION)
console.log("Test Inline Action Number: " + num);
console.log("Test Inline Action Time: " + time);
}
_TestInlineAction();
This script gave me the following results on my PC;
Test Call To Class Method Time: 42
Test Call To Static Class Method Time: 42
Tell Call To Concrete Class: 42
Test Inline Action Time: 37
And it really impressed me that calling other classes, instantiating objects and etc. didn’t affect the performance too much. But just to be sure I wanted to conduct a similar test on Unity with C# too. So I wrote the following script and attached it on a GameObject in a fresh scene that is also in a fresh Unity project.
using System;
using UnityEngine;
using UnityEngine.UI;
namespace PerformanceTest
{
public class TestingBehaviours : MonoBehaviour
{
[SerializeField] private Text _text;
private readonly int ITERATION_COUNT = 5000;
private readonly bool SHOW_NUMBER_VERIFICATION = false;
private bool _isTestInitiated = false;
private string _logText;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space) && !_isTestInitiated)
{
_isTestInitiated = true;
EvaluateTests();
}
}
private void EvaluateTests()
{
TestMethod(
"Test Call To Class Method",
(i, j) =>
{
return _TestCallToClassMethod(i, j);
});
TestMethod(
"Test Call To Concrete Class",
(i, j) =>
{
return _TestCallToConcreteClass(i, j);
});
TestMethod(
"Test Call To Static Class",
(i, j) =>
{
return _TestCallToStaticClass(i, j);
});
TestMethod(
"Test Inline Class",
(i, j) =>
{
return _TestInlineClass(i, j);
});
_MethodAction();
// ----- Start of Inline Action -----
long startTime = GetSystemTime();
ulong num = 0;
for (int i = 0; i < ITERATION_COUNT; i++)
{
for (int j = 0; j < ITERATION_COUNT; j++)
{
num += (ulong)(i * j);
}
}
long endTime = GetSystemTime();
long processTime = endTime - startTime;
WriteNumber("Inline Action Process Number", num);
WriteTime("Inline Action Process Time", processTime);
// ----- End of Inline Action -----
_text.text = _logText;
}
private void TestMethod(string logHeader, Func<int, int, ulong> testAction)
{
long startTime = GetSystemTime();
ulong num = 0;
for (int i = 0; i < ITERATION_COUNT; i++)
{
for (int j = 0; j < ITERATION_COUNT; j++)
{
num += testAction(i, j);
}
}
long endTime = GetSystemTime();
long processTime = endTime - startTime;
WriteNumber(logHeader + " Number", num);
WriteTime(logHeader + " Time", processTime);
}
private void _MethodAction()
{
long startTime = GetSystemTime();
ulong num = 0;
for (int i = 0; i < ITERATION_COUNT; i++)
{
for (int j = 0; j < ITERATION_COUNT; j++)
{
num += (ulong)(i * j);
}
}
long endTime = GetSystemTime();
long processTime = endTime - startTime;
WriteNumber("Method Action Process Number", num);
WriteTime("Method Action Process Time", processTime);
}
private ulong _TestCallToClassMethod(int i, int j)
{
var test = new TestCallToClassMethod(i, j);
return test.num;
}
private ulong _TestCallToConcreteClass(int i, int j)
{
var test = new TestCallToConcreteClass(i, j);
return test.num;
}
private ulong _TestCallToStaticClass(int i, int j)
{
var test = new TestCallToStaticClass(i, j);
return test.num;
}
private ulong _TestInlineClass(int i, int j)
{
var test = new TestInlineClass(i, j);
return test.num;
}
private void WriteNumber(string header, ulong num)
{
if (SHOW_NUMBER_VERIFICATION)
_logText += $"{header}: {num}\n";
}
private void WriteTime(string header, long time)
{
_logText += $"{header}: {time}\n";
}
private long GetSystemTime()
{
return DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
}
public class TestCallToClassMethod
{
public ulong num;
public TestCallToClassMethod(int num1, int num2)
{
num = Multiply(num1, num2);
}
private ulong Multiply(int num1, int num2)
{
return (ulong)(num1 * num2);
}
}
public class TestCallToConcreteClass
{
public ulong num;
public TestCallToConcreteClass(int num1, int num2)
{
Multiply multiply = new Multiply(num1, num2);
num = multiply.num;
}
}
public class Multiply
{
public ulong num;
public Multiply(int num1, int num2)
{
num = (ulong)(num1 * num2);
}
}
public class TestCallToStaticClass
{
public ulong num;
public TestCallToStaticClass(int num1, int num2)
{
num = Math.Multiply(num1, num2);
}
}
public static class Math
{
public static ulong Multiply(int num1, int num2)
{
return (ulong)(num1 * num2);
}
}
public class TestInlineClass
{
public ulong num;
public TestInlineClass(int num1, int num2)
{
num = (ulong)(num1 * num2);
}
}
}
And the results were really shocked me as you can check below
Test Call To Class Method Time: 2366
Test Call To Concrete Class Time: 4468
Test Call To Static Class Time: 2327
Test Inline Class Time: 2269
Method Action Process Time: 15
Inline Action Process Time: 15
I tested the code on two different Unity versions and also by building the project and testing on a standalone build, outcome of these different environments didn’t alter the results too much. While on JavaScript, instantiating new objects and making calls to static class methods weren’t making too much performance difference, on C# with Unity the performance affected by almost 150 to 300 times.
Do I oversight something in here with these tests? Or this interpreted JavaScript runtime beats the compiled (IL2CPP btw) C# code for real?