たくさんの自由帳
Androidのお話
たくさんの自由帳
投稿日 : | 0 日前
文字数(だいたい) : 16687
目次
本題
はじめに
Android で Rust を呼び出す方法
環境
Rust
Rust を入れる
HelloWorld
VSCode
カニさんに挨拶されたい
Android Kotlin から Rust を呼び出す
Android NDK を入れる
JNI
JNI Android プロジェクトを作成
JNI Rust 側も作成
JNI Cargo.toml
JNI lib.rs
JNI ビルド
JNI 共有ライブラリを回収
JNI AndroidStudio のプロジェクトに共有ライブラリを入れる
JNI 呼び出してみる
JNI 実行
JNI は長すぎる
UniFFI
UniFFI 同様にカニさんプロジェクトを作る
UniFFI カニさんコードを書く
UniFFI Kotlin で使いたい関数に目印をつける
UniFFI Kotlin バインディングを生成する準備
UniFFI バインディングを生成する
UniFFI ビルド
UniFFI AndroidStudio のプロジェクトに共有ライブラリを入れる
UniFFI バインディングをコピーしてくる
UniFFI 呼び出してみる
UniFFI 実行
カニさんソースコード
Android NDK だと速いの?
SIMD
SIMD 命令のプログラムは難しくないんですか
関数
付録 ところでコンパイラが SIMD 命令を使っているか確認したい
付録 SIMD を無効にしてビルドすると
付録 ブラウザでアセンブリコードを見たい
SIMD 実験
アプリ作った
実験結果
Snapdragon 8 Gen 2 (Xperia 1 V)
Google Tensor G3 (Pixel 8 Pro)
まとめ
実験ソースコード
Q&A 16KB ページサイズに対応してないんですけど
おわりに
おわりに2
どうもこんばんわ。
スカイコード 攻略しました。OP曲がかっこよくて気になってたやつ
この先どうなるんや、、が続くゲームでよかった(語彙力。作中じゃないけどシナリオに惹かれた
?な発言もちゃんと回収してた
天使ちゃんかわいい。
重いシーンが何個かあって個別あるか心配だった、あります。
!!!あがっちゃうシンジュちゃん
かわいい!!
あと天使ちゃんルートの中間曲がとてもいい!!シンジュちゃんのところ
しばらくリピートしてる、CD買ってよかった。
えちえちシーンも結構良かった!!!、、(けど本筋の話が気になってそれどころじゃないよ;;)
それはそうと、何回か(も?)気持ちが揺さぶられて疲れた、、、。休み明けのシンジュちゃんのやつは結構効いた
シンジュちゃんEDの後はどうなったのか、、な
ふと、Rust 言語を書いてみたくなった。
というのも、Android 開発者は難しいC++で処理を書いてJNIで繋いで呼び出せば速くなると盲目的に思ってる(節がある)(私だけか。)
ただC++難しいそうだなあ~思ってたところ、RustをAndroid の CPU向けにクロスコンパイル出来るらしく、しかも簡単な方法があると聞いた。
本当に速いか確かめます。
この記事で言うネイティブコードは、クロスプラットフォームの人たちが言う 各プラットフォームの開発言語 (Swift / Kotlin) の事では無く、C++やRustのような Android NDK が必要なコードのことを指します。
ネイティブライブラリも同様にRustをコンパイルしたやつを指します。
調べた感じ、2つくらいRustコードを呼び出す方法があるっぽい。
前者の古い方がC++と同じような感じで、Java Native Interface (JNI)の形式で関数を書いてコンパイルしてって感じのやつ。
後者がRustコードを少し書き足すだけでRustとKotlinを繋ぐバインディングを自動で生成してくれるというもの。自動生成とは言え使うのは難しくない。
ただ、前者はJNIなので複雑、後者はJNIとは別のJava Native Access (JNA)を採用しているためか速度が出ないという欠点があります。
今回のように速度を出して欲しいときはJNIの方を使う必要があるかも。
Windowsを使いますが、どうやらWindowsでRustするにはVisual Studio Installerを経由して数GBのビルドツールを入れないといけないらしい。(本当?)without Visual Studio Installerでセットアップする方法ないのかな...
Rust をインストール
効率的で信頼性のあるソフトウェアを書く力を与える言語
数回しか使わないのに面倒すぎるので、今回はWSL 2をインストールしLinuxでRustすることにします。今回Android向けにビルドするため、別にWindowsで動く必要ないので。容量無くなったらすぐ消したいしで
また、Rustを書く際にVSCodeを使います。何でも良いです。
| なまえ | あたい |
|---|---|
| パソコン | Windows 10 Pro |
| Rust 開発 | WSL 2 ( Ubuntu ) |
| Android 端末 | Pixel 8 Pro / Xperia 1 V |
| Rust | rustc 1.85.0 (4d91de4e4 2025-02-17) |
| UniFFI | 0.29 |
| Android NDK | 27 ( WSL2でRustをビルドする場合はWindows側は不要 ) |
DiskInfo3のお陰で、Cドライブを少し開放できたのでWSL 2を入れます。ちょいまって
古いAndroid Studioの残骸を消したら空いた。
Rustほとんどやったこと無いので、まずはチュートリアルをこなしてみる。Rustのマスコットキャラクター?のカニが挨拶をしてくれるプログラムを作るらしいです。
Linuxなのでこっち。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shそのままエンターでいいはず。
1) Proceed with standard installation (default - just press enter)
2) Customize installation
3) Cancel installationRust is installed now. Great!って表示されれば良いはず。
ターミナルを再起動します。面倒なのでexitで抜けて再起動するか。
あと私の場合はerror: linker cc not foundエラーが表示されたため、以下のコマンドを叩く必要がありました。
sudo apt update
sudo apt upgrade
sudo apt install -y build-essentialこれをやってRustの感覚を掴んでみる。
カニのマスコットが挨拶してくれるプログラムを作るらしい。
新しいプロジェクトを作って
cargo new hello-rust移動して実行
cd hello-rust
cargo buildHello Worldできた
WSL2上でvimを使うのは厳しいので、VSCodeとWSL2を接続してVSCodeで開発できるようにします。
Developing in WSL
Using Visual Studio Code Remote Development with the Windows Subsystem for Linux (WSL)
https://code.visualstudio.com/docs/remote/wsl
VSCode側に拡張機能をあらかじめインストールしておいて、
WSL2上のUbuntuで、code .コマンドを叩くと、WSL2と接続したVSCodeが開きます。左下にWSL: Ubuntuって出てる!!!
すごく統合されていてこの手の開発者は嬉しそう、わたしはAndroidなんで,,,
これの依存関係のところから。Cargo.tomlを開きdependenciesに1行足します。
[dependencies]
ferris-says = "0.3.1"そしたらcargo buildを入力することでライブラリを取ってきてくれるそう。
最後に挨拶してくるプログラムを書きます。srcの中のmain.rsをこの用に書き換えて
use ferris_says::say; // from the previous step
use std::io::{stdout, BufWriter};
fn main() {
let stdout = stdout();
let message = String::from("Hello fellow Rustaceans!");
let width = message.chars().count();
let mut writer = BufWriter::new(stdout.lock());
say(&message, width, &mut writer).unwrap();
}出来たら、ターミナルでもう一度cargo runを叩きます。
カニさんが出てきたら成功です。
アスキーアート、かわいい。
バックスラッシュが¥マークになってるけど気にしないことに。
AndroidでRustを呼び出すためには、冒頭の通りC++時代のようにJNIを使うか、UniFFIでKotlinバインディングを自動生成するかの2択っぽいです。
両方試してみますが、速度が必要ない場合はUniFFIが簡単で良かったです。
ビルドするためにはAndroid NDKをRustをコンパイルするマシンに入れておく必要があります。JNIにしろ、UniFFIにしろ必要です。

