Entries

スポンサーサイト

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

NSOperationQueueのコスト比較

タグ: Objective-C Mac NSOperation

前回の記事で「NSOperationQueueは最小限のスレッドを作って処理を回す」と書いたが、実際のところそれは他の方法と比べてどれだけ効率的なのか。
ここでは、以前NSOperationQueueのコストを計るために使ったのとほぼ同じ処理を、

  1. NSOperationQueueを普通に使って処理を回す
  2. NSOperationQueueを使い、毎回スレッドを作って処理する
  3. NSOperationQueueを使わずに別スレッドで処理を回す

の3つの方法で実装し、同じMac(ただしOSはMac OS X 10.5.6に更新済み)で処理時間を比較してみる。

まずは、3つの方法全てで使用する「値を2乗してしかるべき場所に格納するクラス」を作成。

/// value を2乗して *result に格納する処理を行うクラス
@interface Request : NSObject
{
    int value;
    int *result;
}
- (void)invoke;
@property (nonatomic) int value;
@property (nonatomic) int *result;
@end
@implementation Request
@synthesize value, result;
- (void)invoke
{
    *result = value * value;
}
@end

「毎回スレッドを作って処理するNSOperation」を作成。

/// 毎回スレッドを作って処理する
@interface ThreadOperation : NSOperation
{
    Request *request;
    BOOL isExecuting;
    BOOL isFinished;
}
@property (nonatomic, retain) Request *request;
@property BOOL isExecuting;
@property BOOL isFinished;
@end
@implementation ThreadOperation
@synthesize request, isExecuting, isFinished;
/// @name NSObject クラスメソッドのオーバーライド
//@{
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if([key isEqualToString:@"isExecuting"] || [key isEqualToString:@"isFinished"])
    {
        return YES;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
//@}
/// @name 初期化と解体
//@{
- (void)dealloc
{
    [request release];
    [super dealloc];
}
//@}
/// @name プライベートメソッド
//@{
- (void)threadMain
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    [request invoke];
    self.isExecuting = NO;
    self.isFinished = YES;
    [pool release];
    [NSThread exit];
}
//@}
/// @name NSOperation インスタンスメソッドのオーバーライド
//@{
- (BOOL)isConcurrent
{
    return YES;
}
- (void)start
{
    self.isExecuting = YES;
    [NSThread detachNewThreadSelector:@selector(threadMain) toTarget:self withObject:nil];
}
//@}
@end

「NSOperationQueueを使わずに別スレッドで処理を回すクラス」を作成。

#include <pthread.h>

/// NSOperationQueue を使わずに別スレッドで処理を回す
@interface RequestQueue : NSObject
{
    NSMutableArray *_requests;
    // volatile は……いらないか
    /*volatile*/ NSUInteger _workerThreadCount;
    /*volatile*/ NSUInteger _workingThreadCount;
    // ミューテクス1つに対しコンディションを1つしか持てない NSCondition では以下を代替出来ない……
    pthread_mutex_t _mutex;
    pthread_cond_t _popOrWaitCondition;
    pthread_cond_t _workingCondition;
}
- (void)pushRequest:(id)request;
- (void)waitUntilAllRequestsAreFinished;
@property NSUInteger workerThreadCount;
@end
@implementation RequestQueue
/// @name 初期化と解体
//@{
- (id)init
{
    if(self = [super init])
    {
        _requests = [[NSMutableArray alloc] init];
        pthread_mutex_init(&_mutex, NULL); // プロセスプライベート、再帰ロック不可
        pthread_cond_init(&_popOrWaitCondition, NULL); // プロセスプライベート
        pthread_cond_init(&_workingCondition, NULL); // プロセスプライベート
    }
    return self;
}
- (void)dealloc
{
    pthread_cond_destroy(&_workingCondition);
    pthread_cond_destroy(&_popOrWaitCondition);
    pthread_mutex_destroy(&_mutex);
    [_requests release];
    [super dealloc];
}
//@}
/// @name プライベートメソッド
//@{
- (id)popOrWaitRequest
{
    pthread_mutex_lock(&_mutex);
    _workingThreadCount--;
    NSUInteger count = [_requests count];
    if(_workingThreadCount == 0 && count == 0)
    {
        // waitUntilAllRequestsAreFinished で待機しているスレッドを全て動かす
        pthread_cond_broadcast(&_workingCondition);
    }
    BOOL living;
    while((living = (_workingThreadCount < _workerThreadCount)) && count == 0)
    {
        // _requests の要素数が増えるか _workerThreadCount が減るのを待つ
        pthread_cond_wait(&_popOrWaitCondition, &_mutex);
        count = [_requests count];
    }
    id request = nil;
    if(living)
    {
        request = [[[_requests objectAtIndex:0] retain] autorelease];
        [_requests removeObjectAtIndex:0];
        _workingThreadCount++;
    }
    pthread_mutex_unlock(&_mutex);
    return request;
}
- (void)threadMain
{
    id request = nil;
    do
    {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
        request = [self popOrWaitRequest];
        [request invoke];
        [pool release]; // 以下、 request は release されているので扱いに注意
    } while(request);
    [NSThread exit];
}
//@}
/// @name パブリックメソッド
//@{
- (void)pushRequest:(id)request
{
    pthread_mutex_lock(&_mutex);
    [_requests addObject:request];
    // popOrWaitRequest で待機しているスレッドを1つ動かす
    pthread_cond_signal(&_popOrWaitCondition); 
    pthread_mutex_unlock(&_mutex);
}
- (void)waitUntilAllRequestsAreFinished
{
    pthread_mutex_lock(&_mutex);
    while(_workingThreadCount > 0 || [_requests count] > 0)
    {
        // _requests の要素数 と _workingThreadCount が 0 になるのを待つ
        pthread_cond_wait(&_workingCondition, &_mutex);
    }
    pthread_mutex_unlock(&_mutex);
}
//@}
/// @name パブリックプロパティ
//@{
- (void)setWorkerThreadCount:(NSUInteger)workerThreadCount
{
    pthread_mutex_lock(&_mutex);
    int newThreadCount = workerThreadCount - _workerThreadCount;
    _workerThreadCount = _workerThreadCount + newThreadCount;
    if(newThreadCount > 0)
    {
        for(int i = 0; i < newThreadCount; i++)
        {
            _workingThreadCount++;
            [NSThread detachNewThreadSelector:@selector(threadMain) toTarget:self withObject:nil];
        }
    }
    else if(newThreadCount < 0)
    {
        // popOrWaitRequest で待機しているスレッドを全て動かす
        pthread_cond_broadcast(&_popOrWaitCondition);
    }
    pthread_mutex_unlock(&_mutex);
}
- (NSUInteger)workerThreadCount
{
    pthread_mutex_lock(&_mutex);
    NSUInteger workerThreadCount = _workerThreadCount;
    pthread_mutex_unlock(&_mutex);
    return workerThreadCount;
}
//@}
@end

