blog

スピード・クエスト:Objective-Cの高性能ループ

Cocoaプログラミングの一般的なタスクは、オブジェクトのコレクションをループで処理することです。 この一見単純な問題に対する解決策は数多くあり、その多くはパフォーマンスの問題に対する微妙な考慮がない...

Jan 10, 2016 · 17 min. read
シェア

Cocoaプログラミングの一般的なタスクは、オブジェクトのコレクションをループ処理することです。 この一見単純な問題には多くの解決策があり、その多くは微妙なパフォーマンスを考慮したものです。

スピードの追求

まず最初に、免責事項です。Objective-Cのメソッドの生の速度は、プログラミングをするときに他のどんなことよりも考える必要がある****ものの1つです。

しかし、スピードの二次的な性質が理解を妨げるわけではありません。 パフォーマンスへの配慮が自分の書いているコードにどのような影響を与えるかを常に意識しておくことで、万が一問題が発生したときにどうすればよいかがわかるようになります。

また、ループのシナリオでは、ほとんどの場合、可読性や明瞭性のためにどの技術を選択するかはあまり重要ではありません。 必要以上に遅いエンコード速度を選択する意味はありません。

これを念頭に置いて、以下のオプションが用意されています。

古典的なループ・アプローチ

for (NSUInteger i = 0; i < [array count]; i++){  
  id object = array[i];  
   } 

これは配列をループするシンプルでおなじみの方法ですが、パフォーマンス的にはかなり悪いです。 このコード***の問題は、ループが進むたびに配列のcountメソッドを呼び出していることです。 配列の総数は変わらないので、毎回呼び出すのは冗長です。 このようなコードは通常Cコンパイラによって最適化されますが、Objective-Cは動的な性質を持っているため、このメソッドの呼び出しは自動的に最適化されません。 そのため、パフォーマンスを向上させるには、このようにループの最初に合計数を変数に格納する価値があります。

NSUInteger count = [array count];for (NSUInteger i = 0; i < count; i++){  
  id object = array[i];  
   } 

NSEnumerator

NSEnumerator は、コレクションをループ処理するオプションの方法です。 すべてのコレクションには、呼び出されるたびに NSEnumerator 実体を返す 1 つ以上の列挙メソッドがあります。 与えられた NSEnumerator には、コレクション内の *** オブジェクトへのポインタが含まれ、現在のオブジェクトを返してポインタを成長させる nextObject メソッドがあります。 このメソッドは、コレクションの最後に到達したことを示す nil を返すまで繰り返し呼び出すことができます。

id obj = nil;NSEnumerator *enumerator = [array objectEnumerator];while ((obj = [enumerator nextObject]));{  
  ...            
} 

NSEnumeratorの性能はネイティブのforループに匹敵しますが、インデックスの概念を抽象化しているため、より実用的です。これは、連鎖リストなどの構造化データ、あるいは構造内のエントリ数が未知または未定義の無限シーケンスやストリームでも使用できることを意味します。

高速列挙

- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state   
   objects:(id *)stackbuf count:(NSUInteger)len; 

履き心地が悪そう!」と思われたかもしれません。と思われたかもしれません。 しかし、新しいアプローチには新しいループ構文、for...inループがあります。 これは裏側の新しい列挙メソッドであり、重要なことは、従来の for ループや NSEnumerator メソッドを使用する場合と比較して、構文的にもパフォーマンス的にも中立であることです。

for (id object in array){  
   } 

列挙ブロック

ブロックの誕生により、Appleはブロック構文に基づく第4の列挙メカニズムを追加しました。 これは確かに高速列挙よりも一般的ではありませんが、他の列挙メソッドがオブジェクトのみを返すのに対し、オブジェクトとインデックスの両方が返されるという利点があります。

列挙ブロックのもう1つの重要な機能は、オプションの同時列挙です。 これは、自分のループで何をしようとしているかによって、必ずしも有用ではありませんが、多くの作業を行っていて、列挙の順序をあまり気にしないようなシナリオでは、マルチコア・プロセッサ上で大幅な性能向上をもたらす可能性があります。

ベンチマーキング

では、これらのメソッドを積み重ねるとどうなるでしょうか。 ここに、いくつかの異なる方法を使用してデータの一部を列挙した場合のパフォーマンスを比較する簡単なベンチマークコマンドラインアプリケーションがあります。 最終的な結果を邪魔するような舞台裏での保持や除外処理を除外するため、ARCをオフにして実行しています。 高速なMac上で実行されているため、これらのメソッドはすべて非常に高速に実行され、実際に結果を測定するには10,000,000オブジェクトの配列を使用する必要があります。 iPhoneでテストするのであれば、もっと少ない数を使うのが賢明でしょう!

