meryngii.neta

今日も新たな"ネタ"を求めて。

JNIの自動リソース解放

久々に記事を書こうと思い立ったので書きます。ブログという形態が情報共有に適切なのかどうかはさておいて、小さなひらめきであっても何らかの形で記録していかなければ自分の糧にならない、と改めて感じている今日この頃です。

最近Android NDKで作業する機会を得ていまして、C++をモリモリ書ける喜びを感じています。Androidデバイス本来の力を生かすにはネイティブコードは現状必須です。初期のAndroidのモッサリ感は、限られた資源の中でわざわざJava VM上にアプリケーションを実行しているせいだったのではないかと勘ぐることもしばしばです。
ネイティブコードを利用したAndroidアプリケーションを作るにはJNI (Java Native Interface)を使うことが必須となるのですが、このJNIがいろんな意味で非常に厄介です。JNIの問題点を適当にあげつらってみます。

  • グルーコードを書くのが非常に面倒。(アプリケーションの本質でないところに労力をさく必要)
  • ネイティブ領域で各種IDをいちいちキャッシュしないと実行速度が大幅に低下する。
  • JavaC++も静的コンパイルされる言語なのに、実行時まで正常に関数が呼び出されるかわからない。(失敗するとUnsatisfiedLinkErrorがthrowされる)
  • ローカル参照は512個まで。(DeleteLocalRefで解放せずにそれ以上確保しようとすると即死)

以上のような内容に関して、IBMのサイトの以下の記事が参考になります。多くのこういったバッドノウハウ系の記事は読む気がしないものですが、この記事に関しては隅々まで非常に実践的な内容なのでおすすめです。
http://www.ibm.com/developerworks/jp/java/library/j-jni/

今回は、文字列と配列アクセスのグルーコードを書く手間を削減する方法についてです。大した内容ではないですが、一応ソースコードGitHubにあげてあります。
https://github.com/meryngii/jni_util
Javaから渡される文字列と配列へのアクセスには、事前にネイティブ領域にロードする関数を呼び出す必要があります。そしてさらに、ネイティブ関数が終了するときには解放処理の関数をまた呼び出す必要があります。こんな感じで。

extern "C"
JNIEXPORT void JNICALL Java_Foo_bar
  (JNIEnv* env, jobject thisj, jintArray arrayj)
{
    jboolean isCopy;
    int* elements = env->GetIntArrayElements(arrayj, &isCopy);
    // Do something with array...
    env->ReleaseIntArrayElements(arrayj, elements, 0);
}

C++プログラマとしてはRelease系の関数を直接呼び出したくないもので、これはRAII的にデストラクタで処理させるものでしょう。同じ考えのコードは探してみたらありました。
JNI C++ templates to acquire/release resources
あといちいち要素の型名を書かなければいけないのも気に食わないですね。ここはテンプレートでうまいことやるべきでしょう。というわけで以下のように。

#include "jni_util.hpp"

extern "C"
JNIEXPORT void JNICALL Java_Foo_bar
  (JNIEnv* env, jobject thisj, jintArray arrayj)
{
    jni_util::CopiedArrayAccess<jint> array(env, arrayj);
    int* elements = array.elements();
    size_t size = array.size();
    // Do something with array...

    // Destructor automatically releases the resource
}

まあ結局jintとか書いてる訳ですが…。本当はちゃんとiteratorをつけてコンテナとしての要件を満たすようにしようかなとも思うのですが、とりあえず実用上はこれで十分でしょう。
JNIでのJava配列へのアクセスには2種類あって、

  • Get***ArrayElementsで一気にコピーする
  • Get***ArrayRegionで少しずつコピーする

の2種類あります。それぞれ適当にCopied, Regionalと名前をつけて対応させておきました。
Release時の第3引数はmodeで、0, JNI_COMMIT, JNI_ABORTから選べます。0にしてReleaseしないと変更した値がJava側で反映されません。読み出しのみの場合はJNI_ABORTでOKです。*1modeに関してもコンストラクタに与える第3引数として付加してあります。

とりあえずこんなところで。

*1:JNI_COMMITって何のために使うんだろう…。

参照カウントとバッファ再利用

std::shared_ptrの参照カウントを、バッファメモリの再利用に応用できるのではないかというお話。例として、単純なリソースクラスとそれのローダを考えてみます。

#include <vector>
#include <memory>
#include <numeric>

class Resource
{
public:
    std::size_t size() const { return data_.size(); }

    void resize(std::size_t size) { data_.resize(size); }

    std::vector<int>::iterator begin() { return data_.begin(); }
    std::vector<int>::iterator end() { return data_.end(); }

private:
    std::vector<int> data_;
};

class Loader {
public:
    std::shared_ptr<Resource> load();
    
private:
    std::shared_ptr<Resource> resource_;
};

std::shared_ptr<Resource> Loader::load()
{
    if (!resource_.unique()) // <===
        resource_ = std::make_shared<Resource>();
    
    resource_->resize(100);
    std::iota(resource_->begin(), resource_->end(), 0); // prepares data
    
    return resource_;
}

void f()
{
    Loader loader;
    
    // load and release
    for (int i = 0; i < 100; i++) {
        auto resource = loader.load();
        
        // "loader" reuses "resource_"
        // because "resource" is released here.
    }
}

void g()
{
    Loader loader;
    
    // load and store
    std::vector< std::shared_ptr<Resource> > gotResources;
    for (int i = 0; i < 100; i++) {
        auto resource = loader.load();
        gotResources.push_back(resource);
        
        // "loader" does not reuse "resource_"
        // because it's still shared with gotResources,
    }
    
    // ...
}

毎回同じデータが返ってくるくだらないコードですが…。
話の肝は<===と書いてあるところの2行だけです。要はshared_ptrの指している先がunique(=参照カウントが1)だったら、自分しか使ってない訳だからリソースを再利用してもいいじゃん、というわけです。
参照カウントが0(=何も持っていない)ときにもuniqueにはならないので、nullの場合も自動的に確保されます。
大きなデータを受け取る場合は、利用側であらかじめ一時バッファ変数を用意しておいて、内部処理でそこに入れてもらうというやり方が一番よく使われますし簡単です。しかし、メモリを再利用したい場合は利用側が適切に管理する必要がありますし、バッファの型も一つに固定されます。上の例のようにshared_ptrで利用者にバッファを返却し、その利用状況を逐一監視しておけば、もう少し柔軟に運用できるんじゃないかというわけです。
shared_ptrの参照カウントを、単なるメモリの確実な解放だけでなく別の面白い用途に使えないかなあと考えたお話でした。
もっとも、上の例は一番単純な例で、もう少しうまくやるにはバッファプールなどを作る必要があると思います。ちなみに、weak_ptrだと解放されてしまうのでうまくいきません。