State of foreign memory support

原文はこちら。
The original article was written by Maurizio Cimadamore (Software Architect, Oracle).
http://cr.openjdk.java.net/~mcimadamore/panama/foreign-memaccess.html

ネイティブとの相互運用のストーリーの重要な部分は、効率的な方法でオフヒープメモリにアクセスする能力に手がかりがあります。Panamaは、この目標を達成するために、いわゆるForeign Memory Access APIを使用しています。このAPIは、Java 14、15のインキュベーションAPIとして利用可能になっており、Panamaの相互運用ストーリーの中で最も成熟した部分です。

JEP 370: Foreign-Memory Access API (Incubator)
https://openjdk.java.net/jeps/370
JEP 383: Foreign-Memory Access API (Second Incubator)
https://openjdk.java.net/jeps/383

Segments

メモリ・セグメントは、Javaヒープ上またはJavaヒープ外に位置するメモリ領域をモデル化するために利用可能な抽象化です。セグメントは、ネイティブ・メモリから割り当てられたり(mallocなど)、既存のメモリ・ソースにラップされたり(Java配列やByteBufferなど)できます。メモリ・セグメントは、メモリのデリファレンスを安全にする強力な保証を提供します。より具体的には、メモリ・セグメントは以下を提供します。

  • 空間的な境界:セグメントにはベースアドレスとサイズがあり、境界外のセグメントへのアクセスは禁止されています。
  • 時間的な境界:セグメントは状態を持っています。つまり、セグメントを利用し、背後のメモリが不要になったときにセグメントをクローズできます(これは、メモリの解放を引き起こす可能性があることに注意してください)。
  • スレッド制限:セグメントはそのセグメントを作成したスレッドが所有するメモリ領域上のビューです。このスレッドの外部でセグメントのデリファレンスやクローズは禁止されています(これは、マルチスレッドのシナリオでアクセスとクローズの競合を避けるために非常に重要です)。

例えば、以下のスニペットでは100バイトのオフヒープを割り当てています。

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
    ...
} // frees memory

セグメントは AutoCloseable であるため、try-with-resources 文の中で使用できます。これにより、セグメントが不要になった時点でメモリをリリースすることを保証できます。

メモリ・セグメントはスライスをサポートしています。つまり、セグメントを指定すると、元のセグメントよりも空間的な境界を狭くした新しいセグメントを作成できます。

MemorySegment segment = MemorySement.allocateNative(10);
MemorySegment slice = segment.asSlice(4, 4);

上記のコードは、オフセット4で始まり、4 バイトの長さを持つスライスを作成します。スライスは親セグメントと同じ時間的境界を持ちます。つまり、親セグメントが閉じられると、その親セグメントから派生したすべてのスライスも閉じられます。逆も同様で、スライスを閉じると親セグメント(およびそこから派生した他のすべてのスライス)が閉じられます。ライブラリがスライスを共有したいが、クライアントがスライスを閉じないようにしたい場合(例えば、スラブ・アロケータを実装する場合などに便利です)、ライブラリは、non-closeable viewを作成することで、クライアントがスライスを閉じるのを防ぐことができます。

MemorySegment sharedSlice = slice.withAccessModes(ALL_ACCESS & ~CLOSE);

sharedSliceでcloseを呼び出そうとすると例外が発生します。メモリ・セグメントは様々なアクセスモード(読み書きアクセスを含む)をサポートしており、これを使ってクライアントが利用できる操作のセットを制限できます。

Memory access handles

メモリアクセスVarHandle(memory access var handle)を使用するとセグメントに関連付けられたメモリのデリファレンスが可能です。メモリアクセスVarHandleは特殊な種類のVarHandleで、メモリ・セグメントのアクセス座標と、バイト・オフセットを受け取ります。このオフセットは、デリファレンスを行うそのセグメントのベースアドレスからの相対的なオフセットです。メモリアクセスVarHandleは、次のようにして取得できます。

VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder());

dereferenceハンドルを作成するには、キャリアタイプを指定しなければなりません。これは、メモリから値を抽出する際などに使用したいタイプで、コンテンツをメモリから読み出したり、メモリに格納したりする際にバイトのスワップを適用するかどうかも指定します。さらに、ユーザは追加のアラインメントパラメータ(ここでは示していません)を渡すことができます。これは、メモリのデリファレンスに対し追加の制約を課すのに便利です。例えば、クライアントが誤ってアラインメントされた32ビット値へのアクセスを防ぎたい場合などです。

