首页 > 系统相关 >只有 2G 内存, 如何排序 128G 数据?

只有 2G 内存, 如何排序 128G 数据?

时间:2024-02-27 17:36:27浏览次数:22  
标签:归并 data 128G 2G 内存 临时文件 排序 数据

【外排序】只有 2G 内存, 如何排序 128G 数据?

加内存

在说明这个问题之前, 先来回顾一项计组基础知识: CPU最多可以访问到内存, 无法访问磁盘; 因此一般的排序算法(如快速/归并/堆/希尔)都只能对至少可存储在内存中的数据进行排序, 但现在需要面对的场景是数据量过大, 无法放入内存, 因此无法直接使用一般排序方法, 这时就需要使用外排序算法

外排序的初步思路

现在将问题缩小为, 假设有8G大小的数据(且假设这些数据都是整数)待排序, 而内存中只能放下2G数据, 那么最自然的思路当然是:

第一步, 每次读取2G大小的内容, 然后使用快排对这2G的内容进行排序, 再输出到临时文件中, 这样, 就拥有了4个有序的临时文件

预处理

第二步, 查看指向这四个临时文件的文件指针, 假设第i个(1<=i<=4)指针指向的数据大小最小, 则将第i个指针指向的数据输出到结果中, 并位移第i个指针, 使其指向文件内的下一项数据

第三步, 如果4个文件指针都指向文件末尾, 则结束算法, 否则再次执行第二步

这种思路将整个外排序的过程分为了2个阶段:

  1. 预处理: 将大量无序内容分割成N个内部有序的小块

  2. 归并: 利用归并算法的思维, 对上一阶段产生的小块进行N路归并

缺陷

  • 内存利用率不足: 快速排序的空间复杂度为$O(n)$, 这意味着如要排序2G大小的数据, 则要额外提供大约2G的内存空间, 实际存储数据的内存空间仅占全部空间的一半不到

  • CPU空置: 可以看到, 预处理过程中, 必须要首先读够2G大小的数据, 再进行排序, 但读取内容为io密集型任务, 而排序为CPU密集型任务, 两者应该并发进行, 才能最大化利用计算机资源

  • 归并比较次数多: 假设每个临时文件的长度为m, 则归并算法的时间复杂度为$O(m*N)$, 耗时较多

改进

由于初步思路存在一定缺陷, 因此需要针对上述问题, 进行改进以提升效率:

对于预处理, 引入小根堆来对数据进行排序

对于归并过程, 引入竞赛树最佳合并树来提升归并效率

预处理

小根堆

关于小根堆, 详细请学习"数据结构"课程

选择置换

假设内存空间最多可容纳s条数据, 获取待排序数据由另一线程RT执行, 则选择置换排序步骤如下:

  1. RT通信, 获取s条数据, 利用这s条数据建立小根堆data

  2. 创建与data相关的新临时文件data file, 同时建立临时堆temp

  3. 输出data的堆顶数据x, 从RT获取一条待排序数据y; 如果$y<x$, 则y放入data, 否则, 将y放入temp

  4. 如果data为空(此时temp可能存储了s条数据), 则temp变为data, 跳转到第二步继续执行

  5. 当不再能从RT处获取任何数据时, 输出内存中剩余的数据并结束预处理流程

注意:

  1. 第三步中, 之所以要将y与刚输出到data file的x进行比较, 是因为如果y在x之后输出到data file, 且y<x, 则data file内就会出现乱序的情况, 因此y必须要输出到其他临时文件中

  2. 可以等到temp内存储的数据数量达到$s/2$之后, 再对temp进行堆的初始化操作

有此可见, 外排序的预处理流程就是执行选择置换算法的流程

最佳合并树

假设, 经过预处理, 共生成三个临时文件A, B, C, 它们所包含的数据量如下, 现在要将它们进行2路归并, 且希望尽量减少从磁盘上读取的操作:

文件名 数据量
A 89
B 24
C 34

假设现在进行2路归并, 如果先将A和C归并为D(数据量为89+34=123), 则需要执行读写操作123次, 再将D与B归并为最终文件, 则需要执行读写操作123+24=147次, 因此, 一共需要执行读写操作123+147=270次

但是, 同样是2路归并, 如果先归并B和C为D, 则需执行读操作24+34=58次, 再归并A和D, 则需要执行读操作58+89=147次, 一共读写58+147=205次

将两种归并方法用树的形式表示出来, 其中, 叶节点权重值为临时文件数据量, 可以发现, 读写次数完全取决于合并树的加权路径长度, 因此, 减少读写次数的目标就转化为求得加权路径最短的合并树

加权路径最短的合并树被称为最佳合并树

最佳合并树的构造方法与哈夫曼树类似, 区别仅在于最佳合并树为K叉树(K根据合并路数而变, 不一定为2)

归并

决定好合并次序后, 就可以开始执行归并操作

