lambda/Blocks/GCD」カテゴリーアーカイブ

GCDのブロックの実行時間の許容範囲は5ミリ秒くらい?

 Grand Central Dispatch+Blocks、便利である。おいらの作ってるアプリも非同期で動作する部分のほとんどがGCDに書き改められた。

 さて、いい気になってあれもこれもGCDで処理させていたら、おかしな挙動に出くわすようになった。くそやったらと遅くなるのだ。何故? Why? と思って調べてみると、dispatch_queueのスレッドが大量に作られていた。
スクリーンショット 2013-11-20 2.53.30
 え? GCDって最適化された数のスレッドにブロックをいいカンジに割り振ってくれるんじゃなかったの? なんでこんな大量にスレッド作られちゃってるの??

テキトーに確認してみた。

 こんなカンジのコードをiPad2とiPad mini Retinaの2台で走らせて最大並列実行数をチェックしてみる。ブロックの実行にかかる時間=スリープ時間。

 実行時間が0.1ミリ秒の場合どちらの端末でも最大並列実行数は2だった。CPUコア数と同じでいいカンジである。
 実行時間を徐々に長くしてみると1ミリ秒でも最大数並列実行数は2のまま。5ミリ秒でも同じく。

 ところが実行時間がiPad2では6ミリ秒、iPad mini Retinaでは7ミリ秒になったところででスレッド数がガンガン膨れ上がった。ファック!!!!

 どうやら用意されたスレッドの全てが時間のかかるブロックに塞がれてしまってる場合、いつになったらそのブロックの実行が終わるのか判らんので残りのブロックが待たされっぱなしになるのを防ぐために実行スレッドをひとつ増やすっぽい。その閾値が5ミリ秒強のようだ。性能差が何倍もあるiPad2とiPad mini Retinaでおおよそ同じ値なのでこの辺CPU時間でなく実時間ベースで閾値があるのであろう。

 ここで、10ミリ秒とかかかるブロックを大量にqueueに投入した場合、あっという間にデフォルトで用意されたスレッドが埋まり、しびれを切らして実行スレッドが増やされるんだけどそのスレッドも時間のかかるブロックで埋まり。。。。 が繰り返されてスレッドがガンガン増殖していく模様。

 なお上に書いたのは簡単な計測までは実際にやったけどその後の考察は単なる憶測、おいらの妄想です。GCDはソース公開されてるんだからそれくらいコード読んで調べろよってハナシなんですが納期に追われるだけのウンコプログラマなのでそこまでする時間的余裕がありません。あ、ウソ書きましたすみません。そこまでするスキルがありません。

 この問題、そんな粗い粒度のもんGCDぶっこむなよってハナシなのかもしれませんが、粒度細かすぎればGCDのオーバーヘッドで相殺されちゃうし、ある程度の処理時間があるけど5ミリ秒以内っていうのは何も考えずにGCD使える範囲って実は結構狭いんじゃないか的な。
 また入力データ長によって処理時間が変わるブロックの場合、小さなデータであればそれがどれだけ大量にあってもいいカンジに処理してくれるのに、大きなデータがまとまった数やってきて実行時間が5ミリ秒を超えた途端に無意味に大量にスレッドが作られ始めて全体の動作が緩慢になってしまう。これ、知らずに使ってると結構落とし穴。っていうかおいらがごぼっとハマった。

 そんなこんながあって、同時に処理されるブロックの数が指定数以下になるようdispatch_queueを外側から包むクラスを作成した。外側にもひとつ自前キューを作ってブロックをそこに溜めて、コーヒーサーバにお湯を注ぐかのようにdispatch_queueのブロック実行が完了してから次のブロックをdispatch_queueに注いで同時実行数を指定以下に押さえる真似してます。
 大きなブロックによって処理スレッド数が無意味に増殖していくのを防ぐ他に、最大同時ダウンロード数の指定などなどいろいろ便利に使える予定。

本来なら最大同時実行数の指定はGCD側で対応してもらいたいものである。

おわり

Lambda/Blocks の引数内のObjective-C++クラス派生関係

オブジェクトを引数に取るLamdaやBlockの変数に対して、そのオブジェクトと派生関係にあるクラスのオブジェクトを渡すとどうなるか試してみた。
環境はXCode4.5。

まず、LambdaでC++ classの場合。