例えば、10個のint値をセグメントから読み出すために、以下のコードを記述できます。

MemorySegment segment = ...
int[] values = new int[10];
for (int i = 0 ; i < values.length ; i++) {
    values[i] = (int)intHandle.get(segment, (long)i * 4);
}

メモリアクセスVarHandleは他のVarHandleと同様、強い型付けがなされており、最大の効率を得るためには、一般にアクセス座標が期待される型に一致することを保証するためにキャストを使う必要があります。メモリアクセスVarHandleは(他の var ハンドルと同様に)強く型付けされており、最大の効率を得るためには、一般的にアクセス座標が期待される型と一致することを確認するためにキャストを導入する必要があります。今回の場合、私たちは i * 4 を long にキャストしなければなりません。同様に、シグネチャのポリモーフィックメソッドである VarHandle::get は名目上Objectを返すので、 v正しい戻り値の型をar ハンドル操作に強制するためにキャストが必要です。

デリファレンスのオフセットはバイト単位で表されるので、論理インデックス i にJava の int 値のバイトサイズである4を掛けて、開始オフセットを手で計算しなければならないことに注意してください。これは、ByteBuffer の絶対get/putメソッドで行われることと同様です。後ほど、メモリレイアウトがどのように高レベルの構造化アクセスにあたって役立つかを見てみましょう。

Safety

メモリアクセスAPIは、メモリのデリファレンス操作に対して基本的な安全性を保証しています。具体的には、メモリのデリファレンスは成功するか、実行時の例外が発生するかのどちらかになりますが、重要なのは、VM のクラッシュや、メモリ・セグメントに関連付けられたメモリの領域外で発生したメモリの破損を絶対に起こしてはならないということです。

これを実現するために、すべてのアクセスに対して、空間的および時間的な強力なチェックが行われます。次のコードを考えてみましょう。

MemorySegment segment = MemorySegment.allocateNative(10);
intHandle.get(segment, 8); //throws ISE (IllegalStateException)

上記コードは、セグメントの境界外からメモリアクセスしようとしているため実行時例外が発生します。アクセス操作は(境界内の)オフセット8から始めていますが、オフセット11(境界外)で終わっているためです。

同様に、すでにクローズ済みのセグメントへのアクセス試行は失敗します。

segment.close();
intHandle.get(segment, 0); //throws ISE

今回は、アクセスはセグメントが暗示する空間的な境界内で発生しますが、アクセス発生時にはセグメントはすでに閉じられているため、アクセス操作は失敗します。これは安全性を保証するために非常に重要です。メモリ・セグメントは決定論的な解放を保証するため、上記のコードは既に解放されたメモリをでリファレンスしようとしてしまう可能性があります。

基本的な空間的・時間的拘束の保証に加えて、メモリ・セグメントはスレッドの制限も保証しますが、これについては後述します。これらのチェックを単独で考えるとコストがかかると思われるかもしれませんが、Foreign Memory Access API は、JIT コンパイラがホットループの外でほとんどのチェックを行うことができるように設計・実装されていることに注意してください。したがって、メモリアクセス効率はAPIの安全性要件によって悪影響を受けることはありません。

Layouts

上記の例のように)バイトオフセットを表現すると、読みにくく、非常に壊れやすいコードになる可能性があります。それは、オフセットを拡張するために使われる定数にメモリレイアウトの不変性が暗黙的に取り込まれるからです。この問題に対処するために、クライアントがプログラムでメモリレイアウトを定義できるメモリレイアウトAPIを追加しました。例えば、上記の例で使用されている配列のレイアウトは、以下のコードで表現できます。

MemoryLayout intArray = MemoryLayout.ofSequence(10, MemoryLayout.ofValueBits(32));

つまり、このレイアウトはサイズがそれぞれ 32 ビットの 10 個の要素の繰り返しです。APIを使用してメモリレイアウトを前もって定義する利点は、レイアウト上でクエリを実行できることです。例えば、配列の3番目の要素のオフセットを計算できます。

