Optimizing GDScript code

intermediate

By: François Bélair

Some code optimizations are universal across most programming languages. Some are specific to GDScript.

In this guide, we share some general tips and GDScript-specific techniques to improve your code’s execution speed in Godot 3.2.

Cache expensive calls ahead of time

Suppose you have an expression that always returns the same value. In that case, you should execute it ahead of time and store the result in a variable. A prime example in Godot is get_node() or its shorthand $. If there is no need to call get_node() more than once per node. It has a cost, and repeating it throughout a single frame is wasteful.

Avoid:

var direction

func _process(delta):
    var turret = $Turret

    direction = Vector2.UP.rotated(turret.rotation)

Prefer:

var direction

onready var turret = $Turret

func _process(delta):
    direction = Vector2.UP.rotated(turret.rotation)

Then, there is also no need to do more math than you need to. Cache the results of values that will stay constant throughout a loop before entering the loop.

# Slow
var alloy_strength = 1.0
var alloy_thickness = 5.0
var alloy_layers = 15.0

for robot in army:
    robot.armor = pow(alloy_strength * alloy_thickness, alloy_layers) + robot.native_armor


# Faster
var alloy_strength = 1.0
var alloy_thickness = 5.0
var alloy_layers = 15.0

# Here, we're calculating the base armor rating of robots outside of the loop
var base_armor = pow(alloy_strength * alloy_thickness, alloy_layers)

for robot in army:
    robot.armor = base_armor + robot.native_armor

1D array over an array of arrays

In small arrays that are not accessed often, iterating over nested arrays over a single array is negligible. But for large sets of data or arrays you access frequently, the time difference adds up.

# 0.115443 seconds
for x in 1000:
    for y in 1000:
        var element = my_array[y][x]


# 0.108107 seconds
for x in 1000:
    for y in 1000:
        var element = my_array[y * 1000 + x]


# 0.062938 seconds, about 45% faster than the first example
for i in 1000000:
    var element = my_array[i]

The most interesting part comes when using Godot’s native iterator instead of using indices to access an array’s elements. Both 1D and 2D arrays get similar, superior speeds with code like this:

# 0.048952 seconds, almost 60% faster than the first example
for x in 1000:
    for element in my_array[x]:
        #...

# 0.047986 seconds
for element in my_array:
    #...

Of course, you do not always have the luxury of accessing elements in any order. Sometimes, you need to know the position of the element in the array for more processing. But it’s a tradeoff to keep in mind.

If you want high performing arrays in GDScript, the main takeaways are:

  • 1D arrays are faster than multi-dimensional arrays
  • Single loops are faster than nested loops
  • Accessing an array element with the iterator for element in array is faster than using indices, e.g. for i in array.size().

Remove elements from the back of the array

Whenever you add or remove an element at a given position within an array, Godot has to resize the array and move all its elements. The cost of this operation is proportional to the number of entries in the array. When the removed element is the last one, the engine only needs to resize the array, discarding the last value.

Favor pop_back() and push_back() or append() when removing or adding elements to an array. Avoid pop_front() and push_front().

Use the correct data structure and algorithm for the job

Choosing the right data structure is not a GDScript-specific issue, but the data structures you use affects all algorithms.

Here are three rules of thumb for choosing data structures with GDScript:

  • If you want to remove elements from a collection at any location and at any time, use a Dictionary.
  • If you will access elements from a collection by key randomly, use a Dictionary.
  • If you lay your elements in order, use an Array.

For more information, read the Data preferences page in the official manual.

Use breakpoints instead of printing

Calling the print() function is expensive. You don’t want to export your games with many print statements left in.

Instead, you can use the built-in debugger to inspect your game at runtime. Another option is to call print_debug(), which only runs in your game’s debug builds or when testing it from the editor.

It can have a significant impact on the framerate at no benefit to the end-user.

If you want to read a large dump of text or data, which is useful to analyze errors that are hard to track down, output text to a log file. You can do so by creating an Autoload dedicated to logging:

extends Node
onready var log = File.new()


func _ready():
    log.open("res://log.txt", File.WRITE)


func _exit_tree():
    log.close()


