[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: https://github.com/dart-lang/sdk/issues/38171
Change-Id: I0c5fec81333e9a8da092b4f51ceb9f80a2eb2cf7
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/407020
Reviewed-by: Liam Appelbe <liama@google.com>
Reviewed-by: Slava Egorov <vegorov@google.com>
This commit is contained in:
Daco Harkes
2025-02-04 03:36:29 -08:00
parent 68d334d620
commit 6157f4598c
7 changed files with 289 additions and 0 deletions
@@ -0,0 +1,126 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// These micro benchmarks track the speed of reading and writing C memory from
// Dart with a specific marshalling and unmarshalling of data.
import 'dart:ffi';
import 'dart:io';
import 'package:args/args.dart';
import 'dlopen_helper.dart';
// The native library that holds all the native functions being called.
DynamicLibrary ffiTestFunctions = dlopenPlatformSpecific(
'native_functions',
path: Platform.script.resolve('../native/out/').path,
);
abstract class FfiCallbackBenchmark {
final String name;
FfiCallbackBenchmark(this.name);
// Returns ns per callback.
double measureFor(Duration duration) {
const int batchSize = 100000;
int numberOfCalls = 0;
int totalMicroseconds = 0;
final sw = Stopwatch()..start();
final durationInMicroseconds = duration.inMicroseconds;
do {
run(batchSize);
numberOfCalls += batchSize;
totalMicroseconds = sw.elapsedMicroseconds;
} while (totalMicroseconds < durationInMicroseconds);
final totalNanoSeconds = totalMicroseconds * 1000;
return totalNanoSeconds / numberOfCalls;
}
// Runs warmup phase, runs benchmark and reports result.
void report({bool verbose = false}) {
// Warmup for 100 ms.
measureFor(const Duration(milliseconds: 100));
// Run benchmark for 2 seconds.
final double nsPerCall = measureFor(const Duration(seconds: 2));
// Report result.
print('$name(RunTimeRaw): $nsPerCall ns.');
if (verbose) {
final callsPerSecond = (1000 * 1000 * 1000 / nsPerCall).toInt();
print('$name: $callsPerSecond calls per second.');
}
shutdown();
}
void run(int batchSize);
void shutdown();
void expectEquals(actual, expected) {
if (actual != expected) {
throw Exception('$name: Unexpected result: $actual, expected $expected');
}
}
}
final class Uint8x1 extends FfiCallbackBenchmark {
Uint8x1() : super('FfiCallbackBenchmark.Uint8x1');
final function = ffiTestFunctions.lookupFunction<
Void Function(Pointer<NativeFunction<Void Function(Uint8)>>, Uint32),
void Function(Pointer<NativeFunction<Void Function(Uint8)>>, int)
>('CallFunction1Uint8');
static int x = 0;
static void callback(int value) {
x += value;
}
static final nativeCallable =
NativeCallable<Void Function(Uint8)>.isolateLocal(callback);
static final pointer = nativeCallable.nativeFunction;
@override
void run(int batchSize) {
x = 0;
function(pointer, batchSize);
expectEquals(x, batchSize);
}
@override
void shutdown() {
nativeCallable.close();
}
}
final argParser =
ArgParser()..addFlag(
'verbose',
abbr: 'v',
help: 'Verbose output',
defaultsTo: false,
);
void main(List<String> args) {
final results = argParser.parse(args);
final benchmarks = [Uint8x1.new];
final filter = results.rest.firstOrNull;
for (var constructor in benchmarks) {
final benchmark = constructor();
if (filter == null || benchmark.name.contains(filter)) {
benchmark.report(verbose: results['verbose']);
}
}
}
@@ -0,0 +1,62 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:ffi';
import 'dart:io';
const arm = 'arm';
const arm64 = 'arm64';
const ia32 = 'ia32';
const x64 = 'x64';
// https://stackoverflow.com/questions/45125516/possible-values-for-uname-m
final _unames = {
'arm': arm,
'arm64': arm64,
'aarch64_be': arm64,
'aarch64': arm64,
'armv8b': arm64,
'armv8l': arm64,
'i386': ia32,
'i686': ia32,
'x86_64': x64,
};
String _checkRunningMode(String architecture) {
// Check if we're running in 32bit mode.
final int pointerSize = sizeOf<IntPtr>();
if (pointerSize == 4 && architecture == x64) return ia32;
if (pointerSize == 4 && architecture == arm64) return arm;
return architecture;
}
String _architecture() {
final String uname = Process.runSync('uname', ['-m']).stdout.trim();
final String? architecture = _unames[uname];
if (architecture == null) {
throw Exception('Unrecognized architecture: "$uname"');
}
// Check if we're running in 32bit mode.
return _checkRunningMode(architecture);
}
String _platformPath(String name, String path) {
if (Platform.isMacOS || Platform.isIOS) {
return '${path}mac/${_architecture()}/lib$name.dylib';
}
if (Platform.isWindows) {
return '${path}win/${_checkRunningMode(x64)}/$name.dll';
}
// Unknown platforms default to Unix implementation.
return '${path}linux/${_architecture()}/lib$name.so';
}
DynamicLibrary dlopenPlatformSpecific(String name, {String path = ''}) {
final String fullPath = _platformPath(name, path);
return DynamicLibrary.open(fullPath);
}
+5
View File
@@ -0,0 +1,5 @@
# Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
out/
+67
View File
@@ -0,0 +1,67 @@
# Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.
# TODO(37531): Remove this makefile and build with sdk instead when
# benchmark runner gets support for that.
CC=gcc
CCARM=arm-linux-gnueabihf-gcc
CCARM64=aarch64-linux-gnu-gcc
CFLAGS=-Wall -g -O -fPIC
.PHONY: all clean
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
cipd:
cipd create -name dart/benchmarks/fficall -in out -install-mode copy
clean:
rm -rf *.o *.so out
out/linux/x64:
mkdir -p out/linux/x64
out/linux/x64/native_functions.o: native_functions.c | out/linux/x64
$(CC) $(CFLAGS) -c -o $@ native_functions.c
out/linux/x64/libnative_functions.so: out/linux/x64/native_functions.o
$(CC) $(CFLAGS) -s -shared -o $@ out/linux/x64/native_functions.o
out/linux/ia32:
mkdir -p out/linux/ia32
out/linux/ia32/native_functions.o: native_functions.c | out/linux/ia32
$(CC) $(CFLAGS) -m32 -c -o $@ native_functions.c
out/linux/ia32/libnative_functions.so: out/linux/ia32/native_functions.o
$(CC) $(CFLAGS) -m32 -s -shared -o $@ out/linux/ia32/native_functions.o
out/linux/arm64:
mkdir -p out/linux/arm64
out/linux/arm64/native_functions.o: native_functions.c | out/linux/arm64
$(CCARM64) $(CFLAGS) -c -o $@ native_functions.c
out/linux/arm64/libnative_functions.so: out/linux/arm64/native_functions.o
$(CCARM64) $(CFLAGS) -s -shared -o $@ out/linux/arm64/native_functions.o
out/linux/arm:
mkdir -p out/linux/arm
out/linux/arm/native_functions.o: native_functions.c | out/linux/arm
$(CCARM) $(CFLAGS) -c -o $@ native_functions.c
out/linux/arm/libnative_functions.so: out/linux/arm/native_functions.o
$(CCARM) $(CFLAGS) -s -shared -o $@ out/linux/arm/native_functions.o
# On M1 Machine.
out/mac/arm64:
mkdir -p out/mac/arm64
out/mac/arm64/native_functions.o: native_functions.c | out/mac/arm64
$(CC) $(CFLAGS) -c -o $@ native_functions.c
out/mac/arm64/libnative_functions.dylib: out/mac/arm64/native_functions.o
$(CC) $(CFLAGS) -s -shared -o $@ out/mac/arm64/native_functions.o
+13
View File
@@ -0,0 +1,13 @@
To upload a new package to CIPD:
```
$ cd benchmarks/FfiCallback/native
$ make
$ find . -name "*.o" -type f -delete
$ cipd create -pkg-def=cipd.yaml
```
Then update the top level DEPS file with the new hash.
The new hash can be found with:
```
$ cipd instances dart/benchmarks/fficallback
```
+5
View File
@@ -0,0 +1,5 @@
package: dart/benchmarks/fficallback
description: Dynamic libaries for running the benchmarks/FfiCallback.
install_mode: copy
data:
- dir: out/
@@ -0,0 +1,11 @@
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#include <stdint.h>
void CallFunction1Uint8(void (*callback)(uint8_t), uint32_t batch_size) {
for (uint32_t i = 0; i < batch_size; i++) {
callback(1);
}
}