Skip to content

Commit 2fe737a

Browse files
authored
purego: add benchmarks for calling methods (#363)
Add benchmarks comparing SyscallN, RegisterFunc, and callback performance across different argument counts. This helps measure and compare the overhead of different calling approaches in purego's function invocation system. Closes #362
1 parent 49bede1 commit 2fe737a

2 files changed

Lines changed: 356 additions & 0 deletions

File tree

syscall_bench_test.go

Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+
package purego_test
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/ebitengine/purego"
13+
"github.com/ebitengine/purego/internal/load"
14+
)
15+
16+
// BenchmarkCallingMethods compares RegisterFunc, SyscallN, and Callback methods
17+
func BenchmarkCallingMethods(b *testing.B) {
18+
testCases := []struct {
19+
n int
20+
goFn any
21+
goFnPtr uintptr
22+
cFnPtr uintptr
23+
cFnName string
24+
cCallbackPtr uintptr
25+
cCallbackName string
26+
args []int64
27+
expectedSum int64
28+
}{
29+
{1, goSum1, 0, 0, "sum1_c", 0, "call_callback1", []int64{1}, 1},
30+
{2, goSum2, 0, 0, "sum2_c", 0, "call_callback2", []int64{1, 2}, 3},
31+
{3, goSum3, 0, 0, "sum3_c", 0, "call_callback3", []int64{1, 2, 3}, 6},
32+
{5, goSum5, 0, 0, "sum5_c", 0, "call_callback5", []int64{1, 2, 3, 4, 5}, 15},
33+
{10, goSum10, 0, 0, "sum10_c", 0, "call_callback10", []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, 55},
34+
{14, goSum15, 0, 0, "sum14_c", 0, "call_callback14", []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}, 105},
35+
{15, goSum15, 0, 0, "sum15_c", 0, "call_callback15", []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, 120},
36+
}
37+
38+
// Build C library for benchmarking
39+
libFileName := filepath.Join(b.TempDir(), "libbenchmark.so")
40+
if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "benchmarktest", "benchmark.c")); err != nil {
41+
b.Fatalf("Failed to build C library: %v", err)
42+
}
43+
b.Cleanup(func() {
44+
os.Remove(libFileName)
45+
})
46+
47+
libHandle, err := load.OpenLibrary(libFileName)
48+
if err != nil {
49+
b.Fatalf("Failed to load C library: %v", err)
50+
}
51+
b.Cleanup(func() {
52+
if err := load.CloseLibrary(libHandle); err != nil {
53+
b.Fatalf("Failed to close library: %s", err)
54+
}
55+
})
56+
57+
// Create callbacks and load C functions
58+
for i := range testCases {
59+
testCases[i].goFnPtr = purego.NewCallback(testCases[i].goFn)
60+
61+
cFn, err := load.OpenSymbol(libHandle, testCases[i].cFnName)
62+
if err != nil {
63+
b.Fatalf("Failed to load C function %s: %v", testCases[i].cFnName, err)
64+
}
65+
testCases[i].cFnPtr = cFn
66+
67+
cCallbackFn, err := load.OpenSymbol(libHandle, testCases[i].cCallbackName)
68+
if err != nil {
69+
b.Fatalf("Failed to load C callback wrapper %s: %v", testCases[i].cCallbackName, err)
70+
}
71+
testCases[i].cCallbackPtr = cCallbackFn
72+
}
73+
74+
b.Run("RegisterFunc/Callback", func(b *testing.B) {
75+
for _, tc := range testCases {
76+
b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
77+
b.ReportAllocs()
78+
registerFn := makeRegisterFunc(tc.n)
79+
purego.RegisterFunc(registerFn, tc.goFnPtr)
80+
81+
b.ResetTimer()
82+
result := callRegisterFunc(registerFn, tc.n, tc.args, b.N)
83+
b.StopTimer()
84+
85+
if result != tc.expectedSum {
86+
b.Fatalf("RegisterFunc/Callback: expected sum %d, got %d", tc.expectedSum, result)
87+
}
88+
})
89+
}
90+
})
91+
92+
// Benchmark RegisterFunc with C functions
93+
b.Run("RegisterFunc/CFunc", func(b *testing.B) {
94+
for _, tc := range testCases {
95+
b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
96+
b.ReportAllocs()
97+
registerFn := makeRegisterFunc(tc.n)
98+
purego.RegisterFunc(registerFn, tc.cFnPtr)
99+
100+
b.ResetTimer()
101+
result := callRegisterFunc(registerFn, tc.n, tc.args, b.N)
102+
b.StopTimer()
103+
104+
if result != tc.expectedSum {
105+
b.Fatalf("RegisterFunc/CFunc: expected sum %d, got %d", tc.expectedSum, result)
106+
}
107+
})
108+
}
109+
})
110+
111+
// Benchmark SyscallN with Go callbacks
112+
b.Run("SyscallN/Callback", func(b *testing.B) {
113+
for _, tc := range testCases {
114+
b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
115+
b.ReportAllocs()
116+
args := int64sToUintptrs(tc.args)
117+
var result uintptr
118+
b.ResetTimer()
119+
for i := 0; i < b.N; i++ {
120+
result, _, _ = purego.SyscallN(tc.goFnPtr, args...)
121+
}
122+
b.StopTimer()
123+
if int64(result) != tc.expectedSum {
124+
b.Fatalf("SyscallN/Callback: expected sum %d, got %d", tc.expectedSum, result)
125+
}
126+
})
127+
}
128+
})
129+
130+
// Benchmark SyscallN with C functions
131+
b.Run("SyscallN/CFunc", func(b *testing.B) {
132+
for _, tc := range testCases {
133+
b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
134+
b.ReportAllocs()
135+
args := int64sToUintptrs(tc.args)
136+
var result uintptr
137+
b.ResetTimer()
138+
for i := 0; i < b.N; i++ {
139+
result, _, _ = purego.SyscallN(tc.cFnPtr, args...)
140+
}
141+
b.StopTimer()
142+
if int64(result) != tc.expectedSum {
143+
b.Fatalf("SyscallN/CFunc: expected sum %d, got %d", tc.expectedSum, result)
144+
}
145+
})
146+
}
147+
})
148+
149+
// Benchmark round-trip: Go → C → Go callback (realistic use case)
150+
b.Run("RoundTrip/GoC", func(b *testing.B) {
151+
for _, tc := range testCases {
152+
b.Run(fmt.Sprintf("%dargs", tc.n), func(b *testing.B) {
153+
b.ReportAllocs()
154+
// Build args: first arg is callback pointer, rest are the arguments
155+
args := int64sToUintptrs(tc.args)
156+
callbackArgs := make([]uintptr, len(args)+1)
157+
callbackArgs[0] = tc.goFnPtr
158+
copy(callbackArgs[1:], args)
159+
160+
// Skip if total args (callback + args) exceeds or meets limit
161+
// SyscallN has issues with exactly 15 or more arguments
162+
if len(callbackArgs) >= 15 {
163+
b.Skipf("Round-trip with %d args + callback (%d total) exceeds/meets SyscallN limit", tc.n, len(callbackArgs))
164+
}
165+
166+
var result uintptr
167+
b.ResetTimer()
168+
for i := 0; i < b.N; i++ {
169+
result, _, _ = purego.SyscallN(tc.cCallbackPtr, callbackArgs...)
170+
}
171+
b.StopTimer()
172+
if int64(result) != tc.expectedSum {
173+
b.Fatalf("RoundTrip: expected sum %d, got %d", tc.expectedSum, result)
174+
}
175+
})
176+
}
177+
})
178+
}
179+
180+
// makeRegisterFunc creates a function pointer of the appropriate signature
181+
func makeRegisterFunc(n int) any {
182+
switch n {
183+
case 1:
184+
return new(func(int64) int64)
185+
case 2:
186+
return new(func(int64, int64) int64)
187+
case 3:
188+
return new(func(int64, int64, int64) int64)
189+
case 5:
190+
return new(func(int64, int64, int64, int64, int64) int64)
191+
case 10:
192+
return new(func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
193+
case 14:
194+
return new(func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
195+
case 15:
196+
return new(func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
197+
default:
198+
panic(fmt.Sprintf("unsupported arg count: %d", n))
199+
}
200+
}
201+
202+
// callRegisterFunc calls the registered function with the appropriate number of arguments
203+
func callRegisterFunc(registerFn any, n int, args []int64, iterations int) int64 {
204+
var result int64
205+
switch n {
206+
case 1:
207+
f := registerFn.(*func(int64) int64)
208+
for i := 0; i < iterations; i++ {
209+
result = (*f)(args[0])
210+
}
211+
case 2:
212+
f := registerFn.(*func(int64, int64) int64)
213+
for i := 0; i < iterations; i++ {
214+
result = (*f)(args[0], args[1])
215+
}
216+
case 3:
217+
f := registerFn.(*func(int64, int64, int64) int64)
218+
for i := 0; i < iterations; i++ {
219+
result = (*f)(args[0], args[1], args[2])
220+
}
221+
case 5:
222+
f := registerFn.(*func(int64, int64, int64, int64, int64) int64)
223+
for i := 0; i < iterations; i++ {
224+
result = (*f)(args[0], args[1], args[2], args[3], args[4])
225+
}
226+
case 10:
227+
f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
228+
for i := 0; i < iterations; i++ {
229+
result = (*f)(args[0], args[1], args[2], args[3], args[4],
230+
args[5], args[6], args[7], args[8], args[9])
231+
}
232+
case 14:
233+
f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
234+
for i := 0; i < iterations; i++ {
235+
result = (*f)(args[0], args[1], args[2], args[3], args[4],
236+
args[5], args[6], args[7], args[8], args[9],
237+
args[10], args[11], args[12], args[13])
238+
}
239+
case 15:
240+
f := registerFn.(*func(int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64) int64)
241+
for i := 0; i < iterations; i++ {
242+
result = (*f)(args[0], args[1], args[2], args[3], args[4],
243+
args[5], args[6], args[7], args[8], args[9],
244+
args[10], args[11], args[12], args[13], args[14])
245+
}
246+
default:
247+
panic(fmt.Sprintf("unsupported arg count: %d", n))
248+
}
249+
return result
250+
}
251+
252+
// int64sToUintptrs converts []int64 to []uintptr for SyscallN
253+
func int64sToUintptrs(args []int64) []uintptr {
254+
result := make([]uintptr, len(args))
255+
for i, v := range args {
256+
result[i] = uintptr(v)
257+
}
258+
return result
259+
}
260+
261+
func goSum1(a1 int64) int64 { return a1 }
262+
263+
func goSum2(a1, a2 int64) int64 { return a1 + a2 }
264+
265+
func goSum3(a1, a2, a3 int64) int64 { return a1 + a2 + a3 }
266+
267+
func goSum5(a1, a2, a3, a4, a5 int64) int64 { return a1 + a2 + a3 + a4 + a5 }
268+
269+
func goSum10(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10 int64) int64 {
270+
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10
271+
}
272+
273+
func goSum15(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 int64) int64 {
274+
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15
275+
}

testdata/benchmarktest/benchmark.c

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+
#include <stdint.h>
5+
6+
int64_t sum1_c(int64_t a1) { return a1; }
7+
8+
int64_t sum2_c(int64_t a1, int64_t a2) { return a1 + a2; }
9+
10+
int64_t sum3_c(int64_t a1, int64_t a2, int64_t a3) { return a1 + a2 + a3; }
11+
12+
int64_t sum5_c(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5) {
13+
return a1 + a2 + a3 + a4 + a5;
14+
}
15+
16+
int64_t sum10_c(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5,
17+
int64_t a6, int64_t a7, int64_t a8, int64_t a9, int64_t a10) {
18+
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10;
19+
}
20+
21+
int64_t sum14_c(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5,
22+
int64_t a6, int64_t a7, int64_t a8, int64_t a9, int64_t a10,
23+
int64_t a11, int64_t a12, int64_t a13, int64_t a14) {
24+
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 +
25+
a14;
26+
}
27+
28+
int64_t sum15_c(int64_t a1, int64_t a2, int64_t a3, int64_t a4, int64_t a5,
29+
int64_t a6, int64_t a7, int64_t a8, int64_t a9, int64_t a10,
30+
int64_t a11, int64_t a12, int64_t a13, int64_t a14,
31+
int64_t a15) {
32+
return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 +
33+
a14 + a15;
34+
}
35+
36+
typedef int64_t (*callback1_t)(int64_t);
37+
int64_t call_callback1(callback1_t cb, int64_t a1) { return cb(a1); }
38+
39+
typedef int64_t (*callback2_t)(int64_t, int64_t);
40+
int64_t call_callback2(callback2_t cb, int64_t a1, int64_t a2) {
41+
return cb(a1, a2);
42+
}
43+
44+
typedef int64_t (*callback3_t)(int64_t, int64_t, int64_t);
45+
int64_t call_callback3(callback3_t cb, int64_t a1, int64_t a2, int64_t a3) {
46+
return cb(a1, a2, a3);
47+
}
48+
49+
typedef int64_t (*callback5_t)(int64_t, int64_t, int64_t, int64_t, int64_t);
50+
int64_t call_callback5(callback5_t cb, int64_t a1, int64_t a2, int64_t a3,
51+
int64_t a4, int64_t a5) {
52+
return cb(a1, a2, a3, a4, a5);
53+
}
54+
55+
typedef int64_t (*callback10_t)(int64_t, int64_t, int64_t, int64_t, int64_t,
56+
int64_t, int64_t, int64_t, int64_t, int64_t);
57+
int64_t call_callback10(callback10_t cb, int64_t a1, int64_t a2, int64_t a3,
58+
int64_t a4, int64_t a5, int64_t a6, int64_t a7,
59+
int64_t a8, int64_t a9, int64_t a10) {
60+
return cb(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10);
61+
}
62+
63+
typedef int64_t (*callback14_t)(int64_t, int64_t, int64_t, int64_t, int64_t,
64+
int64_t, int64_t, int64_t, int64_t, int64_t,
65+
int64_t, int64_t, int64_t, int64_t);
66+
int64_t call_callback14(callback14_t cb, int64_t a1, int64_t a2, int64_t a3,
67+
int64_t a4, int64_t a5, int64_t a6, int64_t a7,
68+
int64_t a8, int64_t a9, int64_t a10, int64_t a11,
69+
int64_t a12, int64_t a13, int64_t a14) {
70+
return cb(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14);
71+
}
72+
73+
typedef int64_t (*callback15_t)(int64_t, int64_t, int64_t, int64_t, int64_t,
74+
int64_t, int64_t, int64_t, int64_t, int64_t,
75+
int64_t, int64_t, int64_t, int64_t, int64_t);
76+
int64_t call_callback15(callback15_t cb, int64_t a1, int64_t a2, int64_t a3,
77+
int64_t a4, int64_t a5, int64_t a6, int64_t a7,
78+
int64_t a8, int64_t a9, int64_t a10, int64_t a11,
79+
int64_t a12, int64_t a13, int64_t a14, int64_t a15) {
80+
return cb(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15);
81+
}

0 commit comments

Comments
 (0)