func print_msg(message, source):
    var current_time = OS.get_time()
    log.write_line("%s (%s:%s:%s): %s" % [source, current_time.hour, current_time.minute, current_time.second, message])

Favor iteration over recursion

Every time you call a function, the compiler creates an object to store some information about it. One piece of information is which function called the current one, allowing the compiler to return to the caller. Recursion, that is to say, having a function call itself, causes these objects to stack up.

Recursion can be powerful and lead to code that’s easy to read. For small recursion loops, the performance impact is negligible. You shouldn’t optimize them away just because you are using recursion. But if you are looking to get more optimal code, consider converting recursive functions to a loop to avoid excessive calls.

# Loop over nodes recursively to find one of a given type.
# This is a recursive function as it calls itself.
func find_by_type_name_recursive(parent, type_name):
    for child in parent.get_children():
        if child.get_class() == type_name:
            return child
        else:
            var result = find_by_type_name_recursive(child, type_name)
            if result:
                return result

    return null

# Same as the previous function but using a while loop.
# The deeper the scene tree, the higher the performance gain.
func find_by_type_name(parent, type_name):
    var parent_stack = [parent]

    while parent_stack.size() > 0:
        var current = parent_stack.pop_back()

        if current.get_class() == type_name:
            return current

        for child in current.get_children():
            parent_stack.push_back(child)

    return null

Avoid loops of function calls

Function calls are slower than instructions as the compiler needs to store some state for them. Instead of calling a function many times in a loop, putting the loop inside a single function is significantly faster.

var tiles = []
for i in range(1000):
    # We're calling `create_random_tile` one thousand times
    var new_tile = create_random_tile()
    tiles.append(new_tile)


func create_random_tile():
    #...

Is going to be much slower than:

# Here, we only have one function call
var tiles = create_random_tiles()


func create_random_tiles():
    var tiles = []
    for i in range(1000):
        # ...
    return tiles

Conditional chains are 20% faster than match

You can use the match keyword as an equivalent of chains of if, elif, else statements. They can look a bit like case statements in some languages. Currently though, match is a little slower than if for equivalent code. In my tests, the speed difference was about 15% to 20%.

match student.eye_color:
    "Green":
        #...
    "Blue":
        #...
    "Brown":
        #...
    "Black":
        #...
    _:
        #...

# About 20% faster than the code above.
if student.eye_color == "Green":
    #...
elif student.eye_color == "Blue":
    #...
elif student.eye_color == "Brown":
    #...
elif student.eye_color == "Black":
    #...
else:
    #...


Leave loops and functions as soon as possible

Use the return keyword in functions or continue and break in loops to skip instructions you don’t need to run.

As soon as you have found the value you were looking, try to leave it as soon as possible.

func get_inventory_from(container):
    if not container.inventory:
        return null

    var inventory_array = []
    for block in container.inventory:
        inventory_array.append(block.get_slots())

    if inventory_array.size() == 0:
        return null

    return parse_inventory(inventory_array)

Micro-optimizations

You have some optimizations that cause large differences, like removing the elements from the back of an array or caching expensive results. Then, you have the little things you probably shouldn’t sweat but can be good practices. Gains here are much smaller than with previous optimization tips.

Here are three examples:

  1. If you have a or or and boolean operation, try to put conditions that are more likely to return true on the left side of or, and conditions that are more likely to return false on the left side of and.
  2. In conditional chains or match statements, try to put the candidates that appear more commonly near the chain’s top.
  3. Rephrase math to remove expensive operations. Compare distance_squared_to instead of distance_to to avoid a square root operation. If you divide by a value more than three times, pre-calculate that and store it in a variable.

Use GDNative

Even with all the optimization you try to add, GDScript might fail you with performance-intensive algorithms, like heavy procedural generation or heatmap pathfinding.

If this happens, you will want to look into using either C# or C++ GDNative libraries.

Become a better game developer

Join our weekly newsletter and get our latest game creation tutorials, tips, and open-source tools right in your inbox.

🔒 No spam. Unsubscribe anytime.

Made by

Our tutorials are the result of careful teamwork to ensure we produce high quality content. The following team members worked on this one:

François Bélair

Senior Developer

Nathan Lovato

Founder