多核机器上更快的基础数据结构?

时间:2009-02-24 22:25:53

标签: multithreading data-structures parallel-processing

我一直在思考这个问题:

你能否利用你已经拥有的更多的事实,在多核机器上建立一个更快的基础数据结构(即链表,散列表,集合,跳过列表,布隆过滤器,红黑树等)一个CPU?

我做了一些pthreads的初步试验,发现pthread_create()的顺序为30us,但是一个简单的hash_map插入所花费的时间远远少于单个核心。因此,我很难想象创建一个更快的hash_map<>,因为同步原语和线程创建是如此之慢。我也可以想象树的遍历和并行平衡,但同样,同步原语似乎会使运行时更长,而不是更短。

对我来说,“我有更多的CPU,因此,我应该能够更快地完成它”,这仍然让我感觉很直接“,但我不能完全围绕证据或反证据证明这一说法。我在C ++中进行了相当多的实验,但我现在怀疑其他语言可能会为这项任务提供更好的解决方案(erlang?)。想法?

EDIT详细信息:我认为有一些经常使用的编程/数据结构范例可能会加速。例如,我发现自己经常编写基本上看起来像这样的代码(实际数据已被“rand()”替换)

static const int N = 1000000; 
static const int M = 10000000; // 10x more lookups 
hash_map<int, int> m; 
// batch insert a bunch of interesting data 
for (int i = 0; i < N; i++) m[rand()] = rand(); 

// Do some random access lookups. 
for (int i = 0; i < M; i++) m[rand()]++;

这种范例经常用于名称设置和范围之类的事情。配置数据,批处理等.10x(或更多)查找/插入比率是传统hash_map&lt;&gt;的原因。这种操作的理想选择。

这可以很容易地分成两半,具有插入阶段和查找阶段,并且在并行世界中,两半之间可能存在一些“刷新队列”操作。交错插入+查找版本更难:

hash_map<int, int> m; 

for (int i = 0; i < N; i++) { 
   if (rand() % LOOKUP_RATIO == 0) 
     hash_map[rand()]++;  // "lookup" 
   else 
     hash_map[rand()] = rand();  // "insert" 
}

在这种情况下,只要在每次查找之前刷新插入队列,插入就可以是异步的,如果LOOKUP_RATIO足够大(例如,> 1000),那么它变得非常类似于上面的批处理示例,但是有一些排队。虽然,排队意味着同步原语。

想象一下,以下片段:

hash_map<int,int> a;
hash_map<int,int> b; 
for (int i = 0; i < N; i++) { 
  // the following 2 lines could be executed in parallel 
  a[rand()] = rand(); 
  b[rand()] = rand(); 
}

因此,查找可以通过以下方式“并行”完成:

int lookup(int value) { 
  // The following 2 lines could be executed in parallel: 
  v1 = a[value]; 
  v2 = b[value]; 
  if (v1)  // pseudo code for "value existed in a" 
    return v1; 
  else 
    return v2; 
}

8 个答案:

答案 0 :(得分:6)

问题在于共享数据本身就是并行计算的祸根。理想情况下,您希望每个核心处理单独的数据,否则会产生与同步相关的开销。 (如何在没有共享状态的情况下进行通信?通过消息传递。)

另外,谈论加速数据结构有点奇怪。我发现谈论正在加速的数据结构的操作更自然,因为不同数据结构上的不同操作具有不同的特征。是否存在您希望加速的特定类型的访问?

编辑,回应额外的细节:我假设目标是有一个可以并行访问的哈希映射,它的基础可能是多个哈希表,但是它将透明地呈现给用户此数据结构为单个哈希表。当然,我们会担心花太多时间在锁上旋转。同样在这个级别,我们必须了解缓存一致性问题。也就是说,如果核心或处理器具有指向相同数据的单独高速缓存,并且一个修改数据,则另一个上的高速缓存数据无效。如果这种情况反复发生,可能会产生巨大的成本,并行性可能比在单个核心上进行操作更糟糕。所以我非常警惕共享数据。

我的直觉是拥有一个线程池,每个线程拥有哈希表的不同部分。哈希首先从密钥映射到哈希表部分,然后映射到该部分内的偏移量。更新将作为消息传递给拥有该哈希表部分的该线程。这样,没有人试图同时修改同一件事。当然,这在语言(Erlang)中更容易,它具有异步消息传递并发性的特性,而不是其他语言。

答案 1 :(得分:3)

首先,我认为将pthread_create()时间与hashmap操作进行比较是不合适的。与竞争和非竞争情况下的(非)锁定时间进行比较更好。

