前段时间做了个raytracing的程序,过程是从两个文件分别读入所有的ray和triangle,通过计算把所有的交点输出到一个文件。这里不打算讨论计算过程中算法的优化,主要是谈谈关于文件操作的一些想法。
一开始用的是C++标准库提供的ifstream和ofstream, 读写操作类似于:
ifstream input; input >> x >> y >> z; ... ofstream output; output << x << y << z;
结果发现这样的文件读写相当慢,比如说,在我双核2.16GHz,7200rpm的机子上,写2M的数据花了将近1s。
多个文件操作的并行
第一个尝试是将文件操作并行化,在我双核的机子上,我用两个线程并行的来读ray和triangle文件。但结果发现,这比串行来读这两个文件要多花至少5倍以上的时间。一番研究,发现应该是这两个原因引起的:
- ifstream类的实现中用了很多lock,当多个线程同时执行到这些带锁的代码时,很多时间花在了相互等待中。这里有些不错的讨论。
- 在单个硬盘的系统中,并行的磁盘操作会让磁头来回seek疲于奔命,从而是大大的降低的性能,而不是想象中的提升。
所以,在不是SSD或者RAID0的系统上,让多个文件操作并行不是件明智的事情。
将文件一次性读入或写出
第二个尝试是注意到1s钟才写2M不合常理,在这样的硬件上应该说1s钟能写100M左右才对。分析后发现,其实ofstream在用这种方式写数据的时候,还存在一个字符串格式化的操作:将x,y,z格式化成字符串,再写到文件,这存在两个问题:
- "格式化-写文件"这两种操作的频繁切换。
- 多次写文件,每次只写一点点。
于是换了个方法,先将所有的数据格式化成字符串放在一个buff中,然后一次性写入文件,速度果然提高不少,写2M的数据减少到了24ms。其实,这还有另外一个好处 - 提取出来的数据格式化的操作可以并行化了。当然,这对读文件也是一样的道理,我们只需先把所有的文件内容读入内容,然后并行的分析数据。
文件操作与计算的并行
我们可以注意到,第二个尝试中,当我在读文件或者写文件的时候,哪怕机器有8个核,此时也会有7个闲在那。理想的状况当然是谁都别闲着,让文件读写与计算并行:
- 对于读,先读入一部分数据,然后另起线程开始计算,边读边算;
- 对于写,先计算出一部分数据,然后另起一个线程开始写,遍算边写;
但这里有两点需要注意,一是需要一部分"启动资金"(先读入一部分数据),此时文件操作与计算还未开始并行,这应该是无法避免的;二是过程中可能会有等待,要计算或者要写出的数据没跟上,这通过合理的安排应该可以解决。
但是,并不是所有的情况都适用于文件操作与计算并行的。假设机子有n个核,计算所需的总时间为x,文件操作的总时间为y,从理论上来讲,可以推出一个公式来表示什么时候适合并行而什么时候适合串行:
并行所需总时间:y 或者 x/(n-1)
串行所需总时间:x/n + y
我们只要比较并行和串行,哪种情况下总时间比较少即可,这里分两种情况来讨论:
- 如果y>=x/(n-1), 那么并行所需的总时间是y,与串行所需的x/n+y相比,很明显是要小(y<x/n+y),所以此时(x<=(n-1)y)并行是最佳选择。
- 如果y<x/(n-1) ,那么并行所需的总时间为x/(n-1)。与串行相比,化解这个等式:
x/(n-1) == x/n + y
所以当x>=n(n-1)y时,串行是最佳选择;x<n(n-1)y时,并行是最佳选择。
nx == (n-1)x + n(n-1)y
x == n(n-1)y
综合以上1,2,可以得出下表:
情况 | 串/并行 | 时间 |
x<=(n-1)y | 并行 | y |
x<n(n-1)y | 并行 | x/(n-1) |
x>=n(n-1)y | 串行 | x/n + y |
可以看出,n(n-1)y是个分界点,如果x比她小,就是并行;如果比它大,那就是串行。
假设在一个8核的机子上,计算时间需要10s,写文件需要1s,因为10 < 8*(8-1)*1,所以需要并行。我们来验证一下:并行所需的时间为10/7=1.43s;串行所需的时间为10/8+1=2.25s,没错;
假设在一个8核的机子上,计算时间需要100s,写文件需要1s,因为100 > 8*(8-1)*1,所以需要串行。验证一下:并行所需的时间为100/7=14.3s;串行所需的时间为100/8+1=13.5s,也没问题。
其实,说白了就是看单独开一个核来做文件操作值不值得,如果计算量很大而文件操作量很小,那就很不值得了。