PyTables处理内存大小几倍的数据

10 投票
3 回答
2418 浏览
提问于 2025-04-17 16:31

我正在尝试理解PyTables是如何处理比内存大得多的数据的。这里是PyTables代码中的一段注释(GitHub链接):

# Nodes referenced by a variable are kept in `_aliveNodes`.
# When they are no longer referenced, they move themselves
# to `_deadNodes`, where they are kept until they are referenced again
# or they are preempted from it by other unreferenced nodes.

_getNode方法中也可以找到一些有用的注释。
看起来PyTables有一个非常聪明的输入输出缓冲系统。根据我的理解,它将用户引用的数据存储在快速的内存中,称为“活节点”(aliveNodes),而将之前引用过但现在不再引用的数据称为“死节点”(deadNodes),这样可以在需要时快速“复活”这些数据。如果请求的数据在活节点和死节点中都找不到,它就会从磁盘读取数据。

我需要一些专业知识,了解PyTables在处理比可用内存大的数据时具体是如何运作的。我的具体问题有:

  1. 死节点和活节点系统是如何工作的(整体情况)?
  2. 活节点和死节点之间的主要区别是什么?如果我没理解错的话,它们都代表存储在内存中的数据。
  3. 缓冲的内存限制可以手动调整吗?在注释下方,有一段代码读取了params['NODE_CACHE_SLOTS']的值。这可以由用户指定吗?比如说,如果我想留一些内存给其他需要内存的应用程序?
  4. 在处理大量数据时,PyTables可能会崩溃或显著减慢的情况有哪些?在我的情况下,数据可能会超过内存100倍,这种情况下常见的陷阱是什么?
  5. 在使用PyTables时,数据的大小、结构以及数据操作被认为是“正确”的,以达到最佳性能的标准是什么?
  6. 文档建议在每个基本的.append()循环后使用.flush()。这个循环实际上可以持续多长时间?我正在进行一个小的基准测试,比较SQLite和PyTables在处理从大型CSV文件创建巨大的键值对表时的表现。当我在主循环中不那么频繁地使用.flush()时,PyTables的速度提升非常明显。那么,是否正确地将相对较大的数据块.append()后再使用.flush()

3 个回答

0

我也不是PyTable方面的专家,Simon已经很好地解释了交换内存的概念,但如果你想要一个具体的例子,说明如何处理那些太大而无法放进内存的数据,我建议你看看外部排序。

基本的思路是这样的:你不能把所有的数据都放进内存,但你需要对它们进行排序。不过,你可以把一些数据分成大小为k的块放进内存。假设有j个这样的块。

  • 把数据分成大小为k的块。
  • 对每个块,先把它放进内存,然后进行排序(比如用快速排序或其他方法),然后把排序好的结果写回硬盘。

现在,我们有j个已经排序好的数据块,接下来我们想把它们合并成一个长长的排序数据。这听起来就像是归并排序!所以,

  • 把每个已经排序好的j个块中最小的值放进内存。
  • 找出这j个值中最小的那个。那就是当前最小的数据!把它写到硬盘上,作为我们排序数据集的开始。
  • 用它所在块中的下一个最小值替换掉刚写入的值(这就是交换内存中的“交换”部分)。

现在,内存中的数据是最小的j个值,除了我们已经写入最终排序数据集的那个。所以,如果我们重复这个过程,直到所有数据都写入最终的数据集中,它就会一直保持排序状态。

这只是一个使用内存交换来处理太大而无法放进内存的数据的算法示例。PyTable的排序方法可能也是类似的。

额外信息:这里一些链接可以了解更多关于外部排序的解释。

1

我对PyTable不是很懂,但它的工作原理可能和交换内存差不多。

aliveNodes(活节点)存放在内存(RAM)里,而deadNodes(死节点)可能存储在硬盘上的hdf5文件中(这是PyTables使用的一种二进制文件格式)。每次你需要访问某个数据时,它必须在内存中。所以PyTable会先检查这个数据是否已经在内存里(也就是aliveNodes),如果在,就直接给你。如果不在,就需要“复活”那个存放数据的deadNode。由于内存是有限的,它可能会先“杀掉”(写入硬盘)一个不再使用的aliveNode,以腾出空间。

这样做的原因当然是因为内存的大小有限。每次你需要交换节点(“杀掉”一个节点并“复活”另一个)时,性能都会受到影响。

