分类 读论文 下的文章

在粗读 Reservoir-based sampling over large graph streams to estimate triangle counts and node degrees 论文的时候,第一句就读到不认识的东西,真是开幕雷击= =

在详细了解后,发现蓄水池采样算法本身是一个十分简单的算法,采用巧妙的方法解决了一个如下的“简单”问题:

如何在内容和长度均未知的数据流中,仅遍历一遍,且在O(n)的时间复杂度内,任意、随机地选出m个数据来?*

* 问题相较于参考资料有所改变,将在文尾具体解释。

长度和内容均未知的数据流带来了以下问题:

  1. 不一定能够使用存储设备完全储存(进一步说的话,可以特指内存);
  2. 不能预测且无法回看。

针对于题目的任意和随机性来看的话,上述第2点带来的问题就是,必须要对当前数据流中的任意元素进行等价的操作。也就是说,决定任何一个数据去或留(即是否加入最终选出的m个元素的集合)的时机,对于任何元素来说都应该是一样的。对于这种意义下的“操作时机”,我认为可以大致分为三类:

  1. “当机立断”:获取到该数据的同时就对于数据的去向进行决定。此时,程序的内存占用量是可预测的,(按照单线程考虑的话)同一时刻内不会存有两个数据流内的数据;
  2. “前看”:类似于编译原理中的前看原理,但是需要通过在程序中自行设立单独的“前看缓冲区”,通常来说,前看的深度是固定的;
  3. “我全都要”:最让人喜闻乐见的方式,全读入内存,慢慢折腾。

显然,3是首先要排除的。题目中有提到数据流的长度是未知的,很有可能超出当前可能的内存容量。

那么问题的解决策略只能在1或者2中着手。由于本人数学水平实在有限,无法断定并证明其中某种思路的可行性,因而在此,只能说明,蓄水池采样算法是使用思路1,借助随机函数实现对于任意长度序列的所及元素选择。

接下来对于算法,使用类Java伪代码进行描述:

InputStream in;                    // 数据流
Data current;                      // 当前数据
Data ans[] = new Data[m];          // 为要选出的元素预留空间
Random rand = new Random();        // 随机数产生器
Integer index = 0;                 // 已读取的数据数量计数

while(in.hasNext()) {
    current = in.next(); index++;  // 读取了一个数据
    if(index <= m) {
        // 先把“蓄水池”填满,此时不存在元素冲突问题
        // 下标=元素个数-1
        ans[index - 1] = current;
    } else {
        // 从当前已读取的数据编号范围内选出一个,把刚读入的数据“放到”这个位置
        // 这里的randInt取值区间是左闭右开的
        Integer slot = rand.nextInt(0, index);
        if(slot < m) {
            // 若是这个位置是前m个之内的话,替换数据
            ans[slot] = current;
        }
        /* 否则不做任何处理 */
    }
}
return ans;

然后就是简单扼要的原理说明了。

先说结论:使用蓄水池采样算法,可以在仅遍历一遍数据流、且在O(n)时间复杂度的情况下,使得每个数据被选出的概率均为\frac{m}{N}

证明如下:

将“数据能够在蓄水池中保留至结束”这一事件S(Select)分解为两个互相独立、串联的事件 [数据能够进入蓄水池I(In), 数据能够保留在蓄水池中K(Keep)],则可以表示如下:

P(S)=P(I) \times P(K)

从上面的算法描述代码可以看出,第1 \sim m个元素进入蓄水池的概率为1,即

\tag{1} \forall i \in \{1, 2, ..., m\}, P(I_{i}) = 1

而第1 \sim m个元素中的任意一个要保留到最终的话,假设数据流长度为N,则应该要在接下来N-m次数据读取的操作中均不被替换掉。由于第j个数据输入时共有j种选择,则每一次替换时,蓄水池中的第i个元素有\frac{j-1}{j}的概率不被替换。由此则有第1 \sim m中任意一个元素保留到最终的概率为

\tag{2} \forall i \in \{1, 2, ..., m\}, P(K_{i}) = \frac{m}{m+1} \times \frac{m+1}{m+2} \times ... \times \frac{N-1}{N} = \frac{m}{N}

(1)(2)可得

\tag{3} \forall i \in \{1, 2, ..., m\}, P(S_{i}) = P(I_{i}) \times P(K_{i}) = \frac{m}{N}

对于第m+1 \sim N个元素,由于此时蓄水池中已经装满元素了,需要以一定概率来替换已有元素,才能决定是否进入集合。由于新元素要进入集合只需替换原有集合中的任意一个即可,则有:

\tag{4} \forall i \in \{m+1, m+2, .... N\}, P(I_{i}) = \frac{m}{i}

可以发现此时位置不同的元素进入蓄水池的几率是随着位置向后推移而逐渐降低的。

继续计算这些元素保留到最终的概率,原理同公式(2)

\tag{5} \forall i \in \{m+1, m+2, ..., N\}, P(K_{i}) = \frac{i}{i+1} \times \frac{i+1}{i+2} \times ... \times \frac{N-1}{N} = \frac{i}{N}

此时由(4)(5)可得:

