Hatena::Groupiphone-dev

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

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

tokoromのその他の日記

2010-04-04

autoreleaseで解放を遅延した場合の使用メモリ変化のグラフ

| 18:45 | はてなブックマーク -  autoreleaseで解放を遅延した場合の使用メモリ変化のグラフ - iPhoneアプリ開発まっしぐら★

で、release/autorelease周りのパフォーマンス計測を行いましたが、あとはautoreleaseで解放を遅延した場合の使用メモリを気にする必要がありそうです。


といっても特にひねる部分もなく、autoreleaseで解放を遅延したぶん使用メモリが増えていくというだけです。

前々回の検証でせっかくObjectAllocのスクリーンショットを撮ったのでメモ程度に貼りつけておこうと思います。


各矢印は、それぞれ前々回の検証で

  • ループ内で確保したオブジェクトに全てreleaseを適用した場合の使用メモリがMAXになった部分
  • ループ内で確保したオブジェクトに全てautoreleaseを適用した場合の使用メモリがMAXになった部分
  • ループ内で確保したオブジェクトにautoreleaseを適用するが、NSAutoreleasePoolを使って定期的に解放した場合で使用メモリがMAXになった部分

を示します。

f:id:tokorom:20100404183411p:image

このグラフは、確保するメモリ容量やNSAutoreleasePoolの解放の頻度によって全く変わってきますので、あくまでもイメージとして。

見てのとおり、autoreleaseで多くのオブジェクトの解放を遅延させればさせるほど使用メモリが膨大になっていきますので、

  • 何回も繰り返されるループの中で、ローカル変数に対してautoreleaseを使うことは避ける
  • autoreleaseを使ったコードを何度もループせざるを得ない場合には、NSAutoreleasePoolで適切な頻度で解放する

といったことに気をつけないと。。。というのを改めて感じた次第です。


※最後に、くどいかもしれませんが、

  • クラス変数のコンテナにaddしたあとのオブジェクト
  • Viewにaddしたあとのコントロール

など、releaseでもautoreleaseでもどっちみちそのオブジェクトが保持されたままの(参照カウンタが0にならない)ケースでは、autoreleaseに対するデメリットは見当たらないので積極的な利用をオススメします。

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]なんかを参考にさせてもらっています。



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

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

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

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

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

kimadakimada2010/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];

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

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

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

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

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

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

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

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

2010-03-15

前記事の補足:プロパティへのnil代入がreleaseの代わりになる理由

| 03:18 | はてなブックマーク -  前記事の補足:プロパティへのnil代入がreleaseの代わりになる理由 - iPhoneアプリ開発まっしぐら★

こちらの記事について「なんでプロパティへのnil代入がreleaseの代わりになるの?」という質問をいただきましたので、ここで補足させていただきます。


アクセサの自動生成

プロパティ(property)には基本的に、

  • 値を参照するためのゲッター(getter)
  • 値を代入するためのセッター(setter)

の2つのアクセサが必要です。

Objective-Cでは、このアクセサを自動生成させることが可能です。

その「アクセサを自動生成してね」という指定が、

@synthesize brain;

というやつです。

↑の場合は、brainプロパティのアクセサを自動生成してね、という指定です。


ちなみに、アクセサを自動生成するにはプロパティ名と同じ名前のクラス変数が必要です。

自動生成されるアクセサの中身では、そのクラス変数に対して値の出し入れがされるのです。

もし、そのクラス変数名を違うものにしたいなら、

@synthesize brain = brain_;

というように指定します。この場合はbrain_というクラス変数がアクセサの中で使われることとなります。

いちおう、この場合のクラスとプロパティの宣言のサンプルも↓に。

@interface Human : NSObject
{
 @private
  id brain_;
}

@property (nonatomic, assign) id brain;

@end

自動生成されるアクセサの具体的なコード例(assignの場合)

assign属性のプロパティの場合、単純にクラス変数に値を代入したり参照したりするという扱いなので、自動生成されるアクセサは以下のようになります。

// getter
- (id)brain { 
  return brain_; 
} 
 
// setter
- (void)setBrain:(id)newBrain { 
 brain_ = newBrain; 
} 

ということは、このプロパティにnil代入するのは実際には以下に等しくなります。

  // self.brain = nil; は↓と同等
  brain_ = nil;

自動生成されるアクセサの具体的なコード例(retainの場合)

retain属性のプロパティの場合、値を代入するときには、代入する値に対してretainメッセージを送ると同時に、元から入っている値の参照カウンタを減らすために元の値に対してreleaseメッセージを送ることも必要です。具体的には以下のようなアクセサが自動生成されます。

// getter
- (id)brain { 
  return brain_; 
} 
 
// setter
- (void)setBrain:(id)newBrain { 
  if ( brain_ != newBrain ) { 
    [brain_ release]; 
    brain_ = [newBrain retain]; 
  }  
} 