long element3 = intArray.byteOffset(PathElement.sequenceElement(3)); // 12

オフセット計算にどの入れ子になったレイアウト要素を使用するかを指定するために、レイアウトパス(layout path)と呼ばれるものを使用します。これは、ルートレイアウトから選択したいリーフレイアウト(この場合はシーケンスの3番目のレイアウト要素)までレイアウトをナビゲートする選択式です。

レイアウトを使ってメモリアクセスVarHandleを取得することもできるため、上記の例は次のように書き直すことができます。

MemorySegment segment = ...
int[] values = new int[10];
VarHandle elemHandle = intArray.varHandle(int.class, PathElement.sequenceElement());
for (int i = 0 ; i < values.length ; i++) {
    values[i] = (int)elemHandle.get(segment, (long)i);
}

上記の例では、elemHandleが、2個のアクセス座標を取る、int型のvarハンドルです。

  1. MemorySegmentインスタンス:デリファレンスされるメモリを持つセグメント
  2. 論理インデックス:アクセスしたいシーケンスの要素を選択するために使用

換言すると、手動オフセット計算はもはや必要ありません。オフセットやストライドは実はレイアウトオブジェクトから導出できるからです。

メモリ・レイアウトは構造化アクセスが必要な場合に輝きます。以下のCの宣言を考えてみましょう。

typedef struct {
    char kind;
    int value;
} TaggedValues[5];

このCの宣言を以下のレイアウトを使ってモデル化できます。

SequenceLayout taggedValues = MemoryLayout.ofSequence(5,
    MemoryLayout.ofStruct(
        MemoryLayout.ofValueBits(8, ByteOrder.nativeOrder()).withName("kind"),
        MemoryLayout.ofPaddingBits(24),
        MemoryLayout.ofValueBits(32, ByteOrder.nativeOrder()).withName("value")
    )
).withName("TaggedValues");

ここでは、値フィールドのアラインメント要件を満たすために、種類フィールドの後にパディングを挿入する必要があるとします[1]。配列の全要素の値フィールドに手動でオフセット計算を使ってアクセスしなければならないとしたら、コードはすぐにかなり読みにくくなります。各イテレーションで、配列のストライドが 8 バイトであり、TaggedValue 構造体に対する値フィールドのオフセットが 4 バイトであることを覚えておく必要があります。これにより、(i * 8) + 4 のようなアクセス式が得られます(ここでiは、アクセスが必要な値フィールドを持つ要素のインデックスです)。

メモリレイアウトを使うと、以下のようにシーケンス内の値フィールドにアクセスするメモリアクセスVarHandleを簡単に1回で計算できます。

VarHandle valuesHandle = taggedValues.varHandle(int.class,
                                               PathElement.sequenceElement(),
                                               PathElement.groupElement("value"));

この var ハンドルを使うと、手動オフセット計算は必要ありません。結果として得られるvaluesHandleは、所望の値フィールドをシーケンスから選択するために利用できる追加のlong座標を特長としています。

Var handle combinators

注意深い読者なら、ここで見てきた単純なメモリアクセスVarHandleと比較して、レイアウト API が返す var ハンドルが非常にリッチであることにお気づきかもしれません。バイトオフセットを取る単純なアクセス用の var ハンドルから、複雑なレイアウトパスをデリファレンスできる var ハンドルへと、どのようにして移行するのでしょうか?その答えは、var ハンドルcombinatorsを使用することです。メソッドハンドル API に精通している開発者であれば、 MethodHandles API のさまざまなcombinatorのメソッドを使用して、単純なメソッドハンドルをより複雑なものに結合できるかを知っています。例えばこれらのメソッドを使えば、ターゲットメソッドハンドルに引数を挿入(またはバインド)したり、戻り値をフィルタリングしたり、引数の順序を変えたり、その他多くのことが可能です。

悲しいことに、これらの機能はいずれも var ハンドルを扱う際には利用できません。Foreign Memory Access API は、MemoryHandles クラスに豊富な var ハンドルコンビネータのセットを追加することでこの問題を解決します。これらのツールを使えば、開発者はvarハンドルの変換を以下のように表現できます。

  • embedding/projectionというメソッドハンドルの対を使い、varハンドルのキャリアタイプを異なるものにマッピングする
  • 単項フィルタを使用して1個以上のvarハンドルのアクセス座標をフィルタする
  • varハンドルのアクセス座標の順序を変更する
  • 具体的なアクセス座標を既存のvarハンドルにバインドする