การดาวน์โหลด NDK | Android NDK | Android Developers
https://developer.android.com/ndk/downloads?hl=th
cd /opt
sudo wget https://dl.google.com/android/repository/android-ndk-r27c-linux.zip
sudo apt install unzip
sudo unzip android-ndk-r27c-linux.zipダウンロード先はどこにするのがいいのか知らないので、他プロジェクトにならって/optにしてみた。
多分sudoが必要です。
unzipコマンドが使える場合はapt installの行はスキップできます。
少し古いですが、この記事通りにやることが出来ます。
C++をAndroidでやったことある方ならわかるかもしれませんが、JNIの関数はクソ長いです。JNIの関数の命名規則は、パッケージ名、クラス名、関数名を知っている必要があるので、先にAndroidプロジェクトを作ります。ちなみに後者のUniFFIならもっと簡単です。
今回はWSL2でRustをコンパイルするため、Nativeを選択する必要はありません。WindowsでRustをコンパイルする場合もSDK ManagerからAndroid NDKをインストールすればいいはずな気がするので、Nativeを選ぶ必要はないと思います。
次に、Kotlinでexternal fun greeting(message: String)関数を作ります。今回はMainActivityで。
せっかくなので挨拶文を引数で設定できるようにしてみました。また、カニさんのアスキーアートを文字列で受け取るよう返り値はStringです。
この関数の中身をRustで実装する形になります。
package io.github.takusan23.androidrustjni
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AndroidRustJniTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
// これ
private external fun greeting(message: String): String
}で、なんで先にプロジェクトを作ったかと言うと、この関数名、クラス、パッケージ名を確定させておく必要があるからです。
とりあえず次はRustコードを書きましょう。
まずはそれ用のRustプロジェクトを作ります。VSCodeを開きます。
cargo new android-rust-jni
cd android-rust-jni/
code .Cargo.tomlを開き
[lib]の2行も書き足します。dylibにするとビルド時に.soが生成されるようになります。
次に、同様にカニさんに挨拶されたいのでライブラリを入れます。同じ用に[dependencies]の下に足します。
また、JNI用のライブラリも追加します。下の2行ですね。
[lib]
crate-type = ["dylib"]
[dependencies]
ferris-says = "0.3.1"
[target.'cfg(target_os="android")'.dependencies]
jni = { version = "0.21.1", default-features = false }次にmain.rsの名前をlib.rsにします。
(cargo newの時点でlibの方を作る方法があったような気もします。)
ここからRust + JNIコードを書いていきます。
で、ここで、さっき作ったAndroidプロジェクトを確認する必要があります。
というのもここから追加で書く関数が、Kotlin側のexternal funと紐付くわけですが、
名前にルールがあります。こちらです。
Java_{パッケージ名。ドットはアンダーバーに置き換え}_{クラス名}_{関数名}例えば、このKotlinコードだと
package io.github.takusan23.androidrustjni
class MainActivity : ComponentActivity() {
// これ
private external fun greeting(message: String): String
}io.github.takusan23.androidrustjniMainActivitygreetingになるので、これをルール通りに当てはめるとこうなります。
Java_io_github_takusan23_androidrustjni_MainActivity_greeting関数名が分かったところでコードを書いていきましょう。lib.rsに書き足します。Java_io_github_takusan23_androidrustjni_MainActivity_greetingの部分は、各自パッケージ名と関数名を直してください。
use ferris_says::say;
fn rust_greeting(message: String) -> String {
let width = message.chars().count();
let mut writer = Vec::new();
say(&message, width, &mut writer).unwrap();
return String::from_utf8(writer).unwrap();
}
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android {
extern crate jni;
use jni::JNIEnv;
use jni::objects::{JClass, JString};
use crate::rust_greeting;
#[unsafe(no_mangle)]
pub extern "C" fn Java_io_github_takusan23_androidrustjni_MainActivity_greeting<'local>(
mut env: JNIEnv<'local>,
_class: JClass<'local>,
message: JString<'local>,
) -> JString<'local> {
// Rust String へ
let rust_string: String = env
.get_string(&message)
.expect("Couldn't get java string!")
.into();
// カニさん
let greeting_string = rust_greeting(rust_string);
// JString を返す
return env.new_string(greeting_string).unwrap();
}
}JNIの関数はJNI用のプリミティブ型を使う必要があります。
これはRustに限らずC++で書いてもjstringとか言うのになる。
そしたらビルドします。
と、その前にAndroidをターゲットに追加します。多分この4つ?
上からARM 64ビット、ARM 32 ビット、x64、x86。のハズ?x64とかはWindowsでエミュレータを動かした時にIntel CPUなので多分いる。
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
rustup target add i686-linux-android次に、Cargo.tomlの階層に.cargoフォルダを作成し、config.tomlを作成します、
そしたら、以下を貼り付けます。NDKのパスが違う場合は直してください。
[target.aarch64-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
[target.armv7-linux-androideabi]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang"
[target.x86_64-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang"
[target.i686-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android21-clang"そしたら各CPU向けにビルドできるようになっているはずです。
以下のコマンドを順番に叩けばいいはず。
cargo build --release --target aarch64-linux-android
cargo build --release --target armv7-linux-androideabi
cargo build --release --target x86_64-linux-android
cargo build --release --target i686-linux-androidFinished release profile [optimized] target(s) in 8.72sみたいなのが出ればいいはず。
あともうちょっと!
.soファイルを回収します。targetフォルダ内に各 CPU向けのフォルダがあるので開いて、releaseの中のlib から始まる 拡張子 soのファイルを取り出します。WSL2ならエクスプローラーから見れるのでラクラクです。
app→src→mainフォルダへ進み、jniLibsフォルダを作成します。
また、その中に以下4つの名前でフォルダを作ってください。
ファイルツリーの表示をProjectにした時に、この場所にフォルダが作られていれば大丈夫です。
そしたら今作った4つのフォルダに、それぞれさっき取り出したsoファイルを配置します。
armeabi-v7a なら armv7-linux-androideabiarm64-v8a なら aarch64-linux-androidx86 なら i686-linux-androidx86_64 なら x86_64-linux-androidarm64-v8aならaarch64-linux-androidフォルダのreleaseの中にあったsoファイルといった感じです。
カニさんに挨拶されたいことを忘れかけてた。もう見れますよ。MainActivity.ktで共有ライブラリをロードするようにすれば完了です。あとはexternal fun greeting()を呼び出して使いましょう。
System.loadLibrary()ですが、共有ライブラリの名前から先頭のlibと.soを消した名前を渡す必要があります。
ちなみに、RustとKotlin (JNI)名前が間違ってる場合はjava.lang.UnsatisfiedLinkError: No implementation found見たいな例外が投げられます。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AndroidRustJniTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
// Rust のカニさんが挨拶してくれる
Text(
text = greeting("Hello Android + JNI + Rust"),
modifier = Modifier.padding(innerPadding),
fontFamily = FontFamily.Monospace
)
}
}
}
}
// Rust 側の実装
private external fun greeting(message: String): String
// Rust でビルドした共有ライブラリをロード
companion object {
init {
System.loadLibrary("android_rust_jni")
}
}
}これで完了です。早速実行してみましょう。
カニさん出た。
試しに32ビットのAndroid端末でも動かしてみた。(Xperia Z3 Compact)
動いてます。
画像送るほうが大変、、、Android ビーム (懐かしい)を経由してQuickShare。x86_64の.soファイルもちゃんと同梱したので、Windowsでエミュレーターを使ったときの開発でも特に問題なく実行できるはずです。
これが嫌な場合はUniFFIを採用するべきです、少しはラクできるはず。
こちらはRustとKotlinを繋ぐ部分を自動で作ってくれます(Kotlin バインディング)。
しかも簡単。
JNIと同じように、それ用のプロジェクトをcargo newします。
cargo new android-rust-uniffi
cd android-rust-uniffi次にCargo.tomlを開いて書き足します。cdylibで共有ライブラリをビルド、uniffiは記述時時点最新版を入れます。あとはカニさんのライブラリを。
[lib]
crate-type = ["cdylib"]
name = "android_rust_uniffi"
[dependencies]
uniffi = { version = "0.29", features = [ "cli" ] }
[build-dependencies]
uniffi = { version = "0.29", features = [ "build" ] }srcの中のmain.rsをlib.rsにリネームして、カニさんプログラムを書きます。JNIのそれと同じです。カニさんのアスキーアートを文字列で返す。
use ferris_says::say;
pub fn rust_greeting(message: String) -> String {
let width = message.chars().count();
let mut writer = Vec::new();
say(&message, width, &mut writer).unwrap();
return String::from_utf8(writer).unwrap();
}今回はpub fn rust_greeting()関数をKotlinから呼び出したいので、#[uniffi::export]をつけます。
もう一つ、本家ではUDL ファイルを作る方法も紹介されてますが、#[uniffi::export]付けるのが楽だと思います。
#[uniffi::export]
pub fn rust_greeting(message: String) -> String {
// 以下省略...
}それから、lib.rsの一番最初にuniffi::setup_scaffolding!();の一行を書き足します。
これが、ここまでの状態のコードです。
uniffi::setup_scaffolding!();
use ferris_says::say;
#[uniffi::export]
pub fn rust_greeting(message: String) -> String {
let width = message.chars().count();
let mut writer = Vec::new();
say(&message, width, &mut writer).unwrap();
return String::from_utf8(writer).unwrap();
}もうゴールは近い。
次はKotlinでRustコードを呼び出すバインディングを生成するために使うファイルを作ります。
Cargo.tomlと同じ階層にuniffi-bindgen.rsファイルを作成して、以下を貼り付けます。
fn main() {
uniffi::uniffi_bindgen_main()
}次に、Cargo.tomlに書き足します。
[[bin]]
# This can be whatever name makes sense for your project, but the rest of this tutorial assumes uniffi-bindgen.
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"バインディングのために使う.soファイルは多分ターゲット Android向けである必要がない?
以下の2つのコマンドを実行します。soファイルのパスは各自直してください。
cargo build --release
cargo run --bin uniffi-bindgen generate --library target/release/libandroid_rust_uniffi.so --language kotlin --out-dir out終わると、outフォルダの中に.kt(Kotlin)コードが入ってるはず!
rustup targetで一回も追加したことない場合は呼び出します。
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
rustup target add i686-linux-android次に.cargoフォルダを作りconfig.tomlを作成し、以下を貼り付けます。NDKのパスが違う場合は直してください。
[target.aarch64-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android21-clang"
[target.armv7-linux-androideabi]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang"
[target.x86_64-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android21-clang"
[target.i686-linux-android]
ar = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
linker = "/opt/android-ndk-r27c/toolchains/llvm/prebuilt/linux-x86_64/bin/i686-linux-android21-clang"これでAndroid向けにビルド出来るようになりました。
4つのコマンドを順番に叩いて、各アーキテクチャ向けにビルドした共有ライブラリを作ります。
cargo build --release --target aarch64-linux-android
cargo build --release --target armv7-linux-androideabi
cargo build --release --target x86_64-linux-android
cargo build --release --target i686-linux-androidAndroid Studioでプロジェクトを作ってください。Jetpack Composeのやつを選んでいいです。Nativeである必要もないです。ビルド済みRustを使うので。
.soファイルを置くためのフォルダを作ります。
これも JNI AndroidStudio のプロジェクトに共有ライブラリを入れる と同じです。そしたら今作った4つのフォルダに、soファイルを配置します。soファイルはtarget/{各アーキテクチャ}/releaseの中にあります。
armeabi-v7a なら armv7-linux-androideabiarm64-v8a なら aarch64-linux-androidx86 なら i686-linux-androidx86_64 なら x86_64-linux-android例えばarm64-v8aならtarget/aarch64-linux-android/release/の中にあるsoファイルを置けばいいです。
と、その前に、UniFFIを動かすにはJava Native Access (JNA)というライブラリに依存しているので、まずapp/build.gradle.ktsを開いてJNAを追加します。そしたらGradle syncね。
dependencies {
implementation("net.java.dev.jna:jna:5.6.0@aar")
// 以下省略...次にUniFFIが生成したバインディングを持ってきましょう。outフォルダを探すとあるはず。out/uniffi/android_rust_uniffiにありました。これをMainActivity.ktと同じ階層に貼り付ける。
package 名を直してもいいはず。JNAはJNIのときとは違い、パッケージ名が違ったとしても動くはず。
バインディングがあるので、対応する関数が見つかるはず。rustGreeting()関数ですね。JNIのときの用にしてみる。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
AndroidRustUniffiTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Text(
text = rustGreeting("Android UniFFI + Rust !!!"), // 今回作ったコードはトップレベルにあった
modifier = Modifier.padding(innerPadding),
fontFamily = FontFamily.Monospace
)
}
}
}
}
}UniFFIでもカニさんが挨拶してくれました。良かった。
また、使いたい関数にに少し書き出すだけでKotlin (JNI)で呼び出せることに気付きましたか?JNIのときはJavaのプリミティブ型jstringとかを使う必要があったのに対し、UniFFIは気にする必要がない。。
また、JNIの長い関数名のルールもありません。が、ほんとはJNIでも長い関数名を回避できる、、、
というわけで、今回はひたすら計算する処理をRustで書いて、更にSIMD 命令の恩恵を受けようと思います。
CPUにはSIMD命令とか言うのがあって、複数の値に対して同時に四則演算ができるとかなんとか。
どういうことかというと、こんな感じにInt 配列の中身を2倍にしたい場合はまあこうですよね。
val a = listOf(1, 2, 3, 4).map { it * 2 }しかし、これでは掛け算が4回必要です。
そこでSIMDです。以下のコードは擬似コードなので動かないですが、どうやら一回の掛け算処理で、4つの値全てに対して計算ができる、らしい。
val a = listOf(1, 2, 3, 4) * 2しかし、このSIMD命令をJavaから使う方法はない?ので、Rustで書いて使うことにします。
そう思ってたのですが、、、、
実は単純なforループの場合はコンパイラが勝手にSIMDを使ったものに置き換えてくれるそうです。
というか今回はコンパイラにやらせる作戦で行きます。自分でSIMD用の関数を呼ぶくらいならやらない。
単純というか副作用がないコードじゃないとならないらしい?forの中で関数呼び出しとかするとダメそう。
これまでと同じようにcargo newでプロジェクトを作って、lib.rsに関数を作りました。
2つのバイト配列から取り出して引き算するやつ。テストも正解だけだけど書いた。
use std::cmp;
// 2つのバイト配列を取って、引き算した結果を返す
fn sub_two_bytearray(a: Vec<u8>, b: Vec<u8>) -> Vec<u8> {
let size = cmp::min(a.len(), b.len());
let mut result: Vec<u8> = vec![0; size];
for i in 0..size {
result[i] = a[i] - b[i];
}
return result;
}
// テストコード
#[cfg(test)]
mod tests {
use std::vec;
use crate::sub_two_bytearray;
#[test]
fn it_works() {
let a: Vec<u8> = vec![10, 10, 10];
let b: Vec<u8> = vec![1, 2, 3];
let result = sub_two_bytearray(a, b);
assert_eq!(result, vec![9, 8, 7]);
}
}これをJNIとUniFFIで試して速いか確かめます。
そのためには、アセンブリコードを読む必要があるのですが、cargo buildはデフォルトでは生成されないのでオプション付きでコマンドを叩きます。aarch64で作ってみます。なお、releaseの場合は多分SIMDに書き直すのが有効になります。
RUSTFLAGS='--emit asm' cargo build --release --target aarch64-linux-androidtarget/aarch64-linux-android(アーキテクチャ)/release/deps/の中に.sファイルがあるはず。
これを読みます。一部を抜き出してみました。
.LBB0_16:
ldp q0, q3, [x12, #-16]
subs x14, x14, #32
ldp q1, q2, [x13, #-16]
add x12, x12, #32
add x13, x13, #32
sub v0.16b, v1.16b, v0.16b
sub v1.16b, v2.16b, v3.16b
stp q0, q1, [x11, #-16]
add x11, x11, #32
b.ne .LBB0_16
cmp x21, x10
b.eq .LBB0_6
tst x21, #0x18
b.eq .LBB0_4、、、、が、この辺さっぱり分からんのでAIに聞いた。
Geminiさん曰く、ARM NEON(ARM の SIMD の名前)の場合は、アセンブリを見てq0みたいな名前の付くレジスタを使っているらしい。
これを見るとldp q0ってところでq0レジスタを使ってるので確かにそうなのかもしれない?
cargoはRUSTFLAGS="-C target-feature=+hogehoge"でCPUの機能が有効にできますが、逆にマイナス-hogehogeすると無効にできるらしい。
というわけでこの場合のアセンブリコードもみてみます。SIMD 命令、ARM アーキテクチャだとNEONって名前なのでneon。Intelだとまた別。
RUSTFLAGS='--emit asm -C target-feature=-neon' cargo build --release --target aarch64-linux-android同じ様にアセンブリコードを見てみると、q0とかの文字がなくなってそう。
Compiler Explorer
で、アセンブリコードを見ることが出来る。Rustを選ぶ。Compiler Optionsに
-C opt-level=3 -C target-feature=+neon --target aarch64-linux-androidを入れると見れるはず。neon無効は-neonで。
というわけで、今回は結構前にやったボーカルあり曲とカラオケ曲を使ってボーカルのみ曲を作る記事の、
ボーカル曲からカラオケ曲を引き算し、ボーカルだけにしてる計算部分だけをRustで書きます。
音は波なので、足したり引いたり出来ます。
これを応用して、aacをPCMにデコードして、引き算して、aacにエンコードすれば出来るはず。
試す内容としてはJNI SIMD あり、UniFFI SIMD あり、Kotlin リリースビルド、JNI SIMD無効で試します。
あと、そもそもRustやC++を使ったネイティブコードが、本当に速いのか確かめるためにKotlinも。あとSIMDの有り無しを見たい。
使う端末はXperia 1 VとGoogle Pixel 8 Pro。
バイト配列の大きさは47290528 バイト (45MB)。これを引き算していく。
というわけで作りました。
音声のエンコード、デコードは前回の記事のままなので、そちらを見るか、後述するGitHubでソースコードを見てください。
新しいのほちい
| Rust + JNI | Rust + UniFFI | Kotlin | Rust + JNI ( SIMD 未使用 ) |
|---|---|---|---|
| 66 ms | 446 ms | 128 ms | 94 ms |
| 59 ms | 459 ms | 22 ms | 103 ms |
| 72 ms | 440 ms | 20 ms | 96 ms |
| 61 ms | 444 ms | 21 ms | 100 ms |
| 57 ms | 440 ms | 21 ms | 94 ms |
同期のSoCよりも性能がちょっと低い。
| Rust + JNI | Rust + UniFFI | Kotlin | Rust + JNI ( SIMD 未使用 ) |
|---|---|---|---|
| 131 ms | 572 ms | 213 ms | 172 ms |
| 121 ms | 567 ms | 78 ms | 192 ms |
| 121 ms | 563 ms | 70 ms | 181 ms |
| 151 ms | 572 ms | 58 ms | 177 ms |
| 122 ms | 561 ms | 53 ms | 199 ms |
この規模だとKotlin (JVM)が一番速かった。(というか題材にこれを選んだのが良くなかった?)JVMが初回を除いてなぜか速い。もっと大規模だと違うのかもしれない。
C++で試せてないのであれですが、別にネイティブコードを書いても速くなるわけじゃないのか、、、
Android の JVMすごい。
UniFFIはJava Native Access (JNA)都合で遅くなってる?JNI独自の型(jstring)とかを使わずに呼び出せるので、それで時間がかかってるのかも?
.soファイルをapk / aabの中に入れることになるのでこの問題にぶち当たります。
Hello Android 15。16KB ページサイズ編 - たくさんの自由帳
https://takusan.negitoro.dev/posts/android_15_16kb_page_size/
takusan23@DESKTOP-ULEKIDB:~$ chmod +x check_elf_alignment.sh
takusan23@DESKTOP-ULEKIDB:~$ ./check_elf_alignment.sh app-release.apk
Recursively analyzing app-release.apk
NOTICE: Zip alignment check requires build-tools version 35.0.0-rc3 or higher.
You can install the latest build-tools by running the below command
and updating your $PATH:
sdkmanager "build-tools;35.0.0-rc3"
=== ELF alignment ===
/tmp/app-release_out_6q4W9/lib/x86_64/libandroid_jni.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86_64/libandroid_jni_without_simd.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86_64/libandroidx.graphics.path.so: ALIGNED (2**14)
/tmp/app-release_out_6q4W9/lib/x86_64/libjnidispatch.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86_64/libandroid_rust_uniffi.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/armeabi/libjnidispatch.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libandroid_jni.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libandroid_jni_without_simd.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libandroidx.graphics.path.so: ALIGNED (2**14)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libjnidispatch.so: ALIGNED (2**16)
/tmp/app-release_out_6q4W9/lib/arm64-v8a/libandroid_rust_uniffi.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86/libandroid_jni.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86/libandroid_jni_without_simd.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86/libandroidx.graphics.path.so: ALIGNED (2**14)
/tmp/app-release_out_6q4W9/lib/x86/libjnidispatch.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/x86/libandroid_rust_uniffi.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/mips64/libjnidispatch.so: ALIGNED (2**16)
/tmp/app-release_out_6q4W9/lib/mips/libjnidispatch.so: ALIGNED (2**16)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libandroid_jni.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libandroid_jni_without_simd.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libandroidx.graphics.path.so: ALIGNED (2**14)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libjnidispatch.so: UNALIGNED (2**12)
/tmp/app-release_out_6q4W9/lib/armeabi-v7a/libandroid_rust_uniffi.so: UNALIGNED (2**12)
Found 16 unaligned libs (only arm64-v8a/x86_64 libs need to be aligned).
=====================
takusan23@DESKTOP-ULEKIDB:~$今回作った.soが軒並みUNALIGNED。。。
Java / Kotlinで十分に速かった。Android NDKを入れて、Java Native Interfaceのバインディングを書いて、各 CPU アーキテクチャ向けにコンパイルして、、、って面倒だしクラッシュした時に直せる自信がない。GitHubのコードでNDKが必要って言われた瞬間に目を逸らしてる(偏見)
そーいえば昔というか、32bit時代の、ネイティブコードを使っててarm64-v8a(ARM 64bit)が入ってないアプリ、すでに動かなくなってそう。Google TensorはG2 ( Pixel 7 )から32 ビットネイティブコードのサポート無し、Snapdragonも8 Gen 3で32 ビットネイティブコードのサポートが無くなった。
もっとも、targetSdkが古すぎてインストールが弾かれてしまいそうなので、こんなことにはならないと思います、が。