はじめに
クロスプラットフォームなフレームワークのFlutterには、
アプリから各プラットフォームのNative機能を呼び出すためのPlatform Channelという仕組みがあります。
AndroidではJava/Kotlin, iOSではObj-C/Swiftの機能を呼び出すことができます。
一方、アプリからC/C++の関数を呼び出すためのdart:ffiという仕組みもあります。
FFIはForeign Function Interfaceの略で、DartからC/C++言語を呼び出す仕組みです。
さて、Flutter on Linuxは(現状)GTKのアプリとして動作します。
つまり、Platform Channelで呼び出される処理もC/C++言語で記述します(詳細はこちら)。
そこで、大きなデータをC/C++言語で処理した場合のパフォーマンスを
Platform Channelの一つであるMethod ChannelとFFIとで比較してみました。
結論
今回測定した条件では、Dart-C間でデータ変換が必要な場合、
Method ChannelとFFIで有意な差は見られませんでした。
一方で、データ変換が不要な場合はFFIだとasTypedListを使って、
Dart/Cの両側からアクセスしつつデータコピーの回数を減らすことができます。
当然データサイズが大きければ大きいほどその効果は大きく、今回の測定条件では約100倍FFIの方が速い結果になりました。
少なくとも大きなデータをDart-C間でやり取りする必要がある場合、
FFIでコピー回数を減らす設計にできないかを考えたほうが良さそうです。
環境
下記環境で実験しました。
Flutterの動作platformはlinuxです。
$ grep 'model name' /proc/cpuinfo | uniq -c
8 model name : Intel(R) Core(TM) i5-8259U CPU @ 2.30GHz
$ grep MemTotal /proc/meminfo
MemTotal: 32752704 kB
$ uname -a
Linux chama 5.5.8 #1 SMP Sat Mar 7 22:29:22 JST 2020 x86_64 x86_64 x86_64 GNU/Linux
$ lsb_release -d
Description: Ubuntu 18.04.5 LTS
$ flutter --version
Waiting for another flutter command to release the startup lock...
Flutter 1.24.0-8.0.pre.143 • channel master • https://github.com/flutter/flutter
Framework • revision e4d94f7ccd (2 hours ago) • 2020-11-07 12:15:22 +0100
Engine • revision 1e3ceb037f
Tools • Dart 2.12.0 (build 2.12.0-27.0.dev)
計測結果
Dart側で確保したバッファにC/C++側でmemsetしDart側に返す時間を計測しました。
計測に使用したプログラムはこちらに公開しています。
実装は4種類試しています。
- 実装1. Method Channel
- 実装2. FFI (Dart-C間でデータを変換)
- 実装3. FFI (asTypedListを使いDart-C間でバッファを共有)
- 実装4. FFI (C形式でのみバッファを保持)
4つの実装方法での計測結果が以下です。
横軸がmemsetするバッファのサイズ、縦軸がかかった時間(1000回平均)です。
データコピーが発生する実装1/実装2と比較して、データコピーが発生しない実装3/実装4の方が大幅に速いことがわかります。
8MBのmemsetしたときのそれぞれの時間は以下のとおりです。
便宜上[us]単位で表示していますが、少なくとも下二桁ぐらいは測定誤差があると思います。
実装 | 時間[us] |
---|---|
実装1. Method Channel | 23,182 |
実装2. FFI (Dart-C間でデータを変換) | 21,616 |
実装3. FFI (asTypedListを使いDart-C間でバッファを共有) | 251 |
実装4. FFI (C形式でのみバッファを保持) | 261 |
[参考] 完全C実装 | 274 |
参考にdebug版の結果は以下です。
実装2(FFI (Dart-C間でのデータ変換あり))の時間がrelease版に比べ顕著に長くなっています。
Dartで記述したデータ変換処理のAOTが効いていると考えられます。
このとおり軽く数倍変わるので、Flutterでパフォーマンスを計測する時は忘れずにrelease版にしましょう。
(flutter run --release
でrelease版を起動できます)
終わりに
ある程度予測していた結果にはなりましたが、
実装してみてasTypedList
を知ることができ、またrelease版の効果も肌で感じられたので良かったです。
release版の結果で、512KBのところで明らかに意味有りげに線形性が崩れてるのですが、理由はわかっていません(再現性はあります)。
MethodChannelでもFFIでも起こっているから私の実装バグの可能性は低いと思っているのですが、、、
また暇なときに理由を調べてみたいです。
あと今回は試していませんが、ffiで確保したバッファのアドレスをMethodChannelでやり取りするということも理論上はできると思います。
すでに関連処理をMethodChannelで実装してるんだけど、一部どうしても大きなデータをやり取りする必要が出てきた、
というようなケースではそのような選択もあろうかと思います。
(クロスプラットフォーム性はなくなるので本当にMethodChannelがいいのか考える必要はあると思いますが)
なお、Flutter on Linux Desktopはアルファ版、dart:ffiはベータ版の機能です。
性能はもちろん、Platform側の実装がGTKであることなどももしかしたら変わるかも知れませんので、ご容赦ください。
参考
MethodChannel class - services library - Dart API
dart:ffi library - Dart API
Is it possible to get pointer of Uint8List instead of allocating then copy · Issue #31 · dart-lang/ffi