あまり深く掘り下げずに、基本的なメモリアクセスVarHandleを受け取り、特定のオフセットでセグメントをデリファレンスする var ハンドルに変換する方法を考えてみましょう(前に定義した taggedValues レイアウトを再度利用しています)。

VarHandle intHandle = MemoryHandles.varHandle(int.class, ByteOrder.nativeOrder()); // (MS, J) -> I
long offsetOfValue = taggedValues.byteOffset(PathElement.sequenceElement(0),
                                             PathElement.groupElement("value"));
VarHandle valueHandle = MemoryHandles.insertCoordinates(intHandle, 0, offsetOfValue); // (MS) -> I

基本的なメモリアクセスVarHandleから、与えられた固定オフセットでセグメントをデリファレンスする新しい var ハンドルを導出できました。レイアウトAPIを使用して取得した他のリッチな var ハンドルを、 varハンドルcombinator API を使って手作業で簡単に作成できます。

Segment accessors

特に構造化されたアクセスの場合、レイアウトパスとコンビネータAPIを使用して複雑なメモリアクセスVarHandleを作成すると便利です。しかし単純なケースでは、与えられたセグメントオフセットで int 値を読み込めるようにするためだけに VarHandle を作成するのは、やりすぎと感じるかもしれません。このため、Foreign Memory Access API では、MemoryAccess クラスにstaticなアクセサを用意しており、これを使用すれば、さまざまな方法でセグメントをでリファレンスできます。例えば、クライアントがセグメントからint値を読み取りたい場合、以下のメソッドのいずれかを使用できます。

MemoryAccess::getInt(MemorySegment)セグメントのベースアドレスの最初からint値(4バイト)を読み出す
MemoryAccess::getIntAtOffset(MemorySegment, long)セグメントのベースアドレスをB、クライアントから指定されたオフセット(バイト)をOとする場合、B+Oのアドレスからintの値(4バイト)読み出す
MemoryAccess::getIntAtIndex(MemorySegment, long)セグメントのベースアドレスをB、クライアントから指定された論理インデックスをIとする場合、B + (4 * I)のアドレスからintの値(4バイト)読み出す(このアクセサは配列アクセスの模倣に便利)。

言い換えると、少なくとも簡単な場合には、VarHandle APIを使わなくてもメモリのデリファレンス操作を実現できます。もちろん、構造化アクセスや多次元アクセス、フェンスで囲まれたアクセス(fenced access)のようなより複雑な場合には、VarHandle APIのフルパワーが便利かもしれません。

Interoperability

メモリ・セグメントは、既存のメモリソースとの対話に関してはかなり柔軟性があります。例えば次のようなことが可能です。

  • Java配列からセグメントを作成
  • Java配列にセグメントを変換
  • バイトバッファからセグメントを作成
  • バイトバッファにセグメントを変換

例えば、バイトバッファAPIとの双方向の統合のおかげで、ユーザーはメモリ・セグメントの作成が可能で、バイトバッファAPIを使ってのデリファレンスも以下のようにできます。

MemorySegment segment = ...
int[] values = new int[10];
ByteBuffer bb = segment.asByteBuffer();
for (int i = 0 ; i < values.length ; i++) {
    values[i] = bb.getInt();
}

メモリ・セグメントからバイトバッファビューを作成する場合、そのバッファは、元のセグメントと同じ時間的制約とスレッド制限が保証されているということは唯一覚えておくべきことです。つまり、もしセグメントがクローズされると、それ以降、(以前に取得した)バイトバッファビューを使ってメモリのデリファレンスをしようとすると、例外をスローして失敗します。

Unsafe segments

ネイティブコードが管理しているかもしれない既存のメモリソースからセグメントを作成する必要がある場合が時としてあります。例えば、カスタム・アロケータで管理されているメモリからセグメントを作成したい場合などがこれにあたります。

ByteBuffer APIでは、JNIメソッドであるNewDirectByteBufferを使って、このような移動が可能です。このネイティブメソッドを使って、後に何もしらないJavaコードに返される新鮮なバイトバッファインスタンスで長いアドレスをラップできます。

