Performance Guide - goffi v0.4.1

July 5, 2026 · View on GitHub

Comprehensive performance analysis, benchmarks, and usage guidelines Platform: Windows AMD64, 12th Gen Intel Core i7-1255U Go Version: 1.25+


TL;DR - Quick Summary

FFI Overhead: ~88-114 ns/op ✅ Acceptable for: WebGPU, system calls, I/O, GPU operations ❌ NOT acceptable for: Tight loops, hot-path math, high-frequency calls (>100K/sec)

Comparison:

  • goffi: ~100 ns/op overhead
  • CGO: ~140-170 ns/op (Go 1.26 reduced overhead ~30%)
  • purego: ~100-150 ns/op (similar approach)
  • Direct Go: ~0.2 ns/op (baseline)

Verdict: goffi is production-ready for WebGPU and similar use cases where function calls are rare (< 10K/sec) and expensive (> 1µs each).


Benchmark Results

1. FFI Call Overhead

Benchmarkns/opB/opallocs/opNotes
BenchmarkGoffiOverhead88.09642Empty C function (getpid)
BenchmarkGoffiIntArgs113.9723Integer argument (abs)
BenchmarkGoffiStringOutput97.81723String processing (strlen)
BenchmarkDirectGo0.2100Pure Go baseline

Key Insights:

  • Minimum FFI overhead: ~88 ns (empty function)
  • Typical overhead: ~100-115 ns (with arguments)
  • Overhead ratio: ~400-500x vs direct Go call
  • Allocations: 0 in steady state. syscallArgs is heap-allocated via sync.Pool for callback safety (goroutine stack may move during C→Go callbacks). Pool reuse eliminates per-call allocations after warmup.

2. One-Time Costs

Operationns/opB/opallocs/opFrequency
LoadLibrary607.8483Once per library
GetSymbol318.1402Once per function
PrepareCallInterface63.94241Once per function signature

Key Insights:

  • Library loading: ~600 ns (amortize over thousands of calls)
  • Symbol lookup: ~300 ns (cache function pointers)
  • CIF preparation: ~64 ns (reuse CallInterface objects)

3. Platform-Specific

Windows AMD64 (tested):

  • Win64 calling convention (RCX, RDX, R8, R9 + 32-byte shadow space)
  • kernel32.dll: 607.8 ns load time
  • msvcrt.dll: similar

Linux AMD64 (expected):

  • System V AMD64 ABI (RDI, RSI, RDX, RCX, R8, R9)
  • libc.so.6: ~400-600 ns load time (faster dlopen)
  • Similar FFI overhead (~100-120 ns)

Performance Analysis

Overhead Breakdown

Total FFI call time: ~100 ns
├── runtime.cgocall:     ~60 ns  (stack switch, GC coordination)
├── Assembly wrapper:    ~20 ns  (register loads, MOVQ/MOVSD)
├── JMP stub:            ~5 ns   (indirect jump)
├── Return path:         ~10 ns  (stack restore)
└── Bookkeeping:         ~5 ns   (error handling, Go overhead)

Why is it acceptable for WebGPU?

Typical WebGPU operation costs:

wgpuDeviceCreateBuffer():    1-10 µs   (GPU allocation)
wgpuQueueSubmit():           10-100 µs (GPU dispatch)
wgpuRenderPassEncoderDraw(): 0.5-5 µs  (GPU command)

FFI overhead: 100 ns = 0.1 µs

Overhead percentage:
- Fast GPU call (0.5 µs): 100ns / 500ns = 20% overhead (acceptable!)
- Typical GPU call (5 µs): 100ns / 5000ns = 2% overhead (excellent!)
- Batch operation (100 µs): 100ns / 100000ns = 0.1% overhead (negligible!)

Conclusion: For GPU operations, FFI overhead is noise-level (< 5% impact).

When NOT to use goffi

Tight loops with many calls:

// ❌ BAD: 1 million math calls = 100ms overhead!
for i := 0; i < 1_000_000; i++ {
    result := libm.Call("sin", x)  // 100ns × 1M = 100ms
}

