Grand Central Dispatchのキューを増やし高速化

昨日の Grand Central Dispatchを使ったアプリ のアイコン画像取得処理ですが、この処理時間の大部分は Twitterサーバーとの通信時間です。しかもこの処理は複数を同時に動かしてもまったく問題がありません (Twitterのサーバーの負荷が上がるかな?)。

ということで、アイコン画像取得キュー(=スレッド)数を増やして実験してみました。


http://upload.wikimedia.org/wikipedia/en/b/bd/Gcd_icon20090608.jpg


コードの変更はごくわずかで、

  1. キュー用の変数を配列にする
  2. キューの生成、解放を複数個にする
  3. 画像取得の dispatch_async で複数のキューを使うように指定する

です、実際に変更した部分の概要は以下です、ただし、処理時間測定用のコードも入っています。

  • GCDSampleViewController.h
@interface GCDSampleViewController : UITableViewController <UITableViewDelegate, UITableViewDataSource> {
	NSMutableArray *tweetMessages;
	NSMutableArray *tweetIconURLs;

	dispatch_queue_t main_queue;
	dispatch_queue_t timeline_queue;
	dispatch_queue_t image_queue[10];
}
  • GCDSampleViewController.m
#import <sys/time.h>

#define	IMAGE_Q_SIZE 3

void elapsedTimeLog(NSString *msg) {
    static struct timeval lastTime;
    struct timeval currentTime;
    
    gettimeofday(&currentTime, NULL);
    if (msg) {
        float t = ((currentTime.tv_sec * 1000000 + currentTime.tv_usec) -
                   (lastTime.tv_sec * 1000000 + lastTime.tv_usec)) / 1000000.0;
        NSLog(@"++ %@ : %6.3f", msg, t);
    } else {
        NSLog(@"++ timer started");
        lastTime = currentTime;
    }
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    main_queue = dispatch_get_main_queue();
    timeline_queue = dispatch_queue_create("com.ey-office.gcd-sample.timeline", NULL);
    char qLabel[] = {"com.ey-office.gcd-sample.image0"};
    for (int i = 0; i < IMAGE_Q_SIZE; i++) {
        qLabel[strlen(qLabel) - 1] = '0' + i;
        image_queue[i] = dispatch_queue_create(qLabel, NULL);
    }
    
    dispatch_async(timeline_queue, ^{
        [self getPublicTimeline];
        dispatch_async(main_queue, ^{
            [self.tableView reloadData];
        });
    });
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];

    dispatch_release(timeline_queue);
    for (int i = 0; i < IMAGE_Q_SIZE; i++) {
        dispatch_release(image_queue[i]);
    }
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    elapsedTimeLog(nil);
    return tweetMessages ? [tweetMessages count] : 1;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellIdentifier = @"SampleTable";
    
      。。。。 省略 。。。。
      
    cell.imageView.image = [UIImage imageNamed:@"blank.png"];

    dispatch_async(image_queue[[indexPath row] % IMAGE_Q_SIZE], ^{
        UIImage *icon = [self getImage:[tweetIconURLs objectAtIndex:[indexPath row]]];
        dispatch_async(main_queue, ^{
            UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
            cell.imageView.image = icon;
        });
        elapsedTimeLog([NSString stringWithFormat:@"icon %d displayed", [indexPath row]]);
    });

    return cell;
}

3. は (テーブルの行番号 % キュー数) でキューを切り替えるようにしました。 計測用のタイマーですがテーブルを表示する前には
tableView:numberOfRowsInSection: が呼び出されるのでここに入れました、全てのアイコンが表示されたタイミングはコードでは判らないかと思い、アイコン表示毎にタイマーの値を表示して人が判断する事にしました。

結果

アイコンの取得時間は、3G通信速度、Twitterサーバーの負荷具合、画像の大きさによるので5回測定し、最大、最小値を捨てた3回の平均です。

キュー数 表示時間(秒)
1 2.0
2 1.6
3 1.5
4 1.0
5 0.86
6 1.2

やはり、キュー(=スレッド)数を増やすと高速化されますが、ある数以上からはあまり効果が上がらないという教科書通りの結果になりました ^^)

また、sakamotoさんの指摘にあったグローバルキューを image_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); を試したところ、キュー数 6 とほぼ同じ時間になりました。


結論としては、
GCDを使ったアプリで、並列に動かしても良い部分はキューを複数にしたり、グローバルキューを使う事で簡単に高速化できるので、ますます GCD を使うべきだ !!