NewDirectByteBuffer
https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/functions.html#NewDirectByteBuffer
https://docs.oracle.com/javase/jp/8/docs/technotes/guides/jni/spec/functions.html#NewDirectByteBuffer

メモリ・セグメントは同様の機能を提供します。つまり、(あるネイティブコールを通じて取得された可能性がある)アドレスが与えられると、与えられた空間的、時間的、および制限された境界で、その周りのセグメントをラップすることが可能になります。クリーンアップアクションを実行します。また、セグメントがクローズされたときに実行されるクリーンアップアクションを指定することもできます。

例えば、とある外部で管理されているメモリブロックを指すアドレスを想定します。この場合、安全でないセグメントを以下のように作成できます。

MemoryAddress addr = MemoryAddress.ofLong(someLongAddr);
var unsafeSegment = MemorySegment.ofNativeRestricted(addr, 10, Thread.currentThread(),
                                                     () -> System.err.println("Cleaned"), null);

このコードは安全でないセグメントを指定されたアドレスから作成しています。セグメントのサイズは10バイト、制限スレッド(confinment thread)は現在のスレッドで、セグメントがクローズされた場合にデバッグメッセージを表示するクリーンアップアクションがあります。

もちろん、このように作成されたセグメントはまったく安全ではありません。渡されたアドレスが本当に正しいメモリ位置を指していること、addrで指し示すメモリ領域のサイズが本当に10バイトであること、をランタイムは検証できません。同様に、addrに関連付けられている配下のメモリ領域がMemorySegment::closeの呼び出し前に解放されないことを保証できません。

これらの理由から、安全でないセグメントの作成は、Foreign Memory Access APIにおいて制限された操作です。制限付き操作は、実行中のアプリケーションが読み取り専用のランタイムプロパティ(foreign.restricted=permit)を設定している場合にのみ実行できます。このランタイムプロパティを設定しないで制限付き操作を呼び出そうとすると、ランタイム例外が発生して失敗します。

将来的には、制限付き操作へのアクセスをモジュールシステムとより統合したものにすることを計画しています。そのようなモジュールに依存するアプリケーションを実行した場合、ユーザーは制限付きネイティブ操作を実行するためにモジュールに権限を与えなければならないかもしれませんし、ランタイムがアプリケーションのモジュールグラフの構築を拒否します。

Confinement

セグメントには、空間的および時間的な境界に加えて、スレッド制約があります。つまり、セグメントはそのセグメントを作成したスレッドが所有し、他のスレッドはセグメント上のコンテンツにアクセスしたり、特定の操作(close など)を実行したりすることはできません。スレッド制約は制限的ではありますが、マルチスレッド環境でも最適なメモリアクセス性能を保証するためには非常に重要です。制約がなくなると、複数のスレッドが同じセグメントに同時にアクセスしたりクローズしたりすることが可能になりますが、これは、アクセスとクローズの競合を防ぐために非常に高価な形式のロックが導入されていない限り、Foreign Memory Access APIが提供する安全性の保証が無効になる可能性があります。

Foreign Memory Access APIは、スレッド制約の障壁を緩和するための2つの方法を提供しています。まず、スレッドが明示的なハンドオフ操作を実行することで、セグメントを協調的に共有できます。次のコードを考えてみましょう。

MemorySegment segmentA = MemorySegment.allocateNative(10); // confined by thread A
...
var segmentB = segmentA.withOwnerThread(threadB); // confined by thread B

このアクセスパターンはserial confinementとしても知られており、一度に1つのスレッドだけがセグメントにアクセスする必要がある場合、プロデューサー/コンシューマーのユースケースで有用な場合があります。ここで留意すべきは、安全なハンドオフ操作のために、APIは元のセグメントを破棄し(closeが呼び出された場合と同様だが、配下のメモリはリリースしていない状態)、正しいオーナーを持つ新しいセグメントを返す点です。次のスレッドがセグメントにアクセスするまでに、最初のスレッドによる全ての書き込みがメモリにフラッシュされることも実装が確認します。

もう一つは、メモリ・セグメントからSpliteratorインスタンスを取得することで、メモリ・セグメントの内容を(Fork/Joinのようなフレームワークを使って)並列処理できます。例えば、メモリ・セグメントの全32ビット値を並列で合計するために、以下のコードを利用できます。