然而,你是对的,同步时间是瓶颈而且越来越糟,因为它们必须转到CPU间总线/桥接器/通道,无论如何,而大多数其他数据结构都试图保留在缓存中(甚至在影子登记册。)

有两个主要方向来解决这个问题:

  1. 更好的共享结构:检查无锁结构和/或事务内存。两者都尝试通过'try-check-commit / rollback'替换'lock-modify-release'循环来最大化可访问性。在大多数情况下,检查应该成功,因此回滚不应影响平均性能。通常检查/提交是以原子方式完成的,因此它在CPU带宽方面很昂贵,但它比传统锁定要少得多。

  2. 少分享:这就是erlang / haskell语言所强调的。使传输小消息变得容易和便宜,线程间通信看起来更像是带参数的函数调用,而不是共享内存。这样可扩展性更高,因为只有两个进程必须同步,并且(理论上)可以使用具有较低延迟的非RAM通道。

  3. 编辑: 我很惊讶没有人对无锁结构有任何意见。检查this(pdf)和this(视频)关于Java中的无锁哈希表实现,可以(几乎)线性扩展到300 CPUS

答案 2 :(得分:3)

我每天都在处理这个问题。我发现链接列表之类的东西确实非常有用,因为您可以让并行算法的每个线程构建自己的链表,然后在完成后将它们一起缝在主表上。几乎没有开销,只要您的线程真正独立

如果你每个人都有数据阵列要使用,我发现为每个线程分配一个较小的数组几乎总是更好,然后在完成时将小数组合并回主数组 - 事实上,如果你是在集群环境中,使用“相同”数组甚至不可能!

如果你正在实现一个使用关联数组的算法(想想.NET Dictionary),那么你几乎总是会在线程之间复制某些工作。尽可能避免这些。

如果您正在为CUDA(GPU)环境编写代码,那么您将非常快速地了解到整个世界可以(不应该!)在操作之前重新编排为数组:)

答案 3 :(得分:1)

我认为您需要查看数据结构并询问“这可以在异步中完成什么?”

对于很多数据结构,我看到的内容并不多。

但对于一些更深奥或更少使用的结构,我打赌有。我打赌重新平衡一些种类的树可以并行化。我打赌遍历图可能是(虽然这可能是比数据结构更多的算法)。我打赌遍历一个双向链表(从每一端)可能是。

答案 4 :(得分:1)

我不相信在单个查找中有很多并行性。但如果您要查找完整的项目列表,则情况就不同了。

获取哈希表并获取大量密钥以在哈希表或树中查找。将两个CPU之间的密钥列表分开会使性能提高一倍。

或者列出要插入的大量项目。将哈希表划分为每个CPU区域并划分键列表。然后每个CPU都可以将项目填充到自己的哈希表中。

这也适用于向量,B +树和二叉树,但我相信哈希表可以构建为需要稍微更少的锁定来进行更新。

答案 5 :(得分:1)

请查看此CACM文章 - 多核时代的数据结构(不幸的是它是高级内容):http://cacm.acm.org/magazines/2011/3/105308-data-structures-in-the-multicore-age/fulltext

该论文的早期版本在此处:http://www.cs.tau.ac.il/~shanir/concurrent-data-structures.pdf

答案 6 :(得分:0)

Javier有一个很好的观点:如果你并行执行操作,你已经获得了线程,你只需要给他们一些事情。

我认为很多归结为标准的读者和作家问题。如果他们所做的只是读取或其他非破坏性操作,您应该能够使用哈希表拥有几乎无限数量的线程。但是,一旦其中一个需要进行写操作,那么他们必须在整个哈希表上获取一个独占锁(除非你首先在外部对你的密钥进行哈希处理,然后理论上他们可以锁定他们哈希到的桶,取决于您的碰撞解决机制。)

要考虑的一件事是每个数据结构有一个(或一个小池)线程,并将访问视为“服务”。也就是说,它不是在哈希映射中查找某个内容的线程,而是向服务该数据结构的线程发出同步请求。本地化锁定操作(只有服务请求的线程必须知道锁定技术),但可能会使请求队列成为瓶颈。

我认为,正如其他人所说,利用并行性的最佳方法是通过算法,而不是数据结构。

答案 7 :(得分:0)

将所有内容放入工作队列中。这是关键 - 让您更接近跨多台计算机进行扩展。同步是昂贵的,以后只会变得更加昂贵(想象一下有128个CPU的内存屏障)。