このコードをコンパイルするには

  • という名前のファイルにコードを保存します。

  • ターミナルでアプリケーションをコンパイルします。

  • プログラムの実行: 

#import <Foundation/Foundation.h> int main(int argc, const char * argv[]){  
  @autoreleasepool  {  
    static const NSUInteger arrayItems = 10000000;  
     NSMutableArray *array = [NSMutableArray arrayWithCapacity:arrayItems];    for (int i = 0; i < arrayItems; i++) [array addObject:@(i)];  
    array = [array copy];  
    
    CFTimeInterval start = CFAbsoluteTimeGetCurrent();  
     // Naive for loop  
    for (NSUInteger i = 0; i < [array count]; i++)  
    {  
      id object = array[i];    }   
    CFTimeInterval forLoop = CFAbsoluteTimeGetCurrent();  
    NSLog(@"For loop: %g", forLoop - start);  
     // Optimized for loop  
    NSUInteger count = [array count];    for (NSUInteger i = 0; i <  count; i++)  
    {  
      id object = array[i];    }   
    CFTimeInterval forLoopWithCountVar = CFAbsoluteTimeGetCurrent();  
    NSLog(@"Optimized for loop: %g", forLoopWithCountVar - forLoop);  
     // NSEnumerator  
    id obj = nil;    NSEnumerator *enumerator = [array objectEnumerator];    while ((obj = [enumerator nextObject]))  
    {     }   
    CFTimeInterval enumeratorLoop = CFAbsoluteTimeGetCurrent();  
    NSLog(@"Enumerator: %g", enumeratorLoop - forLoopWithCountVar);  
     // Fast enumeration  
    for (id object in array)  
    {     }   
    CFTimeInterval forInLoop = CFAbsoluteTimeGetCurrent();  
    NSLog(@"For…in loop: %g", forInLoop - enumeratorLoop);  
     // Block enumeration  
    [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {     }];  
    
    CFTimeInterval enumerationBlock = CFAbsoluteTimeGetCurrent();  
    NSLog(@"Enumeration block: %g", enumerationBlock - forInLoop);  
     // Concurrent enumeration  
    [array enumerateObjectsWithOptions:NSEnumerationConcurrent   
      usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {     }];  
    
    CFTimeInterval concurrentEnumerationBlock = CFAbsoluteTimeGetCurrent();  
    NSLog(@"Concurrent enumeration block: %g",   
      concurrentEnumerationBlock - enumerationBlock);  }  
  return 0;} 

以下はその結果です。

$ For loop: 0.119066  
$ Optimized for loop: 0.092441  
$ Enumerator: 0.123687  
$ For…in loop: 0.049296  
$ Enumeration block: 0.295039  
$ Concurrent enumeration block: 0.199684 

具体的な時間の長さは無視してください。 興味があるのは、他の方法と比べた相対的な大きさです。 速いものから順番に並べると、次のようになります。

  1. For…in循环 – 最快.

  2. forループの最適化 - for...inの2倍遅い。

  3. 最適化されていないforループ -for...inより2.5倍遅い

  4. Enumerator - 最適化されていないループとほぼ同じ。

  5. 並列列挙ブロック - for...inより約6倍遅い。

  6. 列挙ブロック...for...inより約6倍遅い。

フォー...インが勝者です。 高速列挙と呼ばれるのには理由があるようです! 同時並列列挙はシングルスレッドより少し速く見えるかもしれませんが、それ以上のことを考える必要はありません。

並列実行の主な利点は、ループに多くの実行時間がかかる場合です。 自分のループで実行するものがたくさんある場合は、列挙の順序を気にしないのであれば、並列列挙を試してみることを検討してください 。

#p#

その他のコレクション・タイプ

NSSet や NSDictionary などの他のユニオン型についてはどうですか? NSSet は順序付けされていないため、インデックスによってオブジェクトをフェッチするという概念はありません。NSSet と NSDictionary をベンチマークすることも可能です。

$ Enumerator: 0.421863  
$ For…in loop: 0.095401  
$ Enumeration block: 0.302784  
$ Concurrent enumeration block: 0.390825 

