我有一个有趣的数据结构设计问题,超出了我目前的专业知识。我正在寻找关于解决这个问题的数据结构或算法答案。
要求:
(pointer address, size)
对(实际上是两个数字;第一个用作排序键)(address, size)
对之一内 - 即,如果对定义了内存范围,如果指针位于列表中的任何范围。线程很少会添加或删除此列表中的条目。我热衷于避免一种天真的实现,比如有一个关键部分来序列化对排序列表或树的访问。哪些数据结构或算法可能适合此任务?
因为我正在使用该语言而使用Delphi标记 这个任务。语言无关的答案非常受欢迎。
但是,我可能无法使用任何标准 任何语言的图书馆都没有多少关心。原因是内存访问 (对象的分配,释放等及其内部记忆,例如 树节点等)严格控制,必须通过我自己的 功能。我当前在同一程序中的其他代码使用 红色/黑色的树和有点特里,我自己写了这些。宾语 节点分配通过自定义内存分配例程运行。 这超出了问题的范围,但这里提到要避免 像'使用STL结构foo'这样的答案。我热衷于算法或 结构答案,只要我有正确的参考或教科书, 我可以自己实现。
答案 0 :(得分:3)
我会使用TDictionary<Pointer, Integer>
(来自Generics.Collections
)和TMREWSync
(来自SysUtils
)进行多读独占写访问。只要没有编写器处于活动状态,TMREWSync
就允许多个读者同时访问字典。字典本身提供O(1)指针查找。
如果您不想使用RTL类,答案就变成:使用散列映射与多读取独占写同步对象结合使用。
编辑:刚刚意识到您的对实际上代表了内存范围,因此哈希映射不起作用。在这种情况下,您可以使用排序列表(按内存地址排序),然后使用二进制搜索快速查找匹配范围。这使得查找O(log n)而不是O(1)。
答案 1 :(得分:2)
探索一下复制的想法......
从正确的角度来看,读/写锁将完成工作。然而, 在实践中,虽然读者可以同时并行地进行 通过访问结构,他们将在锁上创建一个巨大的争用 即使是读取访问锁定的明显原因也包括写入到锁本身。 这将破坏多核系统中的性能,甚至更多地插入多插槽中 系统
性能低下的原因是缓存行无效/传输流量 核心/套接字之间。 (作为旁注,这是一项非常近期且非常有趣的研究 关于主题Everything You Always Wanted to Know About Synchronization but Were Afraid to Ask)。
当然,我们可以通过制作来避免读者触发的核心间缓存传输 每个核心上的结构副本,并限制读取器线程仅访问 他们当前正在执行的核心本地副本。这需要一些线程获取其当前核心ID的机制。它还依赖于操作系统调度程序,以便不在核心上无偿地移动线程,即在某种程度上维持核心亲和性。 AFACT,大多数当前的操作系统都是这样做的。
对于作者来说,他们的工作是通过获取每个写入锁来更新所有现有的副本。一次更新一棵树(显然结构应该是一些树)确实意味着复制品之间的临时不一致。从问题 描述这种接缝可以接受。当作家工作时,它会阻止读者一个人 核心,但并非所有读者。缺点是作家具有相同的工作 很多次 - 系统中有核心或套接字的时间很多。
PS。
也许,只是也许,另一种选择是某种RCU - 类似方法,但我不知道 这很好,所以我会在提到之后停止:)
答案 2 :(得分:1)
通过复制,您可以拥有: - 您的数据结构的一个副本(列表w /二进制搜索,提到的间隔树,...)(例如,“原始”一个)仅用于查找(读取访问)。 - 当要更改数据(写访问)时,创建第二个副本“更新”。因此写入更新副本。
写完成后,将“当前”指针从“原始”更改为“更新”版本。将访问计数器与“原始”副本相关联,当计数器减少回零读者时,可以销毁该副本。
在伪代码中:
// read:
data = get4Read();
... do the lookup
release4Read(data);
// write
data = get4Write();
... alter the data
release4Write(data);
// implementation:
// current is the datat structure + a 'readers' counter, initially set to '0'
get4Read() {
lock(current_lock) { // exclusive access to current
current.readers++; // one more reader
return current;
}
}
release4Read(copy) {
lock(current_lock) { // exclusive access to current
if(0 == --copy.readers) { // last reader
if(copy != current) { // it was the old, "original" one
delete(copy); // destroy it
}
}
}
}
get4Write() {
aquire_writelock(update_lock); // blocks concurrent writers!
var copy_from = get4Read();
var copy_to = deep_copy(copy_from);
copy_to.readers = 0;
return copy_to;
}
release4Write(data) {
lock(current_lock) { // exclusive access to current
var copy_from = current;
current = data;
}
release4Read(copy_from);
release_writelock(update_lock); // next write can come
}
要完成有关要使用的实际数据结构的答案: 鉴于数据条目的固定大小(两个整数元组),也非常小,我将使用数组进行存储,并使用二进制搜索进行查找。 (另一种选择是评论中提到的平衡树)。
谈论绩效:据我所知,'地址'和'大小'定义了范围。因此,对这个范围内的给定地址的查找将涉及一遍又一遍地对“地址”+“大小”(用于比较所查询的地址与范围上限)的加法运算。为了避免重复添加,明确地存储开始和结束地址而不是开始地址和大小可能更有效。
答案 3 :(得分:1)
在http://symas.com/mdb/阅读LMDB设计论文。具有无锁读取和写时复制写入的MVCC B +树。读取始终为零复制,写入也可以选择为零复制。在C实现中可以轻松处理每秒数百万次读取。我相信你应该可以在你的Delphi程序中使用它而不需要修改,因为读者也没有内存分配。 (作家可能会做一些分配,但可以避免大部分分配。)
答案 4 :(得分:0)
作为旁注,这里有关于记忆障碍的好读物:Memory Barriers: a Hardware View for Software Hackers
这只是为了回答@fast的评论,评论空间不够大......
@chill:你认为有必要设置任何“记忆障碍”吗?
无处不在,您可以从两个不同的核心访问共享存储。
例如,作家来,制作数据的副本然后调用
release4Write
。在release4write
内,作者完成了作业
current = data
,用新的位置更新共享指针
数据,将旧副本的计数器减少为零,然后继续删除它。
现在,读者介入并调用get4Read
。在get4Read
内,它copy = current
。由于没有内存障碍,这恰好会读取current
的旧值。据我们所知,写入可能在删除调用后重新排序,或者current
的新值可能仍然驻留在编写器的存储队列中,或者读者可能还没有
看到并处理了相应的缓存失效请求等等......
现在,读者很乐意继续搜索该数据副本
作者正在删除或刚刚删除。糟糕!
但是,等等,还有更多! :d
如果&gt;使用propper get ..()和release ..()函数,在哪里可以看到访问已删除数据或多次删除的问题?
请参阅以下读取和写入操作的交错。
Reader Shared data Writer
====== =========== ======
current = A:0
data = get4Read()
var copy = A:0
copy.readers++;
current = A:1
return A:1
data = A:1
... do the lookup
release4Read(copy == A:1):
--copy.readers current = A:0
0 == copy.readers -> true
data = get4Write():
aquire_writelock(update_lock)
var copy_from = get4Read():
var copy = A:0
copy.readers++;
current = A:1
return A:1
copy_from == A:1
var copy_to = deep_copy(A:1);
copy_to == B:1
return B:1
data == B:1
... alter the data
release4Write(data = B:1)
var copy_from = current;
copy_form == A:1
current = B:1
current = B:1
A:1 != B:1 -> true
delete A:1
!!! release4Read(A:1) !!!
编写器访问已删除的数据,然后再次尝试删除它。双哟!