Entries

スポンサーサイト

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

@encodeにテンプレート引数を使う

タグ: Objective-C C++

構造体からNSValueを作るときにいちいちobjCType:を指定するのが面倒。
もし、@encodeの型指定にテンプレート引数を使えるなら、

#include <string.h>
#include <typeinfo>
#import <Foundation/Foundation.h>

template <typename T>
inline NSValue* make_value(const T& source)
{
    return [NSValue valueWithBytes:&source objCType:@encode(T)];
}

template <typename T>
inline T value_cast(NSValue* value)
{
    if(strcmp([value objCType], @encode(T)) != 0)
        throw std::bad_cast(); // Objective-C例外の方が望ましい?
    T res;
    [value getValue:&res];
    return res;
}

struct Structure
{
    unsigned short n1;
    char n2;
    int n3;
};

void test()
{
    Structure hoge;
    hoge.n1 = 1;
    hoge.n2 = 10;
    hoge.n3 = 100;
    NSValue* value = make_value(hoge);
    
    Structure fuga = value_cast<Structure>(value);
    NSLog(@"%s %d %d %d", [value objCType], fuga.n1, fuga.n2, fuga.n3);
}

こうBoost.Anyみたいに書けて楽なんだが。
さすがにCのsizeof()演算子の引数として使用できる任意の型に「C++のテンプレート仮引数」を指定するのは無茶かと……

{Structure=Sci} 1 10 100

別にそんなことはなかった。
侮れんな、Objective-C++。

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に制限した(これくらい中身のない処理だとこの方が速い)のと、平均の計算の仕方が違う(以前のは複数回実行しての中央値。これは複数ループの平均値)ため。
とりわけ後者の影響が大きい。

NSOperationQueueとスレッドの関係

タグ: Objective-C Mac NSOperation

NSOperationQueueを使ったプログラムをデバッグしていてふと気付いたのだが、NSOperationQueueは何もクソ真面目に「concurrentではない全てのoperationのために毎回スレッドを作っている」わけではないらしい。

例えば以下のコード。

@interface Operation : NSOperation
{
    int number;
}
@property (nonatomic) int number;
@end

@implementation Operation
@synthesize number;
- (void)main
{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"%@ %d", [[NSThread currentThread] retain], number);
    // 同じアドレスに別のインスタンスが作られると紛らわしいのでわざとリークさせている
}
@end

#define NUM 10

void test()
{
    NSOperationQueue *queues[NUM];
    NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
    for(int i = 0; i < NUM; i++)
    {
        NSOperationQueue *queue = [[NSOperationQueue alloc] init];
        [queue setMaxConcurrentOperationCount:1];
        Operation *op = [[Operation alloc] init];
        op.number = i;
        [queue addOperation:op];
        [op release];
        queues[i] = queue;
    }
    for(int i = 0; i < NUM; i++)
    {
        [queues[i] waitUntilAllOperationsAreFinished];
        [queues[i] release];
    }
    NSTimeInterval end = [NSDate timeIntervalSinceReferenceDate];
    NSLog(@"%f", end - start);
}

「『1秒待機』するoperationを10個のNSOperationQueueインスタンスに1つずつaddする」このコードをMacBook Pro(C2D、Mac OS X 10.5.6)で実行すると次のような結果となる。

Test[383:1103] <NSThread: 0x111080>{name = (null), num = 3} 1
Test[383:1003] <NSThread: 0x1107e0>{name = (null), num = 2} 0
Test[383:1703] <NSThread: 0x104f80>{name = (null), num = 4} 2
Test[383:1803] <NSThread: 0x111220>{name = (null), num = 5} 3
Test[383:1903] <NSThread: 0x110f40>{name = (null), num = 6} 4
Test[383:1a03] <NSThread: 0x110e10>{name = (null), num = 7} 5
Test[383:1c03] <NSThread: 0x111300>{name = (null), num = 9} 7
Test[383:1b03] <NSThread: 0x111490>{name = (null), num = 8} 6
Test[383:1d03] <NSThread: 0x1112c0>{name = (null), num = 10} 8
Test[383:1e03] <NSThread: 0x110fe0>{name = (null), num = 11} 9
Test[383:813] 1.166345

全処理完了に約1秒。
全てのoperationが並行して動作したようだ。

ではOperationクラスのmainを次のように書き換えたらどうだろう。

