Repository navigation
Convert arrays to Java with one existential open and one ArrayList call - #122
Open
piercifani wants to merge 1 commit into
Open
piercifani wants to merge 1 commit into
piercifani wants to merge 1 commit into
Conversation
Array.toJavaObject cast every element to JConvertible through AnyBridging.toJavaObject(_: Any?) and called ArrayList.add per element. It now opens Element's JConvertible conformance once per array, fills a Java Object[] with SetObjectArrayElement, and builds the ArrayList with a single ArrayList(Arrays.asList(...)) call. The result is still a mutable java.util.ArrayList. Element types that are not JConvertible keep the per-element AnyBridging path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 7, 2026
Merged
piercifani
marked this pull request as ready for review
October 7, 2026 17:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Array.toJavaObjectcalledAnyBridging.toJavaObject(_: Any?)for every element, which boxes the element intoAnyand runs a dynamicas? JConvertiblecast, then made oneArrayList.addJNI call per element. A profile of a Kotlin loop reading[Int]× 200 put about a quarter of the getter inswift_dynamicCastand another quarter in theArrayList.addcalls.Now:
Element.self as? JConvertible.Typeis checked once per array, and the conversion runs through the statically-typed conformance (withMemoryReboundover the array buffer, which is valid because the cast provedElement == E).Object[]viaSetObjectArrayElement(no Java method invocation per element), then oneArrayList(Arrays.asList(...))call builds the list. The result is still a mutablejava.util.ArrayList, whichskip.lib.Array(nocopy:)relies on.JConvertible(Any, existentials,Optional<Any>, errors) keep the per-elementAnyBridgingpath.SetandDictionaryare unchanged: a per-element generic helper measured slower than the existing cast, so I left them alone.Numbers
[Int]× 200 getter[Struct]× 200 getter[Struct]× 200 getter + read every field[String]× 200 getter (with skiptools/swift-jni#11)App-shaped types (Pixel only; the rows above use a deliberately small 4-field struct).
ChatMessagemirrors a production chat message: 13 fields including a nested 10-field user struct,Dates, an enum with associated values and arrays of nested structs.FeedRowIDis a String-raw enum plus a String key.[ChatMessage]× 50 getter[ChatMessage]× 50 getter + read 4 fields per row[FeedRowID]× 200 getterOnce a row's fields are read, per-field getters dominate and the gain from this change shrinks accordingly.
What is left per element is mostly the Kotlin peer allocation (
NewObjectAon a class withfinalize()) and theSwiftValueTypeBox.How this was measured
A benchmark test in
SkipBridgeToKotlinSamplesTests(not part of these PRs) reads a bridgedBenchModelfrom the transpiled Kotlin test: scalar,String, struct and 200-element array getters, plus==on two 200-element[Struct]lists (what Compose'schanged()does). Each row is the best of 15 trials, two interleaved stock/patched rounds.connectedAndroidTestwithtestBuildType = "release". The debug test APK is debuggable, so ART turns on CheckJNI ("Late-enabling -Xcheck:jni"), which roughly doubled every JNI cost and would have overstated these changes.-O.Rows these changes don't touch moved by up to ±12% between rounds on the device; treat that as the noise floor.
Tests
skip test: 153/153 (macOS + Robolectric).assembleDebug).Related: skiptools/swift-jni#11, skiptools/skipstone#292 (independent; each can land on its own)
🤖 Generated with Claude Code