\tag{6} \forall i \in \{m+1, m+2, ..., N\}, P(S_{i}) = P(I_{i}) \times P(K_{i}) = \frac{m}{N}

合并(3)(6),神奇的事情发生了:

\forall i \in \{1, 2, ..., N\}, P(S_{i}) = \frac{m}{N}

证毕。

这也正是本算法的巧妙之处:不论原始数据流有多长,只要带选出的数据量m小于原始数据流的长度N,总能有办法随机、等概率地选出m个元素,即使N完全未知。

原文来自 https://www.jianshu.com/p/7a9ea6ece2af ,作者:邱simple。

在原文中,作者还讲述了分布式蓄水池抽样算法,并给出了Java代码的实现,如有需要可自行浏览。

这里简要说明一下开头提到的与原文中问题的区别。原文中问题还添加了“选出的元素不重复”这一限制条件,但是我认为,添加了该条件后,代码需要做出相应的改动(替换元素时的一些if条件),也不能称之为对原始数据流的等概率选取了,因而在本文中对命题进行了修改。如果有细心的读者发现了我的逻辑问题的话(万一有呢指读者),欢迎指正与交流。

https://dblp.org/rec/conf/cluster/ParkS09

该论文属于实验报告类型,主要内容是对于科研程序这种磁盘读写密集型的系统在SSD上的实际性能进行了一个定量的测试。 论文的发表时间为2009年,也就是实验进行的时间理论上不会晚于2009年。在后来查其他几篇论文的时候,发现作者之一的Stan Park是HP实验室的。

在该文章中,作者使用了三种不同技术的存储介质(两种类型,机械硬盘和半导体闪存),分别是代表机械硬盘的西数鱼子酱WD Caviar SE,SLC的Mtron Pro 7500和MLC的Intel X25-M。MLC里的M指的是Multiple,即相对于(当时的)传统SLC来说,使用一个存储单元存储多位而不是一位二进制数据。(但是就当时(2009年)的科技水平而言,当时最多应该是两位了吧,尤其是这还是企业级的;到了2018/19年TLC才在消费领域普及)要不重新起个名叫BLC算了

然后作者使用了来自Los Alamos和Sandia两个美国国家实验室的I/O数据记录(然而地址都已经打不开了= =),在自己的平台上重放以进行性能对比。当然,除了存储介质不一样(还有存储控制器?),其他变量都是控制了的。结果表明,SSD的性能比机械硬盘提升并不大,西数HDD甚至还在某些场景下性能超过了Mtron的SLC。但是,I/O效率本应低于SLC的Intel MLC却在性能上远远超出了Mtron SLC。根据作者分析,原因如下:

  1. 根据当前NAND闪存芯片的组织方式和操作原理来看,读取和写入的规则是不一致的。虽然读取和写入都是按照文件系统的来进行的,但是对于NAND闪存芯片的写入必须按照的方式来进行。分别是从逻辑和物理的角度上定义的:页的大小由文件系统决定,作为从该逻辑分区中一次存取的元数据量大小(即逻辑I/O的最小单位);块的大小由闪存芯片的固件决定,作为一次物理上数据记录的元数据量大小。仔细思考会发现这两个参数对应的系统效能变化函数(假想)并不会是单调的:过小的页/块会显著地增大地址的数量,过长的地址编码不利于提升数据的有效传输率;过大的页/块则会因为物理传输速率的有限,增大读写时延,该缺陷会在大量的随机读写中明显地表现出来。至少就目前来看,可能是根据计算机行业的大量测试得出的较优结果,页的大小是略小于块的,该比例不会超过二进制的十个数量级。因而可见,最大达到2^10倍的数据量差异,时延差距会有多大。。

  2. SSD连接到计算机的方法是分层的:闪存芯片-Flash Translation Layer(FTL)-计算机外围I/O。因而,FTL在其中扮演了一个很关键的角色,上述提到的块操作就是由它来完成的。顾名思义,闪存翻译(映射)层负责提供计算机能够读取使用的逻辑存储结构,一方面负责操作闪存,一方面与计算机交换存储元信息和数据。作者认为,正是这样的分层结构,成为了闪存I/O优化的一个阻碍:FTL不知道自己被要求读写的数据的语义,只能机械地按照指令存取,同时进行固件中设定的垃圾回收策略;计算机不了解数据的真实存储布局,只能机械地按照程序流程将指令近乎顺序地(系统中可能有I/O调度方案,对I/O请求进行一定程度的重排)发送给FTL。有成语云“因地制宜”,我猜测作者在此同样认为,能够了解实际情况的话,能够使用某些算法对I/O性能有更进一步的提高。(此外私以为,在计算机和FTL尚无法直接沟通的当下,FTL(主控?)的I/O算法同样对性能有很大的影响。)文末提到了不使用文件系统日志来维护当前逻辑分区的文件列表,我就很好奇,不用日志咋办?查一个文件扫一次盘吗??

  3. 实验中的Intel MLC性能超过Mtron SLC的原因很简单,根据作者的解释,因为Mtron Pro 7500是四通道总线,Intel X25-M是十通道总线。看来这时候还不是很擅长挤牙膏啊