// ✅ GOOD: Batch processing or use math.Sin()
result := math.Sin(x)  // Pure Go, 0.2ns

Hot-path math libraries:

// ❌ BAD: FFI for every pixel
for y := 0; y < 1080; y++ {
    for x := 0; x < 1920; x++ {
        pixel := libimage.Call("process", x, y)  // 2M calls!
    }
}

// ✅ GOOD: Batch entire frame
pixels := libimage.Call("process_frame", frameBuffer)  // 1 call!

High-frequency polling:

// ❌ BAD: 10K polls/sec = 1ms/sec = 0.1% CPU just for FFI
ticker := time.NewTicker(100 * time.Microsecond)
for range ticker.C {
    status := hw.Call("poll_status")  // Every 100µs
}

// ✅ GOOD: Batch or use Go channels
events := hw.Call("get_events_batch")  // Get all events at once

Optimization Strategies

1. Amortize One-Time Costs

// ✅ GOOD: Load once, call many times
var (
    handle   unsafe.Pointer
    funcPtr  unsafe.Pointer
    cif      types.CallInterface
)

func init() {
    handle, _ = ffi.LoadLibrary("mylib.dll")
    funcPtr, _ = ffi.GetSymbol(handle, "myFunction")
    ffi.PrepareCallInterface(&cif, types.DefaultCall, ...)
}

// Now each call is just ~100ns overhead
func CallMyFunction(arg int) {
    ffi.CallFunction(&cif, funcPtr, &result, args)
}

2. Batch Operations

// ❌ BAD: N FFI calls
for _, item := range items {
    Process(item)  // 100ns × N
}

// ✅ GOOD: 1 FFI call
ProcessBatch(items)  // 100ns × 1

3. Cache Results

// ✅ Cache expensive computations
var cache = make(map[Key]Result)

func GetResult(key Key) Result {
    if result, ok := cache[key]; ok {
        return result  // 0.2ns (map lookup)
    }
    result := FFIExpensiveCall(key)  // 100ns + C cost
    cache[key] = result
    return result
}

4. Use Go When Possible

// ❌ FFI for simple math
result := libm.Call("sin", x)  // ~100ns + C sin (~10ns) = 110ns

// ✅ Pure Go
result := math.Sin(x)  // ~10-20ns (similar to C!)

Real-World Performance Examples

WebGPU Frame Rendering (Target: 60 FPS = 16.6ms/frame)

Typical frame with goffi:

wgpuQueueSubmit():              100 µs (GPU work)
wgpuRenderPassEncoderDraw(): ×10 = 50 µs (draw calls)
wgpuDeviceCreateBuffer(): ×3   = 15 µs (buffer creation)
Other GPU calls: ×20          = 100 µs
FFI overhead: 33 calls × 0.1µs = 3.3 µs

Total: 268.3 µs per frame
FFI overhead: 3.3µs / 268.3µs = 1.2% ✅

Verdict: goffi overhead is negligible for WebGPU rendering (< 2% impact).

System Call Monitoring (1000 calls/sec)

System calls per second: 1000
FFI overhead per call: 100 ns
Total overhead per second: 1000 × 100ns = 0.1ms = 0.01% CPU ✅

Verdict: Acceptable for monitoring, logging, system integration.

Database Query (10 queries/sec)

Query execution time: ~10ms (typical)
FFI overhead: 0.0001ms = 0.001% ✅

Verdict: FFI overhead is unmeasurable for I/O-bound operations.


Comparison with Alternatives

goffi vs CGO

AspectgoffiCGO
Overhead~100 ns~140-170 ns (Go 1.26)
BuildZero depsRequires C compiler
Cross-compile✅ Easy❌ Complex
Static binary✅ Yes⚠️ Often requires libc

Note: Go 1.26 (Feb 2026) reduced CGO overhead ~30% by removing the dedicated syscall P state. goffi benefits from the same improvement — both use runtime.cgocall internally.

goffi vs purego