結果は NSArray の場合と同じです。 NSDictionary の場合はどうでしょうか? NSDictionary は、キーと値の両方のオブジェクトを反復処理するため、少し異なります。 ディクショナリのキーまたは値のいずれかを反復処理することは可能ですが、通常は両方が必要です。 以下は、NSDictionary で動作するように調整されたベンチマーク・コードです。

#import <Foundation/Foundation.h> int main(int argc, const char * argv[]){  
  @autoreleasepool  {  
    static const NSUInteger dictItems = 10000;  
     NSMutableDictionary *dictionary =   
      [NSMutableDictionary dictionaryWithCapacity:dictItems];    for (int i = 0; i < dictItems; i++) dictionary[@(i)] = @(i);  
    dictionary = [dictionary copy];  
    
    CFTimeInterval start = CFAbsoluteTimeGetCurrent();  
     // Naive for loop  
    for (NSUInteger i = 0; i < [dictionary count]; i++)  
    {  
      id key = [dictionary allKeys][i];      id object = dictionary[key];    }   
    CFTimeInterval forLoop = CFAbsoluteTimeGetCurrent();  
    NSLog(@"For loop: %g", forLoop - start);  
     // Optimized for loop  
    NSUInteger count = [dictionary count];    NSArray *keys = [dictionary allKeys];    for (NSUInteger i = 0; i <  count; i++)  
    {  
      id key = keys[i];      id object = dictionary[key];    }   
    CFTimeInterval forLoopWithCountVar = CFAbsoluteTimeGetCurrent();  
    NSLog(@"Optimized for loop: %g", forLoopWithCountVar - forLoop);  
     // NSEnumerator  
    id key = nil;    NSEnumerator *enumerator = [dictionary keyEnumerator];    while ((key = [enumerator nextObject]))  
    {  
      id object = dictionary[key];    }   
    CFTimeInterval enumeratorLoop = CFAbsoluteTimeGetCurrent();  
    NSLog(@"Enumerator: %g", enumeratorLoop - forLoopWithCountVar);  
     // Fast enumeration  
    for (id key in dictionary)  
    {  
      id object = dictionary[key];    }   
    CFTimeInterval forInLoop = CFAbsoluteTimeGetCurrent();  
    NSLog(@"For…in loop: %g", forInLoop - enumeratorLoop);  
     // Block enumeration  
    [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {     }];  
    
    CFTimeInterval enumerationBlock = CFAbsoluteTimeGetCurrent();  
    NSLog(@"Enumeration block: %g", enumerationBlock - forInLoop);  
     // Concurrent enumeration  
    [dictionary enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent   
      usingBlock:^(id key, id obj, BOOL *stop) {     }];  
    
    CFTimeInterval concurrentEnumerationBlock = CFAbsoluteTimeGetCurrent();  
    NSLog(@"Concurrent enumeration block: %g",   
      concurrentEnumerationBlock - enumerationBlock);  }  
  return 0;}  

NSDictionary は NSArray や NSSet よりも処理速度が遅いため、マシンのロックを避けるためにデータ・エントリ数を 10,000 に減らしています。 そのため、マシンのロックを回避するために、データエントリの数を 10,000 に減らしています。

$ For loop: 2.25899  
$ Optimized for loop: 0.00273103  
$ Enumerator: 0.00496799  
$ For…in loop: 0.001041  
$ Enumeration block: 0.000607967  
$ Concurrent enumeration block: 0.000748038 

最適化されていないループでは、キーの配列が毎回コピーされるため、ここでも速度が著しく低下します。 キーの配列と総数を変数に格納することで、より高速になります。 現在では、オブジェクトを見つけるための消費は他の要因に勝っているため、forループやNSEnumerator、for...inを使用してもほとんど変わりません。 しかし、1 つのメソッドでキーと値の両方を返す列挙子ブロック・メソッドの場合、これが最速の選択肢になります。

リバースギア

おわかりのように、他のすべてが同じであれば、配列をループするときはfor...inループを、辞書をトラバースするときは列挙ブロックを使うようにしましょう。for...inループとenumブロックを使い分けることで、より効率的な処理が可能になります。 たとえば、戻って列挙する必要がある場合や、トラバース中にコレクションを変更する場合などです。

データの一部を戻って列挙するには、reverseObjectEnumerator メソッドを呼び出して、配列を端から端まで走査する NSEnumerator を取得します。 NSEnumerator は、NSArray 自体と同様に、高速列挙プロトコルをサポートしています。 つまり、for...in をこの方法で使用することは、速度や単純さを損なうことなく可能です。