- (void)main
{
    NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
    while([NSDate timeIntervalSinceReferenceDate] - start < 1)
    {}
    NSLog(@"%@ %d", [[NSThread currentThread] retain], number);
    // 同じアドレスに別のインスタンスが作られると紛らわしいのでわざとリークさせる
}
Test[441:1003] <NSThread: 0x10fc00>{name = (null), num = 2} 0
Test[441:1103] <NSThread: 0x104f80>{name = (null), num = 3} 1
Test[441:1003] <NSThread: 0x10f8f0>{name = (null), num = 4} 2
Test[441:1103] <NSThread: 0x10e370>{name = (null), num = 5} 3
Test[441:190b] <NSThread: 0x10e230>{name = (null), num = 6} 4
Test[441:1c07] <NSThread: 0x1107c0>{name = (null), num = 7} 5
Test[441:1003] <NSThread: 0x110580>{name = (null), num = 8} 6
Test[441:1103] <NSThread: 0x10eb70>{name = (null), num = 9} 7
Test[441:190b] <NSThread: 0x10ee70>{name = (null), num = 10} 8
Test[441:1c07] <NSThread: 0x110680>{name = (null), num = 11} 9
Concurrent[441:813] 5.010538

全処理完了に約5秒。
前のコードもこのコードもoperationの内容は「1秒待機」に変わりないが、こちらはループで無駄にCPU負荷が掛かっているため、同時に処理されるoprationの数がNSOperationQueueクラス単位?(少なくともインスタンス単位ではない)で2(CPUコア数?)に制限されたようだ。

さて、ここで気になるのは「最終的にいくつのスレッドが生成されたか」ということ。
結果を見ると、どちらのログでも「全てのoperationに異なるNSThreadが割り振られている」ので、メインスレッドを除いて「10スレッド生成された」と言えそうだが、これはある意味では正しくない。
というのも、NSThreadの手前に出力されている

Test[441:1003]

これは「プロセス名:[プロセスID:スレッドID]」なわけだが、このスレッドIDが最初の結果ではメインスレッドの分(831)を除いて「10種類」あるのに対し、次の結果では「4種類」しかないからだ。
つまり、後者は「4スレッドで処理を回している」ことになる。
もちろん、デタッチされたスレッドIDは再利用可能なため、「スレッドIDが4つだから生成されたスレッド数が4」と言い切れるわけではない。
しかし、デバッガを使って関数testの終わりで

(gdb) t a a bt

してみると、

Thread 5 (process 441 thread 0x1c07):
#0  0x94ff1292 in __workq_ops ()
#1  0x94ff298a in workqueue_exit ()
#2  0x94ff12c2 in start_wqthread ()
Current language:  auto; currently objective-c

Thread 4 (process 441 thread 0x190b):
#0  0x94ff1292 in __workq_ops ()
#1  0x94ff298a in workqueue_exit ()
#2  0x94ff12c2 in start_wqthread ()

Thread 3 (process 441 thread 0x1103):
#0  0x94ff1292 in __workq_ops ()
#1  0x94ff298a in workqueue_exit ()
#2  0x94ff12c2 in start_wqthread ()

Thread 2 (process 441 thread 0x1003):
#0  0x94ff1292 in __workq_ops ()
#1  0x94ff298a in workqueue_exit ()
#2  0x94ff12c2 in start_wqthread ()

Thread 1 (process 441 local thread 0x2e03):

このように「処理を回したスレッドが次の処理を求めて待機しているような」風景が見られるので、「処理を終えたスレッドのIDが再利用された」と考えるよりも、「最小限のスレッド数で処理を回している」と考えた方がやはりスマートだろう。

ちなみに生成されたスレッドの数が「2」ではなく「4」なのは、おそらく「NSLogでもたついている間に新しいスレッドを作って次のoperationが開始された」ため。
mainの中のNSLogを削除すると、少なくとも私の環境では最終的に「2」スレッドしか残らない。

Doxygen 1.5.9

タグ: Doxygen Objective-C C++

id 570960: C++ class defined in a .mm file was sometimes parsed as Objective-C code.

たとえば、拡張子が「.mm」のファイルに以下のコードを記述し、Doxygen 1.5.8でドキュメント化すると、

/// C++のクラス1
class Class1
{
public:
    /// コンストラクタ
    Class1();
};

/// C++のクラス2
class Class2
{
public:
    /// コンストラクタ
    Class2();
};

/// C++の派生クラス
class Class3 : public Class1
{
public:
    /// コンストラクタ
    Class3();
};

Class1の説明は正常に生成されるが、

158_Class1.png

Class2はコンストラクタの構文がObjective-Cっぽくなり、

158_Class2.png

Class3に至っては完全に無視される。

158_annotated.png

Doxygen 1.5.9ではこの問題は修正されており、Class2も

159_Class2.png

Class3も正しく出力される。

159_Class3.png

Appendix

タグ

Blog内検索

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