硬盘的性能特征

我们知道内存比硬盘要快得多,大概能快出一两个数量级(当然价钱也贵得多)。不过,硬盘的问题并不只是速度慢。

硬盘的一个基本特征是不适合做频繁小量读取。所谓频繁小量读取,就每次读取的数据量很小,但次数很多。对于内存,每次取 100 字节、取 100 万次、总共取 1 亿字节;和每次取 100 万字节、取 100 次、也取 1 亿字节;耗时是差不多的。从内存获取数据的最小单位就是字节,无论怎么做,最后涉及到的字节数是一样的。
硬盘的机制完全不同,在硬盘中数据是分块存储的,读取数据有个最小的基本单位,在操作系统中一般是 4K,也就是说从硬盘上读 1 个字节和读 4K 字节的时间一样的。这样,每次取 100 字节取 100 万次,和每次取 100 万字节取 100 次,总读取量相同,但耗时却可能有巨大差异。而且,读取硬盘是一系列复杂的动作,不象内存那样执行一条 CPU 指令就可以,读取 100 万次理论上就要执行 100 万次这些动作,当然一次读 100 万字节也会导致多次动作(100 万字节会存入多个数据块中),但总次数仍然会少得多。同样的读取量,批量少次读取的性能通常会远远好于小量多次读取。

不过,这个结论成立与否,还要考虑数据存储是否连续。如果要数据在硬盘上是连续存储的,那取 100 万次 100 字节也不会很慢,因为后面要读的数据已经在前面读出的数据块里面而不必再读,硬盘和操作系统都有缓存功能,实际硬盘读取次数并没有那么多,性能下降了不会非常严重。所以还要在 "频繁小量" 前面加上“随机”这个定语,也就是读取的内容不连续,这时候,从读出数据块中取出需要的部分外,其它内容没有用,只能浪费掉,后面再用又要重新读,性能就会陡降了。
机械硬盘还有个寻道问题,就是找到数据所在位置。寻道是个非常慢的机械动作,比读数慢得多。即使每次读出的数据块没有浪费,在随机读取时的寻道成本却可能超过读取本身。使用机械硬盘时要特别注意避免频繁的随机读取。固态硬盘的情况要好很多,它没有寻道时间了,不过频繁的随机小量读取仍然不行,硬盘的数据块实在太大了。

那么,如果计算任务只需要连续批量读取数据(比如遍历汇总),使用硬盘的性能是不是就只由其本身速度决定了呢?
对于单个的单线程任务确实是这样。但现代高性能计算不可能不考虑并行,还有许多运算服务要支持多并发。并行和并发运算会使原本可以连续读取的硬盘数据一定程度又变成随机读取,原因很简单:多线程共享同一套硬盘,不同线程的读取请求显然不会连续,硬盘要同时响应这些请求就会发生跳动,也就是随机读取了。
对于机械硬盘这个后果常常很严重,如果线程切换频繁,甚至会发生多线程比单线程更慢的奇怪现象。也有些单任务时性能尚可的场景,一旦并发了性能就会急剧下降。补救的办法是加大读数的缓冲区,每次读出的数据足够多且连续,这样能使寻道时间占比变小而感觉不明显,但会加大内存占用,线程越多对内存需求越大。
类似的场景是列式存储,数据按列存放,需要多列计算时,即使单线程也会发生硬盘随机读取现象。

由于硬盘的这个性能特征,内存和外存的运算实现会采用完全不同的算法,甚至连运算本身的定义都应该不同。
关系代数在设计时并没有涉及内外存的区别,只是笼统地定义出运算。关系数据库要实现关系代数,会发现有些运算面对外存时要比在内存中麻烦得太多。比较典型的 JOIN 运算,数据库常用的 HASH JOIN 算法要把数据遍历两次,这在内存中不要紧,但外存遍历成本通常会高于计算本身,运气不好还多次 HASH 反复遍历,性能就会陡降。但如果我们改变 JOIN 运算的定义,在仍然能满足现实业务的需求的前提下,充分考虑到外存也就是硬盘的性能特征,就可以设计出只遍历一次甚至不需要全遍历的的低复杂度算法,这样就能获得高性能了。
嗯,这就是 esProc SPL 和关系数据库的区别。