From 25693a0a7db0454e3b5a52a01b66eabc6def55de Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Fri, 18 Sep 2026 16:49:58 -0400 Subject: [PATCH] perf(shared-core): copy bytes across the bridge in one memcpy `Data.kotlinByteArray` filled a `KotlinByteArray` with `set(index:value:)`, and `Data.init(_:)` read one back with `get(index:)`. Both are Objective-C message sends, so the cost is one send per byte. That is invisible for a 32-byte key or a hash, and it is not invisible for the discrete-curve tables: the iOS app hands the bridge 6,720,032 bytes on first use of `DiscreteBondingCurve`, and spent between 1.8s and 2.8s inside `SharedBondingCurve.initialize` doing it, on the main thread during launch. `SharedBytes` in a new `appleMain` source set converts through `NSData` with `usePinned` and `memcpy`, so each direction is a single call. Kotlin bytes are signed, but a bulk copy moves the bit patterns unchanged, so the reinterpretation the old reverse copy did by hand is no longer needed. A round trip of 6,720,032 bytes in `SharedCoreKitTests` goes from 1.013s to 0.002s. The existing round-trip tests, including the one over every byte value, still pass, as do the Ed25519, Base58 and bonding-curve vector suites. `Array.kotlinLongArray` has the same per-element shape and is left alone; its callers pass small arrays. --- .../SharedCoreKit/Data+KotlinByteArray.swift | 19 +++------- .../SharedCoreKitTests/DataBridgeTests.swift | 22 +++++++++++ .../kotlin/com/flipcash/shared/SharedBytes.kt | 38 +++++++++++++++++++ 3 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 kmp/shared-core/src/appleMain/kotlin/com/flipcash/shared/SharedBytes.kt diff --git a/kmp/shared-core/spm/Sources/SharedCoreKit/Data+KotlinByteArray.swift b/kmp/shared-core/spm/Sources/SharedCoreKit/Data+KotlinByteArray.swift index b9f956617f..1e4a63e625 100644 --- a/kmp/shared-core/spm/Sources/SharedCoreKit/Data+KotlinByteArray.swift +++ b/kmp/shared-core/spm/Sources/SharedCoreKit/Data+KotlinByteArray.swift @@ -4,24 +4,15 @@ import SharedCore extension Data { /// Kotlin's `ByteArray` has no `Data` bridge of its own, so every exported function taking - /// bytes needs this copy. + /// bytes needs this copy. It goes through `NSData` because the only element-wise alternative, + /// `KotlinByteArray.set(index:value:)`, costs one Objective-C message send per byte. var kotlinByteArray: KotlinByteArray { - let array = KotlinByteArray(size: Int32(count)) - for (offset, byte) in enumerated() { - array.set(index: Int32(offset), value: Int8(bitPattern: byte)) - } - return array + SharedBytes.shared.byteArray(data: self) } /// The reverse copy, for the exported functions that hand bytes back. Kotlin bytes are signed, - /// so anything above 0x7F comes across negative and has to be reinterpreted rather than - /// converted. + /// but a bulk copy moves the bit patterns unchanged, so nothing has to be reinterpreted here. init(_ array: KotlinByteArray) { - var bytes = [UInt8]() - bytes.reserveCapacity(Int(array.size)) - for index in 0..