Skip to content

Convert arrays to Java with one existential open and one ArrayList call - #122

Open
piercifani wants to merge 1 commit into
mainfrom
perf/array-tojavaobject
Open

piercifani wants to merge 1 commit into
mainfrom
perf/array-tojavaobject

Conversation

@piercifani

@piercifani piercifani commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Array.toJavaObject called AnyBridging.toJavaObject(_: Any?) for every element, which boxes the element into Any and runs a dynamic as? JConvertible cast, then made one ArrayList.add JNI call per element. A profile of a Kotlin loop reading [Int] × 200 put about a quarter of the getter in swift_dynamicCast and another quarter in the ArrayList.add calls.

Now:

  • Element.self as? JConvertible.Type is checked once per array, and the conversion runs through the statically-typed conformance (withMemoryRebound over the array buffer, which is valid because the cast proved Element == E).
  • Elements go into a Java Object[] via SetObjectArrayElement (no Java method invocation per element), then one ArrayList(Arrays.asList(...)) call builds the list. The result is still a mutable java.util.ArrayList, which skip.lib.Array(nocopy:) relies on.
  • Element types that are not JConvertible (Any, existentials, Optional<Any>, errors) keep the per-element AnyBridging path.

Set and Dictionary are unchanged: a per-element generic helper measured slower than the existing cast, so I left them alone.

Numbers

Pixel stock Pixel patched Mac stock Mac patched
[Int] × 200 getter 113 µs 76 µs 1.49× 65 µs 33 µs 1.99×
[Struct] × 200 getter 180 µs 120 µs 1.50× 94 µs 52 µs 1.79×
[Struct] × 200 getter + read every field 297 µs 220 µs 1.35× 127 µs 88 µs 1.43×
[String] × 200 getter (with skiptools/swift-jni#11) 123 µs 62 µs 1.97× 64 µs 26 µs 2.45×

App-shaped types (Pixel only; the rows above use a deliberately small 4-field struct). ChatMessage mirrors a production chat message: 13 fields including a nested 10-field user struct, Dates, an enum with associated values and arrays of nested structs. FeedRowID is a String-raw enum plus a String key.

Pixel stock Pixel patched
[ChatMessage] × 50 getter 70 µs 49 µs 1.43×
[ChatMessage] × 50 getter + read 4 fields per row 349 µs 331 µs 1.05×
[FeedRowID] × 200 getter 147 µs 112 µs 1.31×

Once 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 (NewObjectA on a class with finalize()) and the SwiftValueTypeBox.

How this was measured

A benchmark test in SkipBridgeToKotlinSamplesTests (not part of these PRs) reads a bridged BenchModel from the transpiled Kotlin test: scalar, String, struct and 200-element array getters, plus == on two 200-element [Struct] lists (what Compose's changed() does). Each row is the best of 15 trials, two interleaved stock/patched rounds.

  • Device: Pixel 10 Pro Fold, Android 17, connectedAndroidTest with testBuildType = "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.
  • Mac: the same test under Robolectric, Swift built with -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).
  • Polymarket's Android app builds against it (assembleDebug).

Related: skiptools/swift-jni#11, skiptools/skipstone#292 (independent; each can land on its own)

🤖 Generated with Claude Code

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant