使用dispatch_group_async的并发代码的性能比单线程版本慢很多

时间:2014-05-15 18:23:50

标签: ios objective-c macos concurrency grand-central-dispatch

我最近在使用大量随机数生成正常分布时做了一些实验。钟曲线。

方法很简单:

  • 创建一个整数数组并将其归零。 (我使用2001整数)
  • 重复计算此数组中的索引并索引数组中的条目,如下所示
    • 循环999或1000次。在每次迭代时:
      • 使用中心值(1000)种子数组索引
      • 生成随机数= + 1 / -1。并将其添加到数组索引
      • 在循环结束时,递增计算出的数组索引处的值。

由于随机值0/1趋于频繁发生,因此来自上方内环的结束指数值趋于保持接近中心值。比起始值大得多/小的索引值越来越不寻常。

经过大量重复后,数组中的值呈现正态分布钟形曲线的形状。然而,我使用的高质量随机函数arc4random_uniform()相当慢,并且需要大量迭代才能生成平滑曲线。

我想绘制1,000,000,000(十亿)点。在主线程上运行,大约需要16个小时。

我决定重写计算代码以使用dispatch_async,并在我的8核Mac Pro上运行它。

我最终使用dispatch_group_async()提交了8个块,并使用dispatch_group_notify()在所有块都已完成处理时通知程序。

为了简化第一遍,所有8个块都写入相同的NSUInteger值数组。读取/修改写入其中一个数组条目的可能性很小,但在这种情况下,这只会导致一个值丢失。我计划稍后为数组增量添加一个锁(或者甚至可能在每个块中创建单独的数组,然后在它们之后对它们求和。)

无论如何,我重构代码以使用dispatch_group_async()并计算每个块中总值的1/8,并设置我的代码以运行。令我极为困惑的是,并发代码虽然最大化了我Mac上的所有内核,但运行速度比单线程代码慢 MUCH

当在单个线程上运行时,我得到每秒约17,800个点。当使用dispatch_group_async运行时,性能下降到更接近665点/秒,或约1/26快。我改变了我提交的块数--2,4或8,这并不重要。表现很糟糕。我还尝试使用dispatch_async提交所有8个块而没有dispatch_group。这也无关紧要。

目前还没有阻止/锁定:所有块都以全速运行。关于为什么并发代码运行得慢,我完全不知道。

现在代码有点混乱,因为我重构它可以单线程或同时工作,所以我可以测试。

这是运行计算的代码:

randCount = 2;
#define K_USE_ASYNC 1

#if K_USE_ASYNC
    dispatch_queue_t highQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
    //dispatch_group_async

    dispatch_group_t aGroup = dispatch_group_create();
    int totalJobs = 8;
    for (int i = 0; i<totalJobs; i++)
    {
      dispatch_group_async(aGroup,
                           highQ,
                           ^{
                             [self calculateNArrayPoints: KNumberOfEntries /totalJobs
                                          usingRandCount: randCount];
                           });
    }


    dispatch_group_notify(aGroup,
                          dispatch_get_main_queue(),
                          allTasksDoneBlock
                          );
#else
    [self calculateNArrayPoints: KNumberOfEntries
                 usingRandCount: randCount];
    allTasksDoneBlock();
#endif

常用的计算方法,由单线程和并发版本使用:

+ (void) calculateNArrayPoints: (NSInteger) pointCount usingRandCount: (int) randCount;
{
  int entry;
  int random_index;

  for (entry =0; entry<pointCount; entry++)
  {
    static int processed = 0;
    if (entry != 0 && entry%100000 == 0)
    {
      [self addTotTotalProcessed: processed];
      processed = 0;
    }

    //Start with a value of 1000 (center value)
    int value = 0;

    //For each entry, add +/- 1 to the value 1000 times.
    int limit  = KPinCount;
    if (randCount==2)
      if (arc4random_uniform(2) !=0)
        limit--;
    for (random_index = 0; random_index<limit; random_index++)
    {
      int random_value = arc4random_uniform(randCount);
      /*
       if 0, value--
       if 1, no change
       if 2, value++
       */
      if (random_value == 0)
        value--;
      else if (random_value == randCount-1)
        value++;
    }
    value += 1000;
    _bellCurveData[value] += 1;
    //printf("\n\nfinal value = %d\n", value);
    processed++;
  }
}

这是一个快速而肮脏的学习项目。它可以在Mac和iOS上运行,因此它使用共享实用程序类。实用程序类只是类方法。没有创建实用程序方法的实例。它有一个令人尴尬的全球变量。如果我最终对代码执行任何有用的操作,我将重构它以创建实用程序单例,并将所有全局变量转换为单例上的实例变量。

现在,它起作用了,全局变量的可怕使用并没有影响结果,所以我离开了它。

使用&#34;处理&#34;的代码变量只是一种计算在并发模式下运行时计算了多少点的方法。在我发现并发版本的可怕性能后,我添加了该代码,因此我确信它不会导致速度减慢。

我被困在这里。我写了大量的并发代码,这个任务是&#34; embarrassingly parallel&#34;问题,所以没有理由不应该在所有可用内核上全速运行。

是否有其他人看到可能导致此问题的任何内容,或者有任何其他见解可以提供?

3 个答案:

答案 0 :(得分:5)

arc4random在修改其状态时使用临界区。在非竞争情况下(当从解锁状态变为锁定状态时),关键部分是超快的,但在竞争情况下(当试图锁定已经锁定的互斥锁时),它必须调用操作系统并将线程放入睡觉,这会大大降低性能。

u_int32_t
arc4random()
{
    u_int32_t rnd;

    THREAD_LOCK();
    arc4_check_init();
    arc4_check_stir();
    rnd = arc4_getword(&rs);
    THREAD_UNLOCK();

    return (rnd);
}

其中THREAD_LOCK()定义为

#define THREAD_LOCK()                       \
    do {                            \
        if (__isthreaded)               \
            _pthread_mutex_lock(&arc4random_mtx);   \
    } while (0)

来源:Arc4 random number generator for OpenBSD

使其更快

你可以创建一个Arc4Random类,它是arc4random.c中静态arc4_ *函数的包装器。然后你有一个不再是线程安全的随机数生成器,但你可以为每个线程创建一个生成器。

答案 1 :(得分:3)

这是推测,所以我无法以这种或那种方式确认它而不实际分析代码(因为它去了)。

也就是说,arc4random为每次通话锁定Apple's collection of source code。因为您可能跨多个线程使用arc4random_uniform,所以,如果不是多次,您至少调用一次。所以我最好的猜测是,每个任务都在等待所有其他任务对arc4random_uniform的调用(或_uniform如果多个并行启动多个调用并且多次​​调用{arc4random,则可能轮流等待自己{1}}是必要的。)

解决这个问题的最简单方法可能是简单地拉出现有的arc4random.c源代码并将其修改为包含在类中,同时从中删除同步(正如我在聊天中建议的那样,或者像Michael建议的那样)或者使用线程本地存储(这可以解决线程安全问题,但可能同样慢 - 没有自己尝试过,所以大量的盐)。请记住,如果你选择了任何一种路线,你将需要一种替代方法来访问iOS上的/dev/random。在这种情况下,我建议使用SecRandomCopyBytes,因为它应该产生与从/dev/random自己阅读相同或更好的结果。

所以,虽然我很确定它是arc4random,但我不能肯定地说没有分析,因为甚至在arc4random开始做它之前可能还有其他因素导致性能问题。

答案 2 :(得分:1)

好的,感谢Michael和Noel的深思熟虑的回应。

确实,似乎arc4random()和arc4random_uniform()使用了spin_lock的变体,并且在多线程使用中性能非常糟糕。

在存在大量冲突的情况下,自旋锁是一个非常糟糕的选择是有道理的,因为自旋锁会导致线程阻塞,直到释放锁,从而占用该核心。 / p>

理想的做法是创建我自己的arc4random版本,在实例变量中维护它自己的状态数组,而不是线程安全的可能是最好的解决方案。然后我会重构我的应用程序,为每个线程创建一个单独的随机生成器实例。

然而,这是我自己研究的一个侧面项目。如果我没有得到报酬,那比我准备花费更多的努力。

作为一个实验,我用rand()替换了代码,单线程的情况要快得多,因为rand()是一个更简单,更快的算法。随机数也不是很好。从我读过的内容来看,rand()在低位中存在循环模式的问题,所以不使用典型的rand()%2,而是使用rand()%0x4000代替,使用第二个 - 取而代之的是最高位。

然而,当我尝试在我的多线程代码中使用rand()时,性能仍然显着下降。它也必须在内部使用锁定。

然后我切换到rand_r(),它接受一个种子值的指针,假设因为它是无状态的,它可能不使用锁定。

宾果。我现在在我的8核Mac Pro上运行415,674点/秒。