我写了一个最快的dataframe库

2021-03-14 09:40:09

在撰写本文时,冠状病毒已经在我国一年,这意味着我一直在家里坐在家里。在大流行的开始时,我在腰带下有一些宠物项目,我注意到“我们是Dataframe”,并不是在我满意的附近。所以我想知道我是否可以制定一个简约的箱子,解决了我的具体用例。但是男孩,这是脱离的。

一年后,有很多编程导致Rest和Python中可用的最快的DataFrame库之一。这是我个人博客上的波拉的第一个官方“Hello World”。通过这篇文章,我希望我能把读者赶上一些我遇到的设计决策,并更彻底了解波拉斯在引擎盖下的工作原理。

我知道它是一个粗体的主张,我不会轻视。数据库系统有一个基准测试,该系统在内存工具上进行了基准,由H2O.AI运行。该基准测试由10个GroupBy测试组成,用于不同的数据基数和查询复杂性,以提供刀具性能的圆满观察,以及对不同连接问题的5个测试。在撰写本博客时,POLARS是基准测试中最快的DataFrame库.Table,Polars是Top 3所考虑的所有工具。

下面显示了5GB数据集测试的摘要,您可以在此处看到整个基准。

如果您想设计以获得最佳性能,则无法忽略硬件。由于硬件层次结构和分支预测等硬件相关问题,存在算法复杂性的情况下,算法复杂性不会让您对实际性能的良好直觉。例如,最多数量的元素(几个100,根据数据类型),在阵列中查找比在HashMap中查找的给定元素更快,而那些数据结构的时间复杂性是$ \ mathcal {o}(n)$和$ \ mathcal {o}(1)美元分别。这使得设计决策也是非常颞的,这是这一天的瓶颈,可能不会在未来。在数据库系统中清楚地看到了这一点。来自PostgreSQL或MySQL这样的前一代的DB系统都是基于行的Volcano模型[1],这是当磁盘较慢而RAM非常有限时的一个优秀的设计决策。如今,我们有快速的SSD磁盘和大量的可用内存,以及宽的SIMD寄存器,我们看到坐标数据库等蟑螂,DuckDB是最好的DBMSE之一。

过度简化,RAM有两种口味,大而缓慢或快速且小。因此,您在层次结构中有内存缓存。你有主要的记忆,这很大又慢。您使用的内存分别存储在L1,L2,L3缓存中,分别具有更多延迟。下面的求和使您了解不同缓存级别的相对延迟:

顺序访问数据时,我们希望尽可能确保数据处于缓存中,或者我们可以轻松地具有〜100x的性能损失。缓存已加载并删除缓存行中。当我们加载单个数据点时,我们得到一个整个缓存行,但我们也删除了一个整个缓存行。它们通常为64,或128个字节长,并在64字节内存地址上对齐。

CPU预取数据和指令到本地缓存,以减少内存延迟的高损失。如果您在没有任何分支的情况下有一个紧密的循环(如果 - 然后 - 那么结构),CPU没有知道要预取的数据并可以充分利用指令管道的问题。

指令管道通过并行工作来隐藏延迟。每个CPU指令都通过获取,解码,执行,回写序列。而不是在单个管道中顺序顺序执行这条指令,而是有多个管道已经预先获取(解码,执行等)下一个指令。这增加了吞吐量并隐藏延迟。但是,如果此过程中断,则开始使用空管道,您必须等待下一个指令的完整延迟时间。下面我们看到了指令管道的视觉。

CPU确实最佳地预测,预测哪个条件跳跃并引导地提前执行该代码(即将管道填写),但如果它已经错误预测,则必须清除这项工作,我们支付延迟价格,直到管道再次支付延迟价格。

现代处理器具有SIMD寄存器(单指令多数据),其在单个CPU周期中的整个数据向量上运行。矢量通道宽度从128位变化到512位,加速度取决于寄存器的宽度和表示数据类型所需的比特数。这些寄存器大大提高了简单操作的性能,如果您可以填充足够快(线性数据)。因此,柱状存储器格式可以充分利用SIMD指令。 POLARS及其内存后端箭头,利用SIMD获得最佳性能。

波拉基于生锈本机实现Apache箭头。箭头可以被视为DBMS,查询引擎和DataFrame库的中间件软件。 arrow提供非常缓存相干的数据结构和正确缺少的数据处理。

箭头数字阵列由包含一些键入数据的数据缓冲区组成,例如, F32,U64等,如图所示为橙色彩色阵列。除了值数据之外,箭头数组alwas还有一个有效性缓冲区。此缓冲区是位数的位数,其中位指示缺失数据。因为缺失的数据由位表示有最小的内存开销。

这直接显示了Pandas的明显优势,例如,在浮动NaN和缺失数据之间没有明确区分,在那里他们真正应该代表不同的东西。

下图显示了箭头最大阵列的存储器布局。这个数字编码了以下数组[" foo&#34 ;,#34;酒吧和#34 ;,#34;火腿"]。箭头阵列由数据缓冲区组成,其中所有字符串字节都连接到单个顺序缓冲区(适用于缓存一致性!)。为了能够找到字符串值的起始和结束位置,有一个单独的偏移量阵列,最后,有空白位缓冲区以指示缺失值。

