Skip to content

Commit 6157f45

Browse files
committed
[benchmark][ffi] Add simple callback benchmark
A basic benchmark for FFI callbacks. I'm currently not planning on extending it to cover more calling conventions or running it on our benchmark infra. We can leave that for when we want to do it. Bug: #38171 Change-Id: I0c5fec81333e9a8da092b4f51ceb9f80a2eb2cf7 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/407020 Reviewed-by: Liam Appelbe <[email protected]> Reviewed-by: Slava Egorov <[email protected]>
1 parent 68d334d commit 6157f45

File tree

7 files changed

+289
-0
lines changed

7 files changed

+289
-0
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
// These micro benchmarks track the speed of reading and writing C memory from
6+
// Dart with a specific marshalling and unmarshalling of data.
7+
8+
import 'dart:ffi';
9+
import 'dart:io';
10+
11+
import 'package:args/args.dart';
12+
13+
import 'dlopen_helper.dart';
14+
15+
// The native library that holds all the native functions being called.
16+
DynamicLibrary ffiTestFunctions = dlopenPlatformSpecific(
17+
'native_functions',
18+
path: Platform.script.resolve('../native/out/').path,
19+
);
20+
21+
abstract class FfiCallbackBenchmark {
22+
final String name;
23+
24+
FfiCallbackBenchmark(this.name);
25+
26+
// Returns ns per callback.
27+
double measureFor(Duration duration) {
28+
const int batchSize = 100000;
29+
30+
int numberOfCalls = 0;
31+
int totalMicroseconds = 0;
32+
33+
final sw = Stopwatch()..start();
34+
final durationInMicroseconds = duration.inMicroseconds;
35+
36+
do {
37+
run(batchSize);
38+
numberOfCalls += batchSize;
39+
totalMicroseconds = sw.elapsedMicroseconds;
40+
} while (totalMicroseconds < durationInMicroseconds);
41+
42+
final totalNanoSeconds = totalMicroseconds * 1000;
43+
return totalNanoSeconds / numberOfCalls;
44+
}
45+
46+
// Runs warmup phase, runs benchmark and reports result.
47+
void report({bool verbose = false}) {
48+
// Warmup for 100 ms.
49+
measureFor(const Duration(milliseconds: 100));
50+
51+
// Run benchmark for 2 seconds.
52+
final double nsPerCall = measureFor(const Duration(seconds: 2));
53+
54+
// Report result.
55+
print('$name(RunTimeRaw): $nsPerCall ns.');
56+
if (verbose) {
57+
final callsPerSecond = (1000 * 1000 * 1000 / nsPerCall).toInt();
58+
print('$name: $callsPerSecond calls per second.');
59+
}
60+
61+
shutdown();
62+
}
63+
64+
void run(int batchSize);
65+
66+
void shutdown();
67+
68+
void expectEquals(actual, expected) {
69+
if (actual != expected) {
70+
throw Exception('$name: Unexpected result: $actual, expected $expected');
71+
}
72+
}
73+
}
74+
75+
final class Uint8x1 extends FfiCallbackBenchmark {
76+
Uint8x1() : super('FfiCallbackBenchmark.Uint8x1');
77+
78+
final function = ffiTestFunctions.lookupFunction<
79+
Void Function(Pointer<NativeFunction<Void Function(Uint8)>>, Uint32),
80+
void Function(Pointer<NativeFunction<Void Function(Uint8)>>, int)
81+
>('CallFunction1Uint8');
82+
83+
static int x = 0;
84+
85+
static void callback(int value) {
86+
x += value;
87+
}
88+
89+
static final nativeCallable =
90+
NativeCallable<Void Function(Uint8)>.isolateLocal(callback);
91+
92+
static final pointer = nativeCallable.nativeFunction;
93+
94+
@override
95+
void run(int batchSize) {
96+
x = 0;
97+
function(pointer, batchSize);
98+
expectEquals(x, batchSize);
99+
}
100+
101+
@override
102+
void shutdown() {
103+
nativeCallable.close();
104+
}
105+
}
106+
107+
final argParser =
108+
ArgParser()..addFlag(
109+
'verbose',
110+
abbr: 'v',
111+
help: 'Verbose output',
112+
defaultsTo: false,
113+
);
114+
115+
void main(List<String> args) {
116+
final results = argParser.parse(args);
117+
final benchmarks = [Uint8x1.new];
118+
119+
final filter = results.rest.firstOrNull;
120+
for (var constructor in benchmarks) {
121+
final benchmark = constructor();
122+
if (filter == null || benchmark.name.contains(filter)) {
123+
benchmark.report(verbose: results['verbose']);
124+
}
125+
}
126+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:ffi';
6+
import 'dart:io';
7+
8+
const arm = 'arm';
9+
const arm64 = 'arm64';
10+
const ia32 = 'ia32';
11+
const x64 = 'x64';
12+
13+
// https://stackoverflow.com/questions/45125516/possible-values-for-uname-m
14+
final _unames = {
15+
'arm': arm,
16+
'arm64': arm64,
17+
'aarch64_be': arm64,
18+
'aarch64': arm64,
19+
'armv8b': arm64,
20+
'armv8l': arm64,
21+
'i386': ia32,
22+
'i686': ia32,
23+
'x86_64': x64,
24+
};
25+
26+
String _checkRunningMode(String architecture) {
27+
// Check if we're running in 32bit mode.
28+
final int pointerSize = sizeOf<IntPtr>();
29+
if (pointerSize == 4 && architecture == x64) return ia32;
30+
if (pointerSize == 4 && architecture == arm64) return arm;
31+
32+
return architecture;
33+
}
34+
35+
String _architecture() {
36+
final String uname = Process.runSync('uname', ['-m']).stdout.trim();
37+
final String? architecture = _unames[uname];
38+
if (architecture == null) {
39+
throw Exception('Unrecognized architecture: "$uname"');
40+
}
41+
42+
// Check if we're running in 32bit mode.
43+
return _checkRunningMode(architecture);
44+
}
45+
46+
String _platformPath(String name, String path) {
47+
if (Platform.isMacOS || Platform.isIOS) {
48+
return '${path}mac/${_architecture()}/lib$name.dylib';
49+
}
50+
51+
if (Platform.isWindows) {
52+
return '${path}win/${_checkRunningMode(x64)}/$name.dll';
53+
}
54+
55+
// Unknown platforms default to Unix implementation.
56+
return '${path}linux/${_architecture()}/lib$name.so';
57+
}
58+
59+
DynamicLibrary dlopenPlatformSpecific(String name, {String path = ''}) {
60+
final String fullPath = _platformPath(name, path);
61+
return DynamicLibrary.open(fullPath);
62+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
# for details. All rights reserved. Use of this source code is governed by a
3+
# BSD-style license that can be found in the LICENSE file.
4+
5+
out/
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
# for details. All rights reserved. Use of this source code is governed by a
3+
# BSD-style license that can be found in the LICENSE file.
4+
5+
# TODO(37531): Remove this makefile and build with sdk instead when
6+
# benchmark runner gets support for that.
7+
8+
CC=gcc
9+
CCARM=arm-linux-gnueabihf-gcc
10+
CCARM64=aarch64-linux-gnu-gcc
11+
CFLAGS=-Wall -g -O -fPIC
12+
13+
.PHONY: all clean
14+
15+
all: out/linux/x64/libnative_functions.so out/linux/ia32/libnative_functions.so out/linux/arm64/libnative_functions.so out/linux/arm/libnative_functions.so
16+
17+
cipd:
18+
cipd create -name dart/benchmarks/fficall -in out -install-mode copy
19+
20+
clean:
21+
rm -rf *.o *.so out
22+
23+
out/linux/x64:
24+
mkdir -p out/linux/x64
25+
26+
out/linux/x64/native_functions.o: native_functions.c | out/linux/x64
27+
$(CC) $(CFLAGS) -c -o $@ native_functions.c
28+
29+
out/linux/x64/libnative_functions.so: out/linux/x64/native_functions.o
30+
$(CC) $(CFLAGS) -s -shared -o $@ out/linux/x64/native_functions.o
31+
32+
out/linux/ia32:
33+
mkdir -p out/linux/ia32
34+
35+
out/linux/ia32/native_functions.o: native_functions.c | out/linux/ia32
36+
$(CC) $(CFLAGS) -m32 -c -o $@ native_functions.c
37+
38+
out/linux/ia32/libnative_functions.so: out/linux/ia32/native_functions.o
39+
$(CC) $(CFLAGS) -m32 -s -shared -o $@ out/linux/ia32/native_functions.o
40+
41+
out/linux/arm64:
42+
mkdir -p out/linux/arm64
43+
44+
out/linux/arm64/native_functions.o: native_functions.c | out/linux/arm64
45+
$(CCARM64) $(CFLAGS) -c -o $@ native_functions.c
46+
47+
out/linux/arm64/libnative_functions.so: out/linux/arm64/native_functions.o
48+
$(CCARM64) $(CFLAGS) -s -shared -o $@ out/linux/arm64/native_functions.o
49+
50+
out/linux/arm:
51+
mkdir -p out/linux/arm
52+
53+
out/linux/arm/native_functions.o: native_functions.c | out/linux/arm
54+
$(CCARM) $(CFLAGS) -c -o $@ native_functions.c
55+
56+
out/linux/arm/libnative_functions.so: out/linux/arm/native_functions.o
57+
$(CCARM) $(CFLAGS) -s -shared -o $@ out/linux/arm/native_functions.o
58+
59+
# On M1 Machine.
60+
out/mac/arm64:
61+
mkdir -p out/mac/arm64
62+
63+
out/mac/arm64/native_functions.o: native_functions.c | out/mac/arm64
64+
$(CC) $(CFLAGS) -c -o $@ native_functions.c
65+
66+
out/mac/arm64/libnative_functions.dylib: out/mac/arm64/native_functions.o
67+
$(CC) $(CFLAGS) -s -shared -o $@ out/mac/arm64/native_functions.o
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
To upload a new package to CIPD:
2+
```
3+
$ cd benchmarks/FfiCallback/native
4+
$ make
5+
$ find . -name "*.o" -type f -delete
6+
$ cipd create -pkg-def=cipd.yaml
7+
```
8+
9+
Then update the top level DEPS file with the new hash.
10+
The new hash can be found with:
11+
```
12+
$ cipd instances dart/benchmarks/fficallback
13+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package: dart/benchmarks/fficallback
2+
description: Dynamic libaries for running the benchmarks/FfiCallback.
3+
install_mode: copy
4+
data:
5+
- dir: out/
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
#include <stdint.h>
6+
7+
void CallFunction1Uint8(void (*callback)(uint8_t), uint32_t batch_size) {
8+
for (uint32_t i = 0; i < batch_size; i++) {
9+
callback(1);
10+
}
11+
}

0 commit comments

Comments
 (0)