一つ目は A0 に A0 を渡してるから当然OK。
二つ目はまったく関係のないクラスのオブジェクトを渡してるから当然エラー。
三つ目はA0を扱う関数変数にその派生クラスを扱う関数を渡している。これはアップキャストにあたるので当然エラー。
四つ目はA1を扱う関数変数にその基底クラスを扱う関数を渡している。これはダウンキャストにあたるので当然OK。
という、至極当たり前な結果になった。

同じ事を Blocks と Objective-C classでやっても同じ結果になる。

さて、ここからが本題。
じゃあ、LambdaでObjective-Cクラスを扱ったらどうなるの? BlocksでC++クラスを扱ったらどうなるの?
(クラス関係は上に出てきたまま)

LambdaでObjective-Cクラスを扱った場合、何故かアップキャストになる代入に対してもエラーが発生しない。ただしクラス関係が全く関連がない場合はきちんとエラーになるのでこの挙動はようわからん。。。
そしてBlocksでC++クラスを扱った場合、ダウンキャストになる代入に対してもエラーになる。。。 これは微妙に困る。
いやLambda使えよ、って言われてもGCDやる時はそうはいかないじゃないですか。

この辺の挙動将来はちゃんとしてくれるだろうか。

C++オブジェクトを dispatch_async越しに渡すのにstd::shared_ptrを使うでござる

C++クラスのオブジェクトを dispatch_async 越しに渡したい。
たとえばこんな風に。

しかしこのようにC++オブジェクトそのものを渡すと、それは値渡しになる。
つまりコピーが渡されれる。
つまり元の cppObj とブロック内の cppObj は別インスタンスになる。
しかもblock内はconst扱い。
しかも何故かコピーコンストラクタが2度呼ばれる。。。たぶん、ブロックにキャプチャされた時に1回、dispatch_asyncでブロックがコピーされた時に1回、計2回ですね。。
CPPClassが (x,y)の2次元ベクトルのような小さなクラスであれば問題ありませんが、同一性を保持したいとか、オブジェクトがデカくてコピーするとパフォーマンスに影響が出るとか、宗教上の理由とかでコピーコンストラクタが勝手に呼ばれるのが好ましくない場合、躊躇することになります。

ハハハ、こんな時のために __block があるじゃないですか。
これで解決ですよ。

確かめてみると、元関数のcppObjとブロック内のcppObj、ちゃんと同じインスタンスになってます。

これで万事解決。。。。   と思いきや。。。

なんとここでしれっとコピーコンストラクタが呼ばれるではありませんか。
試しに CPPClass のコピーコンストラクタをprivateにするとこの文でエラーがでます。
なんと、一旦コピーと取った後に、元関数部分とブロック内部の両方でそのコピーを指すようになるっぽいですね。。。

えー。。。。 コピーが起きない方法はないものじゃろうか。。。

じゃあローカル変数じゃなくてnewでヒープに割り付ければいいじゃん。

ってなるのですが、newしたC++クラスはどこで delete するかが問題になります。

キャプチャ時に自動的にretain/releaseされるObjective-Cインスタンスとは違って、C++インスタンスの生ポインタにはそんな便利なことなんてしてくれません。
元の部分とブロックの内部のどちらか最後に不要になった方でプログラマの責任で確実に一度だけdeleteする必要があります

じゃあどうするか。

C++インスタンスもObjective-Cのように参照カウンタ形式で管理すれば良いのです。

幸い、C++11で参照カウンタで管理できる std::shared_ptr<>クラスが使えるようになりました。
早速使ってみよう。

オブジェクトの開放はshared_ptr任せでよく、deleteを書く必要はない。楽チン。
えっ ってカンジだけどこれで全部解決。

cppObjSPtr を構築した時点で参照カウンタは1。
ブロックにキャプチャされる時に cppObjSPtr が複製され、この時中のオブジェクトに対する参照カウンタが+1され、2になる。
後は、元の関数終了部分とブロック終了部分で、それぞれのcppObjSPtrが破棄され、中のオブジェクトに対する参照カウンタが−1される。
どっちが後になっても、後になった方で参照カウンタが0になり、その時に中のオブジェクトもdelete。
(実際にはキャプチャされた時とdispatch_asyncでブロックがコピーされた時で2回コピーが起きるのでもうちょっと動きがあるけど、にゅあんすでー)

dispatch_async越しに渡せるし、とてもARCっぽい。素晴らしい!!

cppObjSPtrはブロックにキャプチャされる際にコピーされるけど、指すオブジェクトは同じです。コピーが発生しない。万歳!

C++11によって、こういった微妙〜〜〜に困る問題が結構解決する。
C++11ばんじゃーい∩( ・ω・)∩