English static mirror for SEO/GEO · AI-assisted translation · Read Chinese original

graphics.gd FFI Performance Breakthrough: Go cgo Reaches 8 ns/op on Godot 4.7

Forum topic · ✨步子哥 · 2026-07-21

Summary

A deep-dive into the graphics.gd project (Discussion #277), a Go language binding for Godot via GDExtension, which reduced Go-to-Engine FFI overhead to 8.1–44 ns per operation. The gains combine a Go 1.26 runtime change that eliminates the _Psyscall state (cutting cgo overhead by roughly 30%), plus three project-level optimizations: a 256-slot ring buffer that batches main-thread void calls into a single cgo transition, an assembly 'jumponly' trampoline that calls leaf methods without entering cgo, and a refactored object representation using single-element arrays and phantom types to enable escape analysis. On arm64, a Go method call now measures 8.1 ns/op, faster than GDScript's 12.8 ns/op, and comparable to Rust gdext's cached builtin FFI (9–10 ns). The WebAssembly path remains 9–18x slower than desktop (786 ns for return values). The post also benchmarks the multi-language GDExtension ecosystem, notes graphics.gd's unique position as the only binding offering zero-SDK full-platform support including Web, and explains why the maintainer considers runtime stability—test coverage and sample projects—rather than API stability the main obstacle to a 1.0 release.

graphics.gd FFI Performance Breakthrough (Discussion #277)

Key points

  • graphics.gd (a Go GDExtension binding for Godot) has reduced Go→Engine FFI overhead to 8.1 ns/op (arm64 method call) with Go 1.26 plus all in-house optimizations — faster than GDScript's 12.8 ns/op.
  • On x86_64 with Go 1.25/1.26 and partial optimizations: MethodBind return = 44.17 ns, void = 23.30 ns (ring batch amortized) vs GDScript 23.10 ns.
  • The WebAssembly path remains 9–18x slower than desktop: return value 786.1 ns, void 209.2 ns, vs GDScript 50.62 ns.
  • The claim of being "ahead of safe Rust (gdext)" is qualitative; no comparable public ns/op numbers exist for gdext safe mode.
  • Benchmark numbers

    | Source | Hardware | MethodBind return | MethodBind void | GDScript | |---|---|---|---|---| | Post opening | x86_64 | 44.17 ns | 23.30 ns | 23.10 ns | | Post end | arm64 | 8.114 ns | 13.10 ns | 12.84 ns | | Web | wasm32 | 786.1 ns | 209.2 ns | 50.62 ns |

    Caveats:

  • The 23.30 ns void figure is amortized (tight loop with ring batching); isolated calls with return values approach 44 ns.
  • GDScript is a user-perceived latency floor, not the theoretical FFI limit (bare extern "C" is ~4–10 ns).
  • Go 1.26 cgo improvement

    A single commit (7244e92) by Michael Knyszek removes the _Psyscall state, eliminating an atomic Store and an atomic CAS on the syscall path:

    | Benchmark | Hardware | Before | After | Δ | |---|---|---|---|---| | CgoCall-64 | EPYC 7B13 | 43.69 | 35.83 | −17.99% | | CgoCall-8 | Apple M1 | 28.55 | 19.02 | −33.40% | | CgoCallWithCallback-8 | M1 | 72.76 | 57.38 | −21.14% | | Syscall-8 | M1 | 195.6 | 178.1 | −8.95% |

    The ~30% figure is a cross-hardware geometric mean. Historical context: Go 1.24 added #cgo noescape/nocallback; Go 1.26 delivers the _Psyscall elimination.

    The three-tier optimization stack

    Tiered by call hotness, all landed within ~10 days in February 2026:

    1. Leaf methods (~25% of API)jumponly.Call: assembly trampoline issues a direct CALL to the ptrcall function, skipping cgo entirely. Leaf detection via Clang LibTooling static analysis; virtual methods are excluded. (Commit b26bcf6, 667 files.) 2. Main-thread sequential void callsring.Main.Buffer+Flush: a 256-slot ring buffer packs arguments and executes batched calls with a single cgo transition. Flush triggers: buffer full, return value needed, or object Free. (Commit eb7eef4.) 3. Non-leaf, non-main-thread, return-value callsnoescape.Call + cgocall using //go:noescape to skip escape analysis.

    Coordination: jumponly checks ring.Main.Pending() before calling — flushes if a return value is needed, or routes via noescape to preserve batching for void calls.

    The object representation was also rebuilt around [1]gdreference.Object + pointers.Generic[T,S], letting escape analysis eliminate heap allocations with almost no API breakage.

    FFI cross-language comparison

    | Path | ns/op | |---|---| | Rust bare extern "C" | 4–10 | | gdext builtin_ffi (cached) | 9–10 | | Go 1.26 cgo (arm64, fully optimized) | 8.1 | | GDScript call | 12.8 | | gdext utilities_ffi (cached) | 30–32 | | graphics.gd MethodBind return | 44.17 | | gdext "safe mode" (estimated) | ~50–100+ |

    Go 1.26 cgo is in the same order of magnitude as Rust gdext's cached FFI.

    WebAssembly path

  • Uses a direct bridge: Go exposes //go:wasmexport gd_on_* entry points, avoiding JS trampolines, value boxing, and 64-bit splitting. No pre-bridge baseline was published, so the net benefit cannot be quantified.
  • GOOS=js GOARCH=wasm lacks full cgo, so graphics.gd uses a generated ABI shim.
  • Thread safety on web (commit c83a441) distinguishes frame goroutines from free goroutines under a single OS thread.

Ecosystem context

| Binding | Language | Stars | Web support | Godot | |---|---|---|---|---| | godot-cpp | C++ | official | full | 4.3–4.6 | | gdext | Rust | 4,440+ | experimental | 4.6–4.8 | | Godot C# | C# | official | none | 4.x | | SwiftGodot | Swift | 1,700 | none | 4.4 | | graphics.gd | Go | ~790 | yes | 4.7.0 | | GodotJS | JS | 1,059 | — | 4.1 |

graphics.gd's unique position: the only binding combining zero-SDK full-platform support (including Web/Android, cross-compiling via zig cc), native binaries without .NET runtime, and strongly typed RIDs. Risks: API drift before 1.0, high bus factor (essentially one maintainer, Splizard), and sparse sample projects.

Path to 1.0

The maintainer distinguishes two kinds of stability:

1. API Stability — driven by Godot minor upgrades (5–10 breakages per minor on average, 1–2 hitting GDExtension users; e.g., JSONRPC.set_scope rename in 4.5, OpenXRExtensionWrapper.xr_version in 4.6). graphics.gd refuses major-version import path suffixes. 2. Runtime Stability — semantic version tags alone don't help; only test coverage (currently 47 test cases) and sample project completeness matter.

Conclusion: the main blocker to 1.0 is runtime stability (test coverage and sample projects), not API stability. Recent commit activity focuses on Godot 4.7 adaptation, multi-platform CI, thread safety, and reproducible builds — release-grade polish, but with no explicit 1.0 announcement yet.

Tags

#godot#gdextension#go#cgo#ffi#graphics-gd#webassembly#performance

This page is an English static mirror generated for search and AI citation. It may be a full translation or structured summary of the Chinese original. Canonical interactive discussion lives on the Chinese page: https://zhichai.net/topic/178446975