12

这个问题我想了很久:

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

我对 pthreads 做了一些初步试验,发现 pthread_create() 大约需要 30us,但是一个简单的 hash_map 插入比在单核上花费的时间要少得多。因此,我很难想象创建一个更快的 hash_map<>,因为同步原语和线程创建是如此缓慢。我也可以想象树遍历和并行平衡,但同样,同步原语似乎使运行时间更长,而不是更短。

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

编辑细节:我认为有几种经常使用的编程/数据结构范例可能会加快速度。例如,我发现自己经常编写基本上看起来像这样的代码(其中真实数据已替换为“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()]++;

这种范式经常用于名称-值设置和配置数据、批处理等。10 倍(或更多)的查找/插入比率使传统的 hash_map<> 成为此类操作的理想选择。

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

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; 
}
4

8 回答 8

6

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

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

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

我的直觉是拥有一个线程池,每个线程都拥有哈希表的不同部分。哈希首先从键映射到哈希表部分,然后映射到该部分中的偏移量。更新将作为消息传递给拥有哈希表该部分的线程。这样一来,没有人会试图一次修改相同的东西。自然,这在具有异步消息传递并发特性的语言(Erlang)中比在其他语言中更容易。

于 2009-02-24T22:31:28.887 回答
3

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

仍然,你是对的,同步时间是瓶颈并且变得更糟,因为它们必须进入 CPU 间总线/桥/通道,无论如何,而大多数其他数据结构试图留在缓存中(甚至在影子寄存器中) .

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

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

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

编辑:我很惊讶没有人对无锁结构有任何意见。检查这个(pdf)和这个(视频)关于 Java 中的无锁哈希表实现,它(几乎)线性扩展至 300 CPU

于 2009-02-24T23:00:18.460 回答
3

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

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

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

如果您正在为 CUDA (GPU) 环境进行编码,您将很快了解到整个世界可以(不,应该!)在操作之前被重铸为一个数组:)

于 2009-02-24T23:14:15.553 回答
1

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

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

但是对于一些更深奥或使用较少的结构,我敢打赌。我敢打赌,重新平衡某些类型的树可以并行化。我敢打赌,遍历图可能是(尽管这可能是算法而不是数据结构)。我敢打赌,可以遍历一个双向链表(从每一端)。

于 2009-02-24T22:31:17.827 回答
1

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

获取一个哈希表并获取一个大的键列表以在哈希表或树中查找。在 2 个 CPU 之间拆分键列表会使性能翻倍。

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

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

于 2009-02-25T02:02:52.217 回答
1

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

本文的早期版本在这里:http ://www.cs.tau.ac.il/~shanir/concurrent-data-structures.pdf

于 2011-03-23T16:27:32.917 回答
0

Javier 有一个很好的观点:如果您正在并行执行操作,那么您已经拥有了线程,您只需要让它们有事可做。

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

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

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

于 2009-02-25T02:04:41.323 回答
0

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

于 2009-02-25T02:08:13.263 回答