for (id object in [array reverseObjectEnumerator])   
{  
   } 

(気まぐれでない限り、NSSet や NSDictionary に相当するメソッドはありませんし、NSSet や NSDictionary を逆順に列挙することは、キーが順不同であるため、あまり意味がありません)。

列挙ブロック NSEnumerationReverse を使用する場合は、次のようにします。

[array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id obj, NSUInteger idx, BOOL *stop) {  
     }]; 

変化 変異

同じループ・テクニックをコレクションの変更に適用することは可能です。 しかし、配列や辞書をループさせながら変更しようとすると、次のような例外にしばしば直面します。

'*** Collection XYZ was mutated while being enumerated.' 

最適化されたforループのように、これらのループ技法の性能はすべて、あらかじめデータの総量を保存しておくことに依存します。つまり、ループの途中でデータの追加や削除を始めると、正しく処理されないということです。 つまり、ループの途中でデータの追加や削除を始めると、データが正しくなくなるということです。 しかし、ループの途中でデータを追加したり、入れ替えたり、削除したりすることは、しばしばやりたいことです。 では、この問題の解決策は何でしょうか?

古典的なforループは、常駐する総定数に依存しないのでうまく機能します。ただ、データを追加したり削除したりする場合は、インデックスを増減しなければならないことを覚えておいてください。 しかし、forループは高速な解決策ではないことが分かっています。 最適化されたforループは、必要に応じてテクニカル変数やインデックスのインクリメントやデクリメントを忘れない限り、合理的な代替手段です。

for...inを使用することは可能ですが、配列のコピーが最初に作成される場合に限ります。 これは例えば

for (id object in [array copy])   
 {  
   // Do something that modifies the array, e.g. [array removeObject:object];  
 } 

さまざまな手法(元の配列のデータに変更を加えられるように、必要に応じて配列をコピーするオーバーヘッドを含む)をベンチマークすると、コピーによってfor...inループの利点が相殺されることがわかります。

$ For loop: 0.111422  
$ Optimized for loop: 0.08967  
$ Enumerator: 0.313182  
$ For…in loop: 0.203722  
$ Enumeration block: 0.436741  
$ Concurrent enumeration block: 0.388509 

反復処理中に配列の最速カウントを変更するには、最適化されたforループが必要なようです。

NSDictionary の場合、NSEnumerator や高速列挙を使用するために辞書全体をコピーする必要はありません。 これですべてうまくいきます。

// NSEnumerator  
  id key = nil;  NSEnumerator *enumerator = [[items allKeys] objectEnumerator];  while ((key = [enumerator nextObject]))  
  {  
    id object = items[key];    // Do something that modifies the value, e.g. dictionary[key] = newObject;  
  }   // Fast enumeration  
  for (id key in [dictionary allkeys])   
  {  
    id object = items[key];    // Do something that modifies the value, e.g. dictionary[key] = newObject;  
  } 

しかし、enumerateKeysAndObjectsUsingBlock メソッドを使用する場合、同じ手法は機能しません。 ベンチマーク用の辞書をループし、必要に応じてキーや辞書全体のバックアップを作成すると、次のような結果になります。

$ For loop: 2.24597  
$ Optimized for loop: 0.00282001  
$ Enumerator: 0.00508499  
$ For…in loop: 0.000990987  
$ Enumeration block: 0.00144804  
$ Concurrent enumeration block: 0.00166804 

ここでは、for...inループが最も高速であることがわかります。 これは、for...inループでキーによってオブジェクトをフェッチするオーバーヘッドが、enumブロック・メソッドを呼び出してコピーするオーバーヘッドに上書きされているからです。.inループでは、キーに基づいてオブジェクトをフェッチするオーバーヘッドが、enumブロック・メソッドを呼び出して辞書をコピーするオーバーヘッドに上書きされているからです。

NSArray を列挙するとき:

  • シーケンシャルな列挙の場合に使用します。

  • インデックス値を知る必要がある場合や、配列を変更する必要がある場合に使用します。

NSSetを列挙するとき:

  • ほとんどの場合

  • 使用  for 大多数时候

NSDictionary を列挙する場合:

  • ほとんどの場合

  • 使用  for 大多数时候

これらの方法は最速ではないかもしれませんが、どれもとても明快で読みやすいものです。ですから、きれいなコードを書かないか、速いコードを書くかの選択になることがあることを覚えておいてください。

Read next