Pop_back (swapback-esque) vs remove_at for arrays
What is this?
Arrays get slower to manipulate the bigger they are. In scenarios where an element must be removed from its array and the array needs to have its other items shifted down can get the cpu stuck at that task for some time. In cases where the order of the array elements do not matter, a swapback can be used. A swapback takes the item at the back of the array and puts it in the position of the element which is about to be removed, and then removes the array element at the back of the array. Thus, the array do not need to shift its elements, it only needs to move an element and remove a slot at the back. This should have performance benefits, and this benchmark tests that.
The array class in GDScript to not have a swapback method, but a swapback can be mimicked with a pop_back. It is possible that benchmark_2 could have been faster with a proper swapback method.
Benchmark
Overview
This benchmark instantiates scene node objects in the amount of the property ‘length’ into the array array and swapback_array. Benchmark_1 and Benchmark_2 runs the amount of times of benchmark_cycles and stores the nanoseconds into a dictionary so the sum of the runs can be found.
Settings
Benchmark cycles: 100
Array length: 1000
There are 1000 objects and the benchmark_1 & _2 are repeated 100 times. The results shown in ms is the sum of running benchmark_1 and benchmark_2 those 100 times.
Results
The benchmark itself is run 10 times to find an average.
benchmark_1: 0.072ms, benchmark_2: 0.016ms, difference: 0.056ms
benchmark_1: 0.096ms, benchmark_2: 0.013ms, difference: 0.083ms
benchmark_1: 0.081ms, benchmark_2: 0.013ms, difference: 0.068ms
benchmark_1: 0.08ms, benchmark_2: 0.008ms, difference: 0.072ms
benchmark_1: 0.093ms, benchmark_2: 0.011ms, difference: 0.082ms
benchmark_1: 0.076ms, benchmark_2: 0.011ms, difference: 0.065ms
benchmark_1: 0.081ms, benchmark_2: 0.013ms, difference: 0.068ms
benchmark_1: 0.081ms, benchmark_2: 0.004ms, difference: 0.077ms
benchmark_1: 0.081ms, benchmark_2: 0.004ms, difference: 0.077ms
benchmark_1: 0.081ms, benchmark_2: 0.012ms, difference: 0.069ms
(sum of difference) / (amount of benchmark runs) = average.
0.717ms / 10 = 0.0717ms.
Conclusion
In scenarios where the order of the array do not matter, using a swapback in an array of 1000 elements has between a 6 to 10 times performance benefit.
Code
extends Node2D
@export var benchmark_cycles: int = 100
@export var length: int = 1_000
var benchmark_1_results: Dictionary
var benchmark_2_results: Dictionary
var results: Dictionary
var array: Array
var swapback_array: Array
func _ready() -> void:
for i in length:
array.append(0)
swapback_array.append(0)
for i in benchmark_cycles:
benchmark_1_results[i] = benchmark_1(i)
benchmark_2_results[i] = benchmark_2(i)
measure_results()
func measure_results() -> void:
var sum_1: float = 0.0
var sum_2: float = 0.0
for key in benchmark_1_results:
sum_1 += benchmark_1_results[key]
for key in benchmark_2_results:
sum_2 += benchmark_2_results[key]
sum_1 = sum_1 / 1000
sum_2 = sum_2 / 1000
print("benchmark_1: " + str(sum_1) + "ms")
print("benchmark_2: " + str(sum_2) + "ms")
print("difference: " + str(sum_1 - sum_2) + "ms")
func benchmark_1(i) -> float:
var time_start_benchmark: float = Time.get_ticks_usec()
array.remove_at(i)
return Time.get_ticks_usec() - time_start_benchmark
func benchmark_2(i) -> float:
var time_start_benchmark: float = Time.get_ticks_usec()
swapback_array[i] = swapback_array.pop_back()
return Time.get_ticks_usec() - time_start_benchmark