Hatena::Groupiphone-dev

iPhoneアプリ開発まっしぐら★ このページをアンテナに追加 RSSフィード

引っ越し後の日記はコチラです

tokoromのその他の日記

2010-03-24

autoreleaseを使うと本当に遅くなる?という疑問を実測しながら検証中です

| 13:20 | はてなブックマーク -  autoreleaseを使うと本当に遅くなる?という疑問を実測しながら検証中です - iPhoneアプリ開発まっしぐら★

autoreleaseを使うとパフォーマンスに悪影響を及ぼすという話があり、個人的にはそんなに遅くならないのでは?と感じていましたので実測してみました。

今回は、なんの変哲もない小さなクラスについて、release/autoreleaseをそれぞれ10万回繰り返したときにかかる時間を測りました。


releaseの呼び出し部分とautoreleaseの呼び出し部分の比較

以下、計測した具体的なコードです。

◆releaseの計測コード

  NSDate* start = [NSDate date];

  for ( int i = 0; i < 100; ++i ){
    for ( int l = 0; l < 1000; ++l ){
      Man* hoge = [[Man alloc] init];
      hoge.age = l * i;
      [hoge release];
    }
  }

  NSLog( @"%f", [start timeIntervalSinceNow] * -1 );

◆autoreleaseの計測コード

  NSDate* start = [NSDate date];

  for ( int i = 0; i < 100; ++i ){
    for ( int l = 0; l < 1000; ++l ){
      Man* hoge = [[[Man alloc] init] autorelease];
      hoge.age = l * i;
    }
  }

  NSLog( @"%f", [start timeIntervalSinceNow] * -1 );

◆Manクラスの中身

// Inteface
@interface Man : NSObject
{
 @private
  NSUInteger age_;
}
@property (nonatomic, assign) NSUInteger age;
@end

// Implementation
@implementation Man
@synthesize age = age_;
@end

計測結果1

※計測はすべて、iPhone 3GS OS 3.1.3 で実施しています。

以下、計測結果です(秒単位)。

releaseautorelease
1回目0.5118420.415604
2回目0.5289110.422012
3回目0.5147260.422038

autoreleaseのほうが微妙に速いという結果です。

まずこの結果から、autoreleaseを呼び出す部分については、特にパフォーマンスが悪いということはなさそうだということがわかりました。


autoreleaseのほうが速い理由

これは、当然と言えば当然の結果で、

  • releaseは参照カウンタが0になったらその場で解放処理
  • autoreleaseは後からrelease

といった具合に、releaseのほうはその場で解放処理が走っているのに対して、autoreleaseはその処理を後に遅らせているからというところでしょう。


◆releaseの場合の解放タイミング

  for ( int i = 0; i < 100; ++i ){
    for ( int l = 0; l < 1000; ++l ){
      Man* hoge = [[Man alloc] init];
      hoge.age = l * i;
      [hoge release]; //< ココで解放処理 !!!
    }
  }

◆autoreleaseの場合の解放タイミング

  for ( int i = 0; i < 100; ++i ){
    for ( int l = 0; l < 1000; ++l ){
      Man* hoge = [[[Man alloc] init] autorelease];
      hoge.age = l * i;
    }
  }

  /* ----------------------------
        ↓  ↓  ↓  
    この後、イベントループが終わるときにまとめて解放処理 */

次に、AutoreleasePoolを使い、autorelease呼び出しにより、あとからまとめて実行される解放処理にかかる時間まで含めた計測をしてみました。↓


autoreleaseのあとからまとめて解放する部分も加味した計測

※3/24 19:30 本項追記

※3/25 1:30 計測処理自体で差が出にくいようautoreleaseのほうの計測方法を訂正

ktakayamaさんがタイムリーにAutoreleasePoolを含めた実測をしてくれました。ありがとうございますっ!

せっかくなのでその記事からそのままバトンを継続させていただきます。

AutoreleasePoolを使ってそのAutoreleasePoolの作成と解放の時間を含めて計測すると、autoreleaseのほうが速度が遅くなるというのは、ktakayamaさんに計測していただいたとおりです。

あとは、純粋にrelease自体にかかる時間と、autoreleaseであとからまとめてreleaseするときにかかる時間も比較しておきたいところです。

ということで、次のコードで実測してみました。

◆都度releaseの総計時間の計測

  NSDate* start = [NSDate date];
  NSTimeInterval mark;
  NSTimeInterval elapsed = 0.0;

  for ( int i = 0; i < 100; ++i ){
    for ( int l = 0; l < 1000; ++l ){
      Man* hoge = [[Man alloc] init];
      hoge.age = l * i;
      mark = [start timeIntervalSinceNow];
      [hoge release]; //< ここにかかる時間を足していく
      elapsed -= [start timeIntervalSinceNow] - mark;
    }
  }

  NSLog( @"%f", elapsed );