以上を使って検証コードを作成。

/// 念のため処理結果を確認する
void check_results(int *results, int number)
{
    for(int i = 0; i < number; i++)
    {
        if(results[i] != i * i)
        {
            NSLog(@"%d: expected:%d actual:%d", i, i * i, results[i]);
        }
    }
}

/// NSOperationQueueを普通に使って処理を回す
NSTimeInterval test_operation(int number)
{
    int results[number];
    NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:1];
    for(int i = 0; i < number; i++)
    {
        Request *request = [[Request alloc] init];
        request.value = i;
        request.result = results + i;
        NSInvocationOperation *op =
            [[NSInvocationOperation alloc]
                initWithTarget:request
                selector:@selector(invoke)
                object:nil
            ];
        [request release];
        [queue addOperation:op];
        [op release];
    }
    [queue waitUntilAllOperationsAreFinished];
    [queue release];
    NSTimeInterval end = [NSDate timeIntervalSinceReferenceDate];
    check_results(results, number);
    return end - start;
}

/// NSOperationQueueを使い、毎回スレッドを作って処理する
NSTimeInterval test_thread_operation(int number)
{
    int results[number];
    NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue setMaxConcurrentOperationCount:1];
    for(int i = 0; i < number; i++)
    {
        Request *request = [[Request alloc] init];
        request.value = i;
        request.result = results + i;
        ThreadOperation *op = [[ThreadOperation alloc] init];
        op.request = request;
        [request release];
        [queue addOperation:op];
        [op release];
    }
    [queue waitUntilAllOperationsAreFinished];
    [queue release];
    NSTimeInterval end = [NSDate timeIntervalSinceReferenceDate];
    check_results(results, number);
    return end - start;
}

/// NSOperationQueueを使わずに別スレッドで処理を回す
NSTimeInterval test_request_queue(int number)
{
    int results[number];
    NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
    RequestQueue *requestQueue = [[RequestQueue alloc] init];
    requestQueue.workerThreadCount = 1; // 指定した数だけスレッドが作られる。
    for(int i = 0; i < number; i++)
    {
        Request *request = [[Request alloc] init];
        request.value = i;
        request.result = results + i;
        [requestQueue pushRequest:request];
        [request release];
    }
    [requestQueue waitUntilAllRequestsAreFinished];
    requestQueue.workerThreadCount = 0; // これを忘れるとスレッドが止まらない。要改善点。
    [requestQueue release];
    NSTimeInterval end = [NSDate timeIntervalSinceReferenceDate];
    check_results(results, number);
    // この時点でスレッドが止まっている保証はないので、次の計測に多少影響を及ぼしているかもしれない。
    return end - start;
}

void test()
{
    NSTimeInterval (*tests[])(int) = { test_operation, test_thread_operation, test_request_queue };
    NSTimeInterval intervals[3] = { 0 };
    for(int i = 0; i < 10; i++)
    {
        for(int j = 0; j < 3; j++)
        {
            NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
            intervals[j] += tests[j](10000);
            [pool release];
        }
    }
    NSLog(@"operation:        %f", intervals[0] / 10);
    NSLog(@"thread operation: %f", intervals[1] / 10);
    NSLog(@"request queue:    %f", intervals[2] / 10);
}

そして実行。

operation:        0.542652
thread operation: 1.370841
request queue:    0.201903

さすがに「毎回スレッドを作って処理する(thread operation)」よりかは速いが、「NSOperationQueueを使わずに別スレッドで処理を回す(request queue)」のに比べたら遅い。
とはいえ所詮呼び出しコストの差なので、関数がインライン展開されるかを気にするのと似たようなレベルの差とも言える。

ちなみに、以前の結果(0.9秒)と比べると、ほぼ同等の処理であるoperationの結果が倍近く速くなっているが、これは最大同時処理数を1に制限した(これくらい中身のない処理だとこの方が速い)のと、平均の計算の仕方が違う(以前のは複数回実行しての中央値。これは複数ループの平均値)ため。
とりわけ後者の影響が大きい。

スポンサーサイト

コメント

コメントの投稿

コメントの投稿
管理者にだけ表示を許可する

トラックバック

トラックバック URL
http://idlysphere.blog66.fc2.com/tb.php/198-7a10a012
この記事にトラックバックする(FC2ブログユーザー)

Appendix

タグ

Blog内検索

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。