Aspectgoffipurego
Overhead~100 nsNot published
Per-call allocationsZero (CIF reused)reflect dispatch + sync.Pool per call
Type Safety✅ TypeDescriptor validationGo reflect.Type
Error Handling✅ 5 typed errorsGeneric errors
Callback float returns✅ XMM0 in asm❌ panic
Struct return 9-16B✅ 4 modes (RAX/XMM × RAX/XMM)✅ 4 modes (f1/f2 + a1/a2)
Callback struct args✅ ≤8B, 9-16B, >16B❌ panic
ARM64 HFARecursive struct walkPartial recursive (bug in nested path)
Context support✅ Timeouts/cancellation
Platforms5 (quality focus)9+ (breadth focus)

Go 1.26 CGO Improvements

Go 1.26 (released February 2026) reduced cgo call overhead by ~30% by removing the dedicated syscall P state. Benchmarks on Apple M1 show CgoCall is 33% faster, CgoCallWithCallback is 21% faster.

What this means for goffi:

  • goffi benefits too — our runtime.cgocall path gets the same ~30% speedup, because goffi uses the same Go runtime machinery internally
  • CGO still requires a C compiler at build time — goffi does not
  • Cross-compilation with CGO still requires cross-toolchains — GOOS=linux GOARCH=arm64 go build just works with goffi
  • Static binaries — CGO often pulls in libc, goffi produces fully static Go binaries

The gap between CGO and pure-Go FFI is narrowing from both directions. We welcome it.


Performance Roadmap

v0.5.0 - Usability + Optimization

  • Builder pattern API (less boilerplate)
  • Variadic function support
  • Assembly micro-optimizations

v1.0.0 - Production Benchmarks

  • Comprehensive benchmarks vs CGO/purego (published)
  • Platform-specific tuning (Linux, macOS, ARM64)
  • Real-world case studies (WebGPU, Vulkan)

Troubleshooting

My app is slow with goffi!

Check 1: How many FFI calls per second?

// Add timing
start := time.Now()
for i := 0; i < 10000; i++ {
    YourFFICall()
}
fmt.Printf("Calls/sec: %d\n", 10000 / time.Since(start).Seconds())

// If > 100K calls/sec → Consider batching or Go alternative

Check 2: Are you recreating CIF every call?

// ❌ BAD: Prepare CIF in loop
for _, item := range items {
    cif := &types.CallInterface{}
    ffi.PrepareCallInterface(cif, ...)  // 64ns × N!
    ffi.CallFunction(cif, ...)
}

// ✅ GOOD: Prepare once
cif := &types.CallInterface{}
ffi.PrepareCallInterface(cif, ...)
for _, item := range items {
    ffi.CallFunction(cif, ...)  // Just ~100ns
}

Check 3: Is the C function itself slow?

// Measure C function cost
start := time.Now()
ffi.CallFunction(cif, fn, ...)
fmt.Printf("Total: %v\n", time.Since(start))
// If > 10µs, the C function is slow, not goffi!

Benchmarking Your Code

# Run goffi benchmarks
cd ffi && go test -bench=. -benchmem -benchtime=1s

# Profile your application
go test -bench=YourBenchmark -cpuprofile=cpu.prof
go tool pprof cpu.prof

# Compare before/after
go test -bench=. -benchmem > before.txt
# Make changes
go test -bench=. -benchmem > after.txt
benchstat before.txt after.txt

Conclusion

goffi is production-ready for:

  • ✅ WebGPU bindings (primary use case)
  • ✅ GPU computing (CUDA, Vulkan, DirectX)
  • ✅ System library integration (I/O, networking)
  • ✅ Embedded applications (sensors, hardware)
  • ✅ Legacy library integration (scientific, financial)

NOT recommended for:

  • ❌ Tight loops (millions of calls)
  • ❌ Hot-path math (use math package)
  • ❌ High-frequency polling (> 100K calls/sec)

Performance: ~100 ns overhead = < 5% impact for typical WebGPU/GPU workloads.


Benchmarks conducted on Windows AMD64, Intel i7-1255U @ 12 cores Your results may vary depending on CPU, OS, and workload Last updated: 2026-03-02 | goffi v0.4.1