From e9cebec0aab89f2d988df28d455a4381bf409e32 Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Wed, 22 Apr 2026 14:47:24 +0900 Subject: [PATCH 1/2] jextract/jni: byte passing performance benchmarks --- .../MySwiftLibrary/BenchByteArrays.swift | 67 +++++++ .../swiftkit/ffm/FFMByteArrayBenchmark.java | 125 ++++++++++++ .../MySwiftLibrary/BenchByteArrays.swift | 101 ++++++++++ .../example/swift/JNIByteArrayBenchmark.java | 179 ++++++++++++++++++ 4 files changed, 472 insertions(+) create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift create mode 100644 Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMByteArrayBenchmark.java create mode 100644 Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift create mode 100644 Samples/SwiftJavaExtractJNISampleApp/src/jmh/java/com/example/swift/JNIByteArrayBenchmark.java diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift new file mode 100644 index 000000000..f723213d9 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// FFM mode supports [UInt8] and Data, but NOT [[UInt8]], UnsafeRawBufferPointer, +// or UnsafeMutableRawBufferPointer. The benchmark class on the Java side omits +// the unsupported cases. + +// ==== ----------------------------------------------------------------------- +// MARK: [UInt8] pass-through + +public func benchAcceptBytes(_ bytes: [UInt8]) -> Int { + bytes.count +} + +public func benchReturnBytes(_ size: Int) -> [UInt8] { + [UInt8](repeating: 0, count: size) +} + +public func benchEchoBytes(_ bytes: [UInt8]) -> [UInt8] { + bytes +} + +// ==== ----------------------------------------------------------------------- +// MARK: Data + +public func benchAcceptData(_ data: Data) -> Int { + data.count +} + +public func benchReturnData(_ size: Int) -> Data { + Data(count: size) +} + +public func benchEchoData(_ data: Data) -> Data { + data +} + +// ==== ----------------------------------------------------------------------- +// MARK: real world examples + +public func benchSparseShard( + dimension: Int32, + numShards: Int32, + indices: [Int32], + values: [Int32], + helperKey: [UInt8] +) -> [UInt8] { + let outSize = Int(indices.count) * 40 + helperKey.count + return [UInt8](repeating: 0, count: outSize) +} diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMByteArrayBenchmark.java b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMByteArrayBenchmark.java new file mode 100644 index 000000000..d309acdd4 --- /dev/null +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMByteArrayBenchmark.java @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +package org.swift.swiftkit.ffm; + +import com.example.swift.Data; +import com.example.swift.MySwiftLibrary; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.concurrent.TimeUnit; + +/** + * Byte-array marshalling benchmarks (FFM mode). + * + * FFM mode supports the [UInt8] and Data shapes but not [[UInt8]], + * UnsafeRawBufferPointer, or UnsafeMutableRawBufferPointer — the + * corresponding benchmarks live only in JNIByteArrayBenchmark. + * + * Method suffix _ffm aligns these rows with their _jni counterparts for + * side-by-side reading when reports are sorted alphabetically. + */ +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 2, time = 200, timeUnit = TimeUnit.MILLISECONDS) +@Measurement(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@State(Scope.Benchmark) +@Fork(value = 1, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED", "-Xmx1g" }) +public class FFMByteArrayBenchmark { + + @Param({"4096", "65536", "262144", "1048576", "8388608", "16777216"}) + public int totalBytes; + + byte[] flat; + Data data; + ClosableAllocatingSwiftArena arena; + + int[] sparseIndices; + int[] sparseValues; + byte[] helperKey; + + @Setup(Level.Trial) + public void beforeAll() { + arena = AllocatingSwiftArena.ofConfined(); + + flat = new byte[totalBytes]; + for (int i = 0; i < totalBytes; i++) { + flat[i] = (byte) (i & 0xff); + } + + data = Data.init(flat, arena); + + sparseIndices = new int[8192]; + sparseValues = new int[8192]; + for (int i = 0; i < 8192; i++) { + sparseIndices[i] = i * 17; + sparseValues[i] = i; + } + helperKey = new byte[32]; + } + + @TearDown(Level.Trial) + public void afterAll() { + arena.close(); + } + + // ==== ----------------------------------------------------------------- + // MARK: [UInt8] + + @Benchmark + public long acceptBytes_ffm() { + return MySwiftLibrary.benchAcceptBytes(flat); + } + + @Benchmark + public byte[] returnBytes_ffm() { + return MySwiftLibrary.benchReturnBytes(totalBytes); + } + + @Benchmark + public byte[] echoBytes_ffm() { + return MySwiftLibrary.benchEchoBytes(flat); + } + + // ==== ----------------------------------------------------------------- + // MARK: Data + + @Benchmark + public long acceptData_ffm() { + return MySwiftLibrary.benchAcceptData(data); + } + + @Benchmark + public Data returnData_ffm(Blackhole bh) { + Data result = MySwiftLibrary.benchReturnData(totalBytes, arena); + bh.consume(result.getCount()); + return result; + } + + @Benchmark + public Data echoData_ffm(Blackhole bh) { + Data echoed = MySwiftLibrary.benchEchoData(data, arena); + bh.consume(echoed.getCount()); + return echoed; + } + + // ==== ----------------------------------------------------------------- + // MARK: sparse shard + + @Benchmark + public byte[] sparseShard_ffm() { + return MySwiftLibrary.benchSparseShard(1000, 2, sparseIndices, sparseValues, helperKey); + } +} diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift new file mode 100644 index 000000000..dbc623624 --- /dev/null +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift @@ -0,0 +1,101 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift.org project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift.org project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import SwiftJava + +#if canImport(FoundationEssentials) +import FoundationEssentials +#else +import Foundation +#endif + +// ==== ----------------------------------------------------------------------- +// MARK: [UInt8] pass-through + +public func benchAcceptBytes(_ bytes: [UInt8]) -> Int { + bytes.count +} + +public func benchReturnBytes(_ size: Int) -> [UInt8] { + [UInt8](repeating: 0xff, count: size) +} + +public func benchEchoBytes(_ bytes: [UInt8]) -> [UInt8] { + bytes +} + +// ==== ----------------------------------------------------------------------- +// MARK: [[UInt8]] pass-through + +public func benchAcceptNestedBytes(_ arrays: [[UInt8]]) -> Int { + arrays.reduce(0) { $0 + $1.count } +} + +public func benchReturnNestedBytes(_ outer: Int, _ inner: Int) -> [[UInt8]] { + (0.. [[UInt8]] { + arrays +} + +// ==== ----------------------------------------------------------------------- +// MARK: UnsafeRawBufferPointer (JNI only) + +public func benchAcceptBuffer(_ buf: UnsafeRawBufferPointer) -> Int { + buf.count +} + +public func benchAcceptMutableBuffer(_ buf: UnsafeMutableRawBufferPointer) -> Int { + buf.count +} + +// ==== ----------------------------------------------------------------------- +// MARK: Data + +public func benchAcceptData(_ data: Data) -> Int { + data.count +} + +public func benchReturnData(_ size: Int) -> Data { + Data(repeating: 0xff, count: size) +} + +public func benchEchoData(_ data: Data) -> Data { + data +} + +// ==== ----------------------------------------------------------------------- +// MARK: real world examples + +public func benchSparseShard( + dimension: Int32, + numShards: Int32, + indices: [Int32], + values: [Int32], + helperKey: [UInt8] +) -> [UInt8] { + let outSize = Int(indices.count) * 40 + helperKey.count + return [UInt8](repeating: 0xff, count: outSize) +} + +public func benchDenseShard( + dimension: Int32, + numShards: Int32, + measurement: [UInt8], + helperKey: [UInt8] +) -> [[UInt8]] { + let shardSize = measurement.count / Int(numShards) + return (0.. Date: Wed, 22 Apr 2026 15:28:02 +0900 Subject: [PATCH 2/2] make benchmark less skewed --- .../MySwiftLibrary/BenchByteArrays.swift | 39 ++++------ .../swiftkit/ffm/FFMByteArrayBenchmark.java | 50 ++++++------- .../MySwiftLibrary/BenchByteArrays.swift | 51 +++++-------- .../example/swift/JNIByteArrayBenchmark.java | 75 ++++++++----------- 4 files changed, 86 insertions(+), 129 deletions(-) diff --git a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift index f723213d9..546ed90f8 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift +++ b/Samples/SwiftJavaExtractFFMSampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift @@ -18,50 +18,41 @@ import FoundationEssentials import Foundation #endif -// FFM mode supports [UInt8] and Data, but NOT [[UInt8]], UnsafeRawBufferPointer, -// or UnsafeMutableRawBufferPointer. The benchmark class on the Java side omits -// the unsupported cases. - // ==== ----------------------------------------------------------------------- // MARK: [UInt8] pass-through -public func benchAcceptBytes(_ bytes: [UInt8]) -> Int { +public func acceptBytes(_ bytes: [UInt8]) -> Int { bytes.count } -public func benchReturnBytes(_ size: Int) -> [UInt8] { - [UInt8](repeating: 0, count: size) +public func returnBytes(_ size: Int) -> [UInt8] { + [UInt8](repeating: 0xff, count: size) } -public func benchEchoBytes(_ bytes: [UInt8]) -> [UInt8] { +public func echoBytes(_ bytes: [UInt8]) -> [UInt8] { bytes } // ==== ----------------------------------------------------------------------- // MARK: Data -public func benchAcceptData(_ data: Data) -> Int { +public func acceptData(_ data: Data) -> Int { data.count } -public func benchReturnData(_ size: Int) -> Data { - Data(count: size) -} - -public func benchEchoData(_ data: Data) -> Data { - data +public func returnData(_ size: Int) -> Data { + Data(repeating: 0xff, count: size) } // ==== ----------------------------------------------------------------------- -// MARK: real world examples +// MARK: large multi-parameter function -public func benchSparseShard( - dimension: Int32, - numShards: Int32, - indices: [Int32], - values: [Int32], - helperKey: [UInt8] +public func largeFunction( + a: Int32, + b: [UInt8], + c: [Int32], + d: [UInt8] ) -> [UInt8] { - let outSize = Int(indices.count) * 40 + helperKey.count - return [UInt8](repeating: 0, count: outSize) + let outSize = b.count + d.count + c.count * 4 + return [UInt8](repeating: 0xff, count: outSize) } diff --git a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMByteArrayBenchmark.java b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMByteArrayBenchmark.java index d309acdd4..4d2c0c4d2 100644 --- a/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMByteArrayBenchmark.java +++ b/Samples/SwiftJavaExtractFFMSampleApp/src/jmh/java/org/swift/swiftkit/ffm/FFMByteArrayBenchmark.java @@ -21,16 +21,6 @@ import java.util.concurrent.TimeUnit; -/** - * Byte-array marshalling benchmarks (FFM mode). - * - * FFM mode supports the [UInt8] and Data shapes but not [[UInt8]], - * UnsafeRawBufferPointer, or UnsafeMutableRawBufferPointer — the - * corresponding benchmarks live only in JNIByteArrayBenchmark. - * - * Method suffix _ffm aligns these rows with their _jni counterparts for - * side-by-side reading when reports are sorted alphabetically. - */ @BenchmarkMode(Mode.AverageTime) @Warmup(iterations = 2, time = 200, timeUnit = TimeUnit.MILLISECONDS) @Measurement(iterations = 3, time = 500, timeUnit = TimeUnit.MILLISECONDS) @@ -39,6 +29,9 @@ @Fork(value = 1, jvmArgsAppend = { "--enable-native-access=ALL-UNNAMED", "-Xmx1g" }) public class FFMByteArrayBenchmark { + @Param({"ffm"}) + public String mode; + @Param({"4096", "65536", "262144", "1048576", "8388608", "16777216"}) public int totalBytes; @@ -46,9 +39,11 @@ public class FFMByteArrayBenchmark { Data data; ClosableAllocatingSwiftArena arena; - int[] sparseIndices; - int[] sparseValues; - byte[] helperKey; + // Fixtures for largeFunction(a:b:c:d:) + int large_a; + byte[] large_b; + int[] large_c; + byte[] large_d; @Setup(Level.Trial) public void beforeAll() { @@ -61,13 +56,14 @@ public void beforeAll() { data = Data.init(flat, arena); - sparseIndices = new int[8192]; - sparseValues = new int[8192]; + large_a = 1000; + large_b = new byte[8192]; + large_c = new int[8192]; for (int i = 0; i < 8192; i++) { - sparseIndices[i] = i * 17; - sparseValues[i] = i; + large_b[i] = (byte) (i & 0xff); + large_c[i] = i; } - helperKey = new byte[32]; + large_d = new byte[32]; } @TearDown(Level.Trial) @@ -80,17 +76,17 @@ public void afterAll() { @Benchmark public long acceptBytes_ffm() { - return MySwiftLibrary.benchAcceptBytes(flat); + return MySwiftLibrary.acceptBytes(flat); } @Benchmark public byte[] returnBytes_ffm() { - return MySwiftLibrary.benchReturnBytes(totalBytes); + return MySwiftLibrary.returnBytes(totalBytes); } @Benchmark public byte[] echoBytes_ffm() { - return MySwiftLibrary.benchEchoBytes(flat); + return MySwiftLibrary.echoBytes(flat); } // ==== ----------------------------------------------------------------- @@ -98,28 +94,28 @@ public byte[] echoBytes_ffm() { @Benchmark public long acceptData_ffm() { - return MySwiftLibrary.benchAcceptData(data); + return MySwiftLibrary.acceptData(data); } @Benchmark public Data returnData_ffm(Blackhole bh) { - Data result = MySwiftLibrary.benchReturnData(totalBytes, arena); + Data result = MySwiftLibrary.returnData(totalBytes, arena); bh.consume(result.getCount()); return result; } @Benchmark public Data echoData_ffm(Blackhole bh) { - Data echoed = MySwiftLibrary.benchEchoData(data, arena); + Data echoed = MySwiftLibrary.echoData(data, arena); bh.consume(echoed.getCount()); return echoed; } // ==== ----------------------------------------------------------------- - // MARK: sparse shard + // MARK: large multi-parameter function @Benchmark - public byte[] sparseShard_ffm() { - return MySwiftLibrary.benchSparseShard(1000, 2, sparseIndices, sparseValues, helperKey); + public byte[] wide_ffm() { + return MySwiftLibrary.largeFunction(large_a, large_b, large_c, large_d); } } diff --git a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift index dbc623624..23103a07b 100644 --- a/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift +++ b/Samples/SwiftJavaExtractJNISampleApp/Sources/MySwiftLibrary/BenchByteArrays.swift @@ -23,79 +23,64 @@ import Foundation // ==== ----------------------------------------------------------------------- // MARK: [UInt8] pass-through -public func benchAcceptBytes(_ bytes: [UInt8]) -> Int { +public func acceptBytes(_ bytes: [UInt8]) -> Int { bytes.count } -public func benchReturnBytes(_ size: Int) -> [UInt8] { +public func returnBytes(_ size: Int) -> [UInt8] { [UInt8](repeating: 0xff, count: size) } -public func benchEchoBytes(_ bytes: [UInt8]) -> [UInt8] { +public func echoBytes(_ bytes: [UInt8]) -> [UInt8] { bytes } // ==== ----------------------------------------------------------------------- // MARK: [[UInt8]] pass-through -public func benchAcceptNestedBytes(_ arrays: [[UInt8]]) -> Int { +public func acceptNestedBytes(_ arrays: [[UInt8]]) -> Int { arrays.reduce(0) { $0 + $1.count } } -public func benchReturnNestedBytes(_ outer: Int, _ inner: Int) -> [[UInt8]] { +public func returnNestedBytes(_ outer: Int, _ inner: Int) -> [[UInt8]] { (0.. [[UInt8]] { +public func echoNestedBytes(_ arrays: [[UInt8]]) -> [[UInt8]] { arrays } // ==== ----------------------------------------------------------------------- // MARK: UnsafeRawBufferPointer (JNI only) -public func benchAcceptBuffer(_ buf: UnsafeRawBufferPointer) -> Int { +public func acceptBuffer(_ buf: UnsafeRawBufferPointer) -> Int { buf.count } -public func benchAcceptMutableBuffer(_ buf: UnsafeMutableRawBufferPointer) -> Int { +public func acceptMutableBuffer(_ buf: UnsafeMutableRawBufferPointer) -> Int { buf.count } // ==== ----------------------------------------------------------------------- // MARK: Data -public func benchAcceptData(_ data: Data) -> Int { +public func acceptData(_ data: Data) -> Int { data.count } -public func benchReturnData(_ size: Int) -> Data { +public func returnData(_ size: Int) -> Data { Data(repeating: 0xff, count: size) } -public func benchEchoData(_ data: Data) -> Data { - data -} - // ==== ----------------------------------------------------------------------- -// MARK: real world examples - -public func benchSparseShard( - dimension: Int32, - numShards: Int32, - indices: [Int32], - values: [Int32], - helperKey: [UInt8] +// MARK: large multi-parameter function + +public func largeFunction( + a: Int32, + b: [UInt8], + c: [Int32], + d: [UInt8] ) -> [UInt8] { - let outSize = Int(indices.count) * 40 + helperKey.count + let outSize = b.count + d.count + c.count * 4 return [UInt8](repeating: 0xff, count: outSize) } - -public func benchDenseShard( - dimension: Int32, - numShards: Int32, - measurement: [UInt8], - helperKey: [UInt8] -) -> [[UInt8]] { - let shardSize = measurement.count / Int(numShards) - return (0..