ということは、このプロパティにnil代入するのは実際には以下に等しくなります。

  // self.brain = nil; は↓と同等
  if ( brain_ != nil ) { 
    [brain_ release]; 
    brain_ = nil;
  }  

これがプロパティへのnil代入がreleaseの代わりになる理由です。

iramusairamusa2010/04/11 21:28初見で恐縮ですが、
[newBrain retain]のnewBrainがnilに置き換わることで、retainメソッドを
呼び出そうとしてエラーになったりはしないんでしょうか。

tokoromtokorom2010/04/12 01:25nilオブジェクトに対するメッセージ送信はランタイムにより無視されるので大丈夫ですよー。
ここについては http://www.textdrop.net/google-styleguide-ja/objcguide.xml#nil_チェック などが参考になります。

DaveDave2013/01/20 06:03God, I feel like I sohlud be takin notes! Great work

znaymaiamznaymaiam2013/01/20 22:37HH52gE <a href="http://ayvoxvoezwth.com/">ayvoxvoezwth</a>

2010-03-14

プロパティ解放の記述方法についての提案

| 03:25 | はてなブックマーク -  プロパティ解放の記述方法についての提案 - iPhoneアプリ開発まっしぐら★

例えば、↓のクラスの dealloc で、各プロパティを解放する場合、

@interface Human : NSObject
{
 @private
  NSString* name_; 
  id brain_;
  NSArray* hands_;
  NSArray* foots_;
}

@property (nonatomic, copy) NSString* name;
@property (nonatomic, assign) id brain;
@property (nonatomic, retain) NSArray* hands;
@property (nonatomic, retain) NSArray* foots;

@end

各所でよくみるコードとしては、↓になっていないでしょうか。

◆改善前

- (void)dealloc {
  [name_ release];
  [hands_ release];
  [foots_ release];
  [super dealloc];
}

もちろんこれで問題はありません。

ただ、最近は↓のほうがメリットがあるんじゃないかと考えてます。

◇改善後

- (void)dealloc {
  self.name = nil; 
  self.brain = nil; 
  self.hands = nil; 
  self.foots = nil; 
  [super dealloc];
}

メリットについて

というのも、◇改善後の場合、assign属性のプロパティであっても同じコードで良いからです。

今回のサンプルでいうと、brainプロパティをassign属性としているので、誤って◆改善前のコードで

  [brain_ release];

としてしまったら、解放してはいけないものなのを解放することになってしまいます。対して◇改善後のコードで

  self.brain = nil; 

となっているのは、アクセサの中で単なるnil代入として扱かってくれるため問題ありません。

こうすることで、開発中にプロパティの属性をretainに変えようが、assignだろうがcopyだろうがdeallocメソッドは修正しなくてよくなります。

私はこれを忘れて不正アクセスやメモリリークが発生することがよくありましたが、◇改善後のコードに統一することで皆無となりました。

※もし、ここのassignでもretainでもプロパティへのnil代入で問題ないよ、という辺りがよくわからんという場合には、こちらに補足記事を書かせていただきましたので、よろしければ見てみてください。


他にも、deallocでない部分で、例えばfinalizeメソッドみたいなものを自分で作った場合、

- (void)finalize {
  [hands_ release];
  [foots_ release];
}

↑だとreleaseにより参照カウンタが0となって実体が解放されたとしてもnil代入されないため、その後にもう1度このメソッドが呼ばれると解放済みのアドレスへのアクセスが発生するためプログラムは不正終了してしまいます。

対して

- (void)finalize {
  self.hands = nil; 
  self.foots = nil; 
}

としておけば、解放後に呼ばれたとしても、解放後はただのnil代入扱いになるので問題ありません。


パフォーマンスが落ちたりしない?

ということで、◇改善後のほうが保守性に優れているのは一目瞭然ですが、ひとつ心配なのは、これにより実行時のパフォーマンスが落ちてしまうことです。

それを調査するためにそれぞれ◆改善前のコードと◇改善後のコードで、今回サンプルとして挙げているHumanクラスのallocとreleaseを10万回繰り返して速度を計測してみました。

  for ( int i = 0; i < 100000; ++i ){
    Human* human = [[Human alloc] init];
    human.brain = self;
    [human release];
  }

結果は以下のとおりです。

試行回数◆改善前◇改善後release1回あたりの差異
1回目3.138秒3.228秒約0.001ミリ秒
2回目3.154秒3.301秒約0.001ミリ秒
3回目3.198秒3.328秒約0.001ミリ秒

※iPhone 3GS (OS 3.1.3) で計測


10万回繰り返してこの差であれば、通常時は◇改善後のやりかたのほうが良いと思います。

この差が問題になるくらいの箇所であれば、きっとC言語で書いていることでしょう。