SequenceLayout seq = MemoryLayout.ofSequence(1_000_000, MemoryLayouts.JAVA_INT);
SequenceLayout seq_bulk = seq.reshape(-1, 100);
VarHandle intHandle = seq.varHandle(int.class, sequenceElement());    
​
int sum = StreamSupport.stream(MemorySegment.spliterator(segment, seq_bulk), true)
                .mapToInt(slice -> {
                    int res = 0;
                    for (int i = 0; i < 100 ; i++) {
                        res += MemoryAccess.getIntAtIndex(slice, i);
                    }
                    return res;
                }).sum();

MemorySegment::spliteratorは、セグメントとシーケンス・レイアウトを受け取り、セグメントを、指定されたシーケンス・レイアウトの要素に対応するチャンクに分割するspliteratorインスタンスを返します。ここでは、100万個の要素を含む配列の要素を合計したいとします。各計算で正確に1つの要素を処理するような並列演算による合計は効率が悪いので、代わりにレイアウトAPIを使用してバルク・シーケンス・レイアウトを導出します。バルク・レイアウトとは、元のレイアウトと同じサイズのシーケンス・レイアウトですが、並列処理をしやすくするために100個の要素のグループに配置します。

spliteratorがあれば、これを使ってparallel streamを作成し、セグメントの内容を並列に合計できます。ここでは、セグメントは複数のスレッドから同時にアクセスされますが、アクセスは通常の方法で行われます。つまり、 元のセグメントからスライスが作成され、スレッドに与えられて計算が行われます。foreign memory accessランタイムは、スレッドが現在セグメントのスライスにspliteratorを使ってアクセスしているかどうかがわかりますし、そのため、同じセグメントの並列処理が行われている間はセグメントをクローズしないようにすることで安全性を確保できます。

もちろん、serial confinementやspliteratorベースの共有はすべてのコンテキストで動作するわけではありません。クライアントがある種の危険なセグメントを作成し、それを複数のスレッドで共有したい場合があるかもしれません(スレッドは同期をとって内容に安全にアクセスできるようにする必要があるでしょう)。Foreign Memory Access APIはこのユースケースを直接サポートしていません。安全でないメモリ・セグメント(上記参照)を作成して監禁制限を解除することはできますが、安全かつサポートされている非監禁セグメントを作成する方法はありません。現在このユースケースを実現する解決策を探索中です。現在探索中の主な方策は以下の通りです。

  • メモリ・セグメントにアトミックな参照カウントのサポートを追加する
  • メモリ・セグメントの時間的制約をExecutorServiceのような同時実行の抽象化の時間的制約に結びつける(これはProject Loomの構造化された並行性(structured concurrency)サポートによってより簡単になる可能性がある)

Structured Concurrency
https://cr.openjdk.java.net/~rpressler/loom/loom/sol1_part2.html#structured-concurrency

  • rely on GC safepointing mechanisms or VM thread-local handshakes (JEP 312) to implement lock-free, safe, shared memory accessGCのセーフポイントメカニズムやVMのスレッドローカルハンドシェイクを使って、ロックフリーで安全な共有メモリアクセスを実装する

VarHandle.safepoint() methods
https://mail.openjdk.java.net/pipermail/jmm-dev/2017-January.txt
JEP 312: Thread-Local Handshakes
https://openjdk.java.net/jeps/312

これらの3つの方法のうち、3番目の方法はメモリ・セグメントから所有権の概念がなくなるために、クライアントの観点からは最も柔軟性が高いと思われますが、実現するには(UnsafeメモリのアクセスAPIへの深い変更が必要ゆえに)最も複雑でもあります。参照カウントはJava 14で検討されました(MemorySegment::acquireメソッドを参照)が、所望のユースケースをサポートするほど強力ではなく、APIが非常に複雑になってしまいました。最後に、問題を完全に解決するわけではありませんが、より粒度の荒い抽象化でメモリ・セグメントに制約を与えることで、有用なフォールバックソリューションがもたらされるかもしれません。


[1] 一般に、Cの構造体宣言から完全なレイアウトを導出するのは簡単ではありません。ツールが大いに役立つ分野の一つです。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中