◆autoreleaseであとからまとめてreleaseしたときの時間計測

  NSDate* start = [NSDate date];
  NSTimeInterval mark;
  NSTimeInterval elapsed = 0.0;

  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
  for ( int i = 0; i < 100; ++i ){
    for ( int l = 0; l < 1000; ++l ){
      Man* hoge = [[Man alloc] init];
      hoge.age = l * i;
      mark = [start timeIntervalSinceNow];
      [hoge autorelease]; //< ここにかかる時間を足していく
      elapsed -= [start timeIntervalSinceNow] - mark;
    }
  }
  mark = [start timeIntervalSinceNow];
  [pool release]; //< あとからまとめてreleaseする時間も足す
  elapsed -= [start timeIntervalSinceNow] - mark;

  NSLog( @"autorelease %f", elapsed );

計測結果2

以下、計測結果です。

releaseautorelease
1回目0.7424580.930459
2回目0.7634640.956233
3回目0.7749550.987029

releaseについてもautoreleaseについても、それぞれ10万回実行するのにかかった時間を全て足しこみ、さらにautoreleaseのほうは後回しにしておいた解放処理を実行するのにかかる時間も加算しました。

結果として、autoreleaseのほうが少し遅いということが分かりました。

これはこちらでkhirohisさんがコメントしてくれているようなオブジェクトの管理コスト(たぶんハッシュで管理しているのでは)によるものと思われます。


ただ、10万回実行して200ミリ秒以下の差異ですから、この計測だけだと、まだautoreleaseのパフォーマンスが際立って悪いとは感じません。


つづき

paellaさんから、以下の有用なコメントをいただきました。

autoreleaseが遅い、という話の原因には、

autoreleaseだけど retainされているオブジェクトが大量にプールされているときに、イベントごとに数え上げの対象になってしまい、数え上げの処理が遅くなってしまうことでは?

と思っています。

私はautoreleaseが遅くなる原因がなかなか思い当たらなかったのですが、これは是非調査しておきたいところです!

ということで、つづきのエントリではこのあたりを攻めていく予定です。



※3/25 4:00 追記

追加の計測(条件をもうちょっと複雑にした版)をしてみましたー



使用メモリの変化についてはコチラ


少し条件を複雑にしてautoreleaseのパフォーマンス測定

| 04:05 | はてなブックマーク -  少し条件を複雑にしてautoreleaseのパフォーマンス測定 - iPhoneアプリ開発まっしぐら★

前のエントリの続きです。

前のエントリのautoreleaseの計測だと、

  • autoreleaseで遅延させてreleaseが実行された時点で必ず全てのオブジェクトの参照カウンタが0になる

というシンプルな条件だったので、paellaさんからコメントいただいた件も気にしつつ、もうちょっと条件を複雑にしてみました。

以下、条件をまとめます。

  • AutoreleasePoolに貯めるオブジェクトは10万個(前回と同じ)
  • autorelease呼び出し時の参照カウンタが 2 or 3 のどちらかになるようにする(前回は全て1)
  • AutoreleasePoolの解放時に全てのオブジェクトが解放されないよう、10万/3個のオブジェクトを逃がしておく

2番目と3番目の条件については、NSMutableArrayに3回に1回オブジェクトをaddすることで対応します。


計測用のコード

実際に計測に使用したコードは↓です。

  NSDate* start = [NSDate date];
  NSTimeInterval mark;
  NSTimeInterval elapsed = 0.0;

  NSMutableArray* hoges = [[NSMutableArray alloc] initWithCapacity:100*1000/3];
  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
  for ( int i = 0; i < 100; ++i ){
    for ( int l = 0; l < 1000; ++l ){
      Man* hoge = [[Man alloc] init];
      hoge.age = l * i;
      [hoge retain]; //< 参照カウンタを1つインクリメントしておく
      if ( 0 == l % 3 ) {
        [hoges addObject:hoge]; //< 3つに1つの割合で外部に逃がしておく(AutoreleasePoolにより解放されないように)
      }
      mark = [start timeIntervalSinceNow];
      [hoge autorelease];
      elapsed -= [start timeIntervalSinceNow] - mark;
      [hoge release];
    }
  }
  mark = [start timeIntervalSinceNow];
  [pool release];
  [hoges release]; //< 逃がしておいたインスタンスもここでまとめて解放
  elapsed -= [start timeIntervalSinceNow] - mark;

  NSLog( @"autorelease %f", elapsed );

ここで計測の対象としている部分は以下3点です。

  • autoreleaeの呼び出し部分
  • AutoreleasePoolの解放部分
  • 逃がしておいたオブジェクトを格納しておいた配列の解放部分

計測結果のまとめ

前回までと同様に単純にrelease/autoreleaseしたときの計測結果と合わせて、同タイミングで実施した今回の計測結果を以下にまとめます。

※いずれも iPhone 3GS OS 3.1.3 で計測

※4/4 コードを一部修正して再計測(結果が大きく変わる部分はありません)

releaseautorelease今回
1回目0.7374330.9611571.064178
2回目0.7524110.9552841.089464
3回目0.7482040.9559531.105438

結果を見ての印象としては、autoreleaseにしてもクリティカルなパフォーマンス遅延が発生することはないのではといったところです。