竞赛树

在这一步, 需要引入竞赛树, 竞赛树分为胜方树和败方树, 这里仅演示胜方树:

如图所示, 以5路归并为例, 从上一步选择出的临时文件中, 分别读取一条数据, 作为竞赛树的叶子节点, 使它们两两PK(比谁的值小), 胜方进入PK树的上一层, 循环执行此步骤, 直到选出值最小的数据

流程

归并操作的流程如下:

  1. 读取第一批K个数据, 利用它们构建竞赛树

  2. 从竞赛树中读取最终的胜者, 并确定它来自第i(1<=i<=K)份临时文件

  3. 将胜者输出, 并从第i份临时文件中读取下一条数据, 然后调整竞赛树

  4. 重复执行2, 3步, 直到所有数据均被输出

注意:

  1. 与预处理流程同理, 文件读取操作与比较操作可以并发进行

  2. 归并操作需要反复执行, 直到所有临时文件合成为最终文件

代码实现

我的Go.lang实现

标签:归并,data,128G,2G,内存,临时文件,排序,数据
From: https://www.cnblogs.com/lietoast/p/18037344

相关文章

  • 2024-02-27-物联网系统编程(7- 共享内存)
    7.共享内存7.1共享内存概述​共享内存允许两个或者多个进程共享给定的区域共享内存的特点共享内存是进程间共享数据的一种最快的方法;一个进程向共享的内存区域写入了数据,共享这个内存区域的所有进程就可以立刻看到其中的内容。使用共享内存要注意的是多个进程之......
  • C++内存管理
    关于C++内存和分配的学习笔记C++内存和分配很容易出问题,为了编写高质量的CPP代码,我们必须了解幕后的工作原理。1.内存泄漏例如:voidleaky(){newint;//这里就是内存泄漏cout<<"我泄漏了一个int的内存!"<<endl;}自由存储区中的数据库无法被栈或者间接访问,这块内存被......
  • 第五章 内存和磁盘
    计算机中主要的存储不仅是内存和磁盘。首先要知道的是,不读入内存就无法运行,程序保存在储存设备中,通过有序的被督促的实现运行,这一机制称为存储程序方式。总之,存储在磁盘中的程序需要读入到内存后才能运行。磁盘缓存指的是把从磁盘中读出的数据存储到内存空间中的方式。磁盘缓存加......
  • 第四章 熟练使用内存
    内存的物理机制很简单,内存实际上是一种名为内存IC的电子元件,虽然有多种形式,但从外部来看其基本机制都是一样的。内存的逻辑模型是楼房。在这个楼房中,一层可以储存1个字节的数据,楼层号表示的就是地址。虽然内存的实体是内存IC,但可以把它假想成每层都储存着数据的楼房。编程语言中......
  • nvmet_rdma_offload_mem_start 预留内存
    要在modprobenvmet_rdma时找到合法的nvmet_rdma_offload_mem_start和nvmet_rdma_offload_mem_size_mb参数,你需要进行一系列步骤以确保选定的内存区域既未被系统其他部分使用,又适合于你的NVMeoverFabrics(NVMe-oF)RDMA目标配置。以下是一个示例流程,展示如何进行这一过......
  • 熟练使用有棱角的内存
    熟练使用有棱有角的内存这一章深入浅出地探讨计算机内存管理,不仅解释了内存的工作原理,还通过实例和案例分析,帮助读者更好地理解如何优化内存使用,提高程序的性能。本章给我留下了深刻的印象。它以生动的语言和丰富的实例,将复杂的技术概念变得容易理解。作者在书中强调了内存在编程......
  • 《程序是怎样跑起来的》第四章“熟练使用有棱有角的内存”
    熟练使用有棱有角的内存这一章深入浅出地探讨计算机内存管理,不仅解释了内存的工作原理,还通过实例和案例分析,帮助读者更好地理解如何优化内存使用,提高程序的性能。本章给我留下了深刻的印象。它以生动的语言和丰富的实例,将复杂的技术概念变得容易理解。作者在书中强调了内存在......
  • python 内存优化
    Python 内存管理层次:众所周知,计算机硬件资源由操作系统负责管理,内存资源也不例外。应用程序通过 系统调用 向操作系统申请内存,而 C 库函数则进一步将系统调用封装成通用的 内存分配器 ,并提供了 malloc 系列函数。C 库函数实现的通用目的内存管理器是一个重要......
  • go堆内存分配
    mutator申请内存是以应用视角来看问题,我需要的是某一个struct,某一个slice对应的内存,这与从操作系统中获取内存的接口(比如mmap)之间还有一个鸿沟。需要由allocator进行映射与转换,将以“块”来看待的内存与以“对象”来看待的内存进行映射。在现代CPU上,我们还要考虑内存分配......
  • volatile及内存屏障理解总结
    volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些未知的因素更改。volatile提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存......