为了优化性能,你应该尽量减少交换的次数。例如,如果你的数据可以并行处理,你可能只需要加载每个节点一次。再比如,假设你需要遍历一个巨大的矩阵,而这个矩阵被分成了多个节点。那你最好不要按行或按列访问它的元素,而是一个节点一个节点地访问。

当然,PyTable在后台处理这些事情,所以你不一定能控制每个节点里有什么(但我建议你了解一下NODE_CACHE_SLOTS这个变量,至少能让你明白它是怎么工作的)。一般来说,访问连续的数据会比访问分散的数据要快。如果时间性能对你的应用很重要,记得对你的代码进行性能分析。


1 翻译:我对PyTables几乎一无所知

2

内存结构

我之前没用过pytables,但看了一下源代码:

class _Deadnodes(lrucacheExtension.NodeCache):
    pass

看起来,_deadnodes是用LRU缓存实现的。LRU就是“最近最少使用”的意思,也就是说,它会优先丢弃使用最少的节点。源代码在这里

class _AliveNodes(dict):
    ...

他们把这个用作一个定制的字典,里面存放的是程序中实际运行的节点。

举个简单的例子(节点是字母,缓存中的数字表示一个条目的陈旧程度):

memory of 4, takes 1 time step
cache with size 2, takes 5 times steps
disk with much much more, takes 50 time steps

get node A //memory,cache miss load from disk t=50
get node B // "" t=100
get node C // "" t=150
get node D // "" t=200
get node E // "" t=250
get node A //cache hit load from cache t=255
get node F //memory, cache miss load from disk t=305
get node G //memory, cache miss load from disk t=355
get node E // in memory t=356 (everything stays the same)

t=200              t=250              t=255
Memory    CACHE    Memory    CACHE    Memory    CACHE
A                  E         A0       E         B0
B                  B                  A
C                  C                  C
D                  D                  D

t=305              t=355              
Memory    CACHE    Memory    CACHE
E         B1       E         G0
A         C0       A         C1
F                  F
D                  G

在现实生活中,这些结构非常庞大,访问它们的时间是以总线周期为单位的,也就是1/(你电脑的时钟频率)。

相比之下,访问这些元素所需的时间是相同的。对于内存来说,这个时间几乎可以忽略不计,缓存稍微多一点,而磁盘则要多得多。从磁盘读取数据是整个过程最耗时的部分,因为磁盘和机械臂需要移动等。这是一个物理过程,而不是电子过程,也就是说,它不是以光速进行的。

在pytables中,他们做了类似的事情。他们用Cython编写了自己的缓存算法,作为活跃节点(内存)和完整数据(磁盘)之间的中介。如果命中率太低,缓存就会被关闭,经过一定的周期后又会重新开启。

parameters.py中,DISABLE_EVERY_CYCLEENABLE EVERY_CYCLELOWEST_HIT_RATIO这几个变量用来定义在LOWEST_HIT_RATIO下禁用的周期数,以及重新启用前需要等待的周期数。改变这些值是不推荐的。

你需要记住的主要一点是,如果你需要处理一个大数据集,确保它们在同一个节点上。如果可以的话,先读取一块数据,处理这块数据,得到结果后再加载另一块。如果你先加载A块,再加载B块,然后再加载A块,这样会造成最大的延迟。一次只处理一块数据,尽量减少访问和写入。一旦一个值在_alivenodes中,修改它会很快,而_deadnodes会慢一点,但都不会太慢。

NODE_CACHE_SLOTS

params['NODE_CACHE_SLOTS']定义了死节点的集合大小。追溯到parameters.py,默认值是64。它说明你可以尝试不同的值并反馈结果。你可以在文件中更改这个值,或者这样做:

import parameters
parameters.NODE_CACHE_SLOTS = # something else

这只是限制了缓存中保留的节点数量。超过这个数量,你就受限于Python的堆大小,想要设置这个可以查看这个链接

追加/刷新

对于appendflush确保行数据被输出到表中。你移动的数据越多,从内部缓冲区到数据结构的移动时间就越长。它调用的是一个修改过的H5TBwrite_records函数,并带有其他处理代码。我猜测调用这个函数的时间长度决定了输出周期的长短。

请记住,这些都是来自源代码的内容,没有考虑他们可能尝试的其他特殊处理。我自己从未使用过pytables。从理论上讲,它不应该崩溃,但我们并不生活在一个理论的世界里。

编辑:

实际上,我自己发现需要使用pytables时,遇到了这个问题,可能会解答你的一些疑虑。

谢谢你让我了解pytables,如果我在研究这个问题之前遇到过.h5文件,我可能不知道该怎么处理。

撰写回答