DEV Community

Cover image for Your GDScript arrays are slower than you think? — I built a CLI that detects the pitfalls and benchmarks them
sunnydachs
sunnydachs

Posted on

Your GDScript arrays are slower than you think? — I built a CLI that detects the pitfalls and benchmarks them

"Why is my loop stuttering? I'm not even doing anything heavy here."

Every Godot tutorial tells you to just use arrays. Nobody tells you that an
untyped array holds Variants, that a sequential-key Dictionary is ~2x slower
than a plain Array, or that the official docs contradict themselves about
packed arrays. This is about gd-bench, a CLI I built that scans your
.gd files for array-type performance pitfalls and generates runnable Godot
micro-benchmarks for each one — with zero dependencies and no LLM anywhere.

https://github.com/sunnydachs/gd-bench

What it does

Point it at a Godot project. It scans every .gd file and reports where the
array-type choice is costing you performance, then writes a runnable
benchmark script per finding so you can measure before you rewrite.

pip install gd-bench

gd-bench .              # scan the current project
gd-bench . --gen-bench  # also generate Godot micro-benchmarks per finding
Enter fullscreen mode Exit fullscreen mode

Example output (a small demo project, 7 files):

game/main.gd:4  🐢 UNTYPED ARRAY
    var inventory = []            # untyped array — Variant soup
    ->
game/main.gd:5  🔧 TYPED ARRAY
    var scores: Array[int] = []   # typed array — fine but Packed is faster
    ->
game/main.gd:6   PACKED ARRAY
    var points: PackedVector2Array = PackedVector2Array()  # already optimal
    ->

summary: {"typed_array": 5, "dict_as_array": 2, "untyped_array": 1, "packed_array": 3}
Enter fullscreen mode Exit fullscreen mode

It reports 4 kinds of findings:

  • UNTYPED ARRAYvar x = [1, 2, 3] holds Variants; slowest to iterate
  • TYPED ARRAYArray[int] gives compile-time checks but is still Variant-backed; PackedInt32Array is faster for the same element type
  • PACKED ARRAY — contiguous memory, the fast path (reported as ✅)
  • DICT AS ARRAYvar d = {} filled with sequential keys is ~2x slower and uses about half the memory again versus a plain Array

A static type checker will never tell you which of these is actually slower
in your loop — and the official docs can't agree either. Only a
measurement can.

Why deliberately generate benchmarks instead of just linting

This was the core design decision. A linter could just say "use
PackedInt32Array here" and be done. But I deliberately built it as
scanner + benchmark generator instead. Three reasons:

  1. The docs are contradictory. The GDScript reference (still, as of issue #10300 being discussed) says packed arrays are slower than generic arrays; the class reference says they are faster. A tool that parrots either line would be wrong half the time. A benchmark settles it per call site.
  2. Performance advice without a measurement is folklore. The same operation (append + iterate) can flip the winner depending on element type and size. Generating the benchmark next to the finding makes "measure before you rewrite" the default workflow, not a discipline.
  3. Generated benchmarks are honest about what they test. Each script measures exactly two data structures doing exactly one operation — no framework, no warm-up noise, no statistical smoothing. If the number looks wrong, the script is 40 lines you can read in a minute.

Deterministic work deserves deterministic tools.

The rule: no rewrite without a benchmark run

The detail I obsessed over: the tool must never hand you a rewrite suggestion
it hasn't equipped you to verify. So the rule is:

  • Allowed: findings with a generated benchmark script next to them
  • Forbidden: "packed_array" findings get no benchmark — they're already the fast path, and benchmarking the optimal case just adds noise

That single line (書き換え前に必ず計測 / "always measure before rewriting")
is what keeps the tool from becoming another "just use X" voice. The
generated script is plain GDScript (extends SceneTree, runs headless) so
you can run it in CI or on a machine without the editor:

godot --headless --script gd-bench-out/bench_gd_untyped_array_4.gd
# gd-bench: untyped_array
#   untyped_array: 8231 usec
#   typed_array: 5120 usec
#   ratio: 1.61x
Enter fullscreen mode Exit fullscreen mode

Testing against real-world patterns

Not a toy example — the detection patterns come from the same shapes real
Godot projects and issues use:

  • 17 unit tests covering all 4 finding kinds, comment lines, non-sequential Dictionary keys (not flagged), and end-to-end CLI runs
  • The demo project above caught 8 findings across 5 files — including two DICT AS ARRAY cases that were doing 1000 sequential-key inserts per frame
  • The scanner correctly skipped its own generated benchmarks' comment lines and only flagged real declarations

The real win wasn't just "it works" — it's that the tool's advice and the
docs' advice can be compared in one command, which is exactly what the
godot-docs issue is asking for.

Honest limitations

  • Line-based detection. GDScript has no stable public AST, so this is regex-based pattern matching. It catches the common declaration shapes (var lines, {} + indexed assignment); it does not catch arrays built through multi-line expressions or passed as literals into function calls.
  • Benchmarks measure one operation. append + iterate-sum. Real frames do more (splice, sort, resize) — the generated script is a starting point, read it and extend it for your loop's actual shape.
  • No Godot required to scan, required to bench. The scanner runs anywhere Python runs; running the generated benchmark needs a Godot 4 binary (headless is fine).

Wrap-up

Frame time scales with your worst inner loop. Array-type choice is exactly
the class of problem where "scanner + generated measurement" earns its keep:
the advice is cheap to give, but only a benchmark tells you if it was true
for your code.

https://github.com/sunnydachs/gd-bench

This is a personal OSS project with no warranty. If you hit bugs or have
suggestions, GitHub issues are the best way to reach me.

Top comments (0)