ただ、「autoreleaseが遅いよ」というのはよく聞く話ではありますので、なにかしらの条件を加えると一気にパフォーマンスが遅くなるのかもしれません。

これは想像ですが、autorelease自体が重いというよりは、autoreleaseにより解放タイミングを遅延した結果、メモリ不足が発生して重くなるケースが発生したのではないでしょうか。

そのあたりご存知のかた、アイデア/知識をお持ちのかたは是非コメントください!


念のため補足

今回は、計測のためにループ内のローカル変数にautoreleaseを適用するというマナー違反的なことをしています。

実際にはこのタイミングでautoreleaseを使うと、メモリを余分に食うというデメリットのほうが大きいです。ループ内のローカル変数にはreleaseのほうを使うことをオススメします。


逆に、autoreleaseの適用をオススメするのは以下のようなケースです。

◎ オブジェクトをクラス変数のコンテナにaddしておく

[self.objects addObject:[[[Man alloc] initWithXXX:@"XXX"] autorelease];

◎ Viewにコントロールをaddしておく

[self.view addSubview:[imageView autorelease]];

こういったケースでは、コンテナやViewにそのオブジェクトを保持(retain)させた時点で、releaseは参照カウンタを1つ減らすという意味しか持たなくなります。

このようにreleaseをしても実解放が走らないようなケースでは、autoreleaseを使うほうが保守性の面でメリットがあると思います。

autoreleaseのメリットについてはGoogleのガイドラインの[Prefer To autorelease At Time of Creation]なんかを参考にさせてもらっています。



使用メモリの変化についてはコチラ

paellapaella 2010/03/24 16:51 autoreleaseが遅い、という話の原因には、
autoreleaseだけどretainされているオブジェクトが大量にプールされているときに、イベントごとに数え上げの対象になってしまい、数え上げの処理が遅くなってしまうことでは?
と思っています。

しかし未検証ですので、私も調べてみます。

tokoromtokorom 2010/03/24 17:59 paellaさん、有用なコメントありがとうございます!
私もそのあたりを含めて実測して、次のエントリにつづきを書かせていただきます^^

tokoromtokorom 2010/03/25 04:13 AutoreleasePoolに参照カウンタが残っているオブジェクトをプールさせた状態での計測も追加してみましたー

kimadakimada 2010/03/25 08:08 初めまして。参考になる記事をありがとうございます。
NSAutoreleasePoolのリファレンスマニュアルを見ると、あるオブジェクトのautoreleaseが呼ばれると、NSAutoreleasePoolのaddObject:メソッドが呼ばれるようです。NSAutoreleasePoolは、addObjectされたものに対して、releaseを発行するようです。なので、オーバーヘッドが存在することは事実だと思いますが、それを気にする必要があるかどうかは、ケースバイケースかもしれませんね。
実際に、以下のようなコードでも試してみましたが、addObjectが呼ばれていることが確認できました。

@interface MyAutoreleasePool : NSAutoreleasePool

@end

@implementation MyAutoreleasePool

- (void)addObject:(id)object {
NSLog(@"addObject: %@", object);
[super addObject:object];
}

@end

NSAutoreleasePool *pool = [[MyAutoreleasePool alloc] init];
[NSDate date];
[NSDate date];
[pool release];

tokoromtokorom 2010/03/25 12:19 kimadaさん、NSAutoreleasePoolの検証ありがとうございました!
いただいたコードのおかげでAutoreleasePoolの挙動がよくわかりました。
たしかにaddObject:のオーバーヘッドは気にする必要はなさそうですよね。
Twitterで、@cqa02303 さんから [NSAutoreleasePool showPools]; で実際にPoolされているオブジェクトを確認できるよーということも教えていただきました!

kimadakimada 2010/03/26 01:10 少しでもお役に立てたのならうれしいです ;−)

10万回autoreleaseすると、
- それらのオブジェクトの生存期間が延びる
- 10万個のポインタが新たに作られることになるので、その分メモリを喰う(1MB未満)
- 10万回まとめてrelease(+ dealloc)が呼ばれる(参照が深いと重たくなるかも。。)
といったあたりがオーバヘッドということになるんでしょうか。。

[NSAutoreleasePool showPools]は、デバッグ時に役立ちそうですね!

HimadeusHimadeus 2011/06/09 08:46 AutoreleasePoolの、addObjectは、mutexか何かで排他処理をしているはずです。
ですので、マルチスレッドで、各スレッドが、autorelease を呼び合っていると、遅くなる
でしょう。

AuroraAurora 2012/08/09 00:13 At last! Soeomne with real expertise gives us the answer. Thanks!

iqipxytphiqipxytph 2012/08/09 20:42 IdzrBo , [url=http://ycapyslzgbzl.com/]ycapyslzgbzl[/url], [link=http://tedghmhlsvqe.com/]tedghmhlsvqe[/link], http://syvkumegwibp.com/

xonlvahtcrexonlvahtcre 2012/08/10 04:00 wOvsya <a href="http://qetlmbtidsil.com/">qetlmbtidsil</a>

ゲスト