让我们将此与Pandas String阵列进行比较。 Pandas字符串实际上是Python对象,因此它们被盒装(这意味着还有内存开销来编码数据旁边的类型)。 Pandas中的顺序字符串访问将导致缓存未命中后的缓存未命中,因为每个字符串值都可能指向完全不同的内存位置。

对于缓存同时,箭头表示是一个明确的赢家。但是,有一个价格。如果我们想要过滤此数组,或者我们希望根据某些索引数组取值,我们需要复制更多的数据。 Pandas String数组仅将指针保存到数据,并且可以便宜地创建一个带指针的新数组。箭头字符串阵列必须复制所有字符串数据,尤其是当您有大字符串值时,这可能会成为一个非常大的开销。估计字符串数据缓冲区的大小也更难,因为这包括所有字符串值的长度。

Polars还具有一个分类类型,可以帮助您缓解此问题。 arrow还有一个解决这个问题的解决方案,称为字典类型,它类似于波拉'分类类型。

箭头缓冲器是参考计数和不可变的。意思是复制DataFrame,系列,数组几乎是一个无操作,使其非常容易写出纯粹的功能代码。切片操作的相同计数,这只是参考计数的增量和偏移的修改。

正如我们所见,缺失的数据在单独的缓冲区中编码。这意味着我们可以通过仅在操作期间忽略空缓冲区来轻松编写无分支代码。操作完成后,空白位缓冲区仅复制到新数组。当一个分支未命中比执行操作更昂贵时,这很容易获胜并在箭头和点中的许多操作中使用。

必须在DBMS中进行的操作是过滤器。基于一些谓词(布尔掩码),我们过滤行。箭头空白位缓冲区允许使用筛选器技巧(非正式名称为ME)非常快速过滤。 *对筛选器的抵押符号GOACHE arrow实现。注意,此筛选器诀窍通常导致更快的过滤器,但情况并不总是如此。如果您的谓词包括交替布尔值,则[true,false,true,false,......,true,false]这个技巧有轻微的开销。

筛选器诀窍的核心思想是,我们可以将位数从内存加载为我们想要的任何整数类型。假设我们将位数组加载为无符号整数U64,然后我们知道最大编码值为$ 2 ^ {64} $(二进制中64个连续一个值),最小编码值为0(连续64个连续0值二进制)。我们可以制作一个64个条目的表,表示我们可以过滤和跳过的连续值。如果此整数在此表中,我们知道在很少几个CPU周期中过滤许多值,并且可以进行MEMCPY以有效复制数据。如果表格中不存在整数,我们必须逐一迭代位,并且希望在我们加载以下64位的以下64位命中。

当然,在包括谓词掩模的任何操作中使用此滤波器技巧,例如过滤器,设置和zip。

随着CPU时钟速度的平台和摩尔法在视线中,免费午餐[2]结束了。单线程性能不会再增加了。为了缓解这一点,现在几乎所有硬件都有多个核心。我的笔记本电脑有12个逻辑核心,因此有一个巨大的并行潜力。单击选材以尽可能多地剥削并行性。

最佳的并行化当然是不需要通信,没有数据依赖性。波拉尽可能利用这种并行性。

例如,如果我们在DataFrame中的列上执行聚合,则这是这样的。可以并行聚合所有列。

散列是DataFrame库中许多操作的核心,GroupBy-Operation创建一个哈希表,其中包含组索引指针,并且加入操作需要一个哈希表,以查找映射左侧的行的组与右侧数据帧的组。

在这两个操作中,我们不能简单地拆分线程之间的数据。无法保证所有相同的密钥将在同一线程上的相同哈希表中最终结束。因此,我们需要一个额外的同步阶段,我们构建了一个新的哈希表。该原理如下图所示,2个线程。

发现太贵的另一个选项是散列单独的线程上的数据,并在互斥锁中具有单个哈希表。正如您可以想象的那样,在这种算法中,线程争用非常高,并行性并不真正支付。

代替之前提到的方法,POLARS使用锁定散列算法。这种方法确实比以前昂贵的锁定方法更多的工作,但是这项工作并行完成,保证所有线程都不必等待任何其他线程。每个线程计算键的哈希值,但根据散列的结果,它将确定该键是否属于该线程的哈希表。这只是由哈希值%线程编号确定。由于这个简单的技巧,我们知道每个线程哈希表都有唯一的键,我们可以简单地将哈希表的指针组合在主线程上。

最好的性能收益根本不是在做任何工作。 Polars由两个公共API组成,一个是渴望,程序编程的一个,以及一个是懒惰的声明性编程。我建议在处理性能关键代码时尽可能多地使用懒惰API。

声明性DSL允许POLARS分析查询的逻辑计划,它可以应用几种优化/启发式,使得您的查询可以通过较少的工作来执行。如果您想了解有关对查询计划所做的优化的更多信息,则在选项中有一个章节。

这篇文章仅突出了各个性能相关的设计,在波拉和箭头库中。实现的其他事项例如:

现在,“Hello World”正式在那里,我可能会在后来的帖子中突出那些其他的子宫。检查GitHub上的选项,如果您有任何备注,功能请求等。请告诉我!

[1] GRAEFE G。火山(1994)一个可扩展和并行查询评估系统。 IEEE Trans。知识。数据ENG。 [2] Herb Sutter(2005)免费午餐结束:在软件博客中的并发性的基本转向[3] Angela Chang:蟑螂(2019)40x速度哈希木匠与矢量化执行挂图