为什么file_put_contents在这个基准测试中表现不佳?

3
我为自己的一部分代码创建了一个简单的基准测试,因为我担心它无法正常工作。但是我得到了非常奇怪的结果。请看这个基准测试:
基准测试
测试文件

基准测试代码如下:

$start = microtime(true)*1000;

//code
$log=file_get_contents('test.txt').'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'."\n";
file_put_contents('test.txt', $log, LOCK_EX);

$end=microtime(true)*1000;
$time = $end-$start;
echo 'Time : '.(int)$time.'ms<br />';


$start = microtime(true)*1000;

//code
$log='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'."\n";
file_put_contents('test.txt', $log, LOCK_EX|FILE_APPEND);

$end=microtime(true)*1000;
$time = $end-$start;
echo 'Time : '.(int)$time.'ms<br />';

我注意到的是,追加选项应该更快,但实际上更慢。如果问题出在我的基准测试上,请告诉我。
有人能解释一下为什么会更慢吗?
此外,我发现当你不断按F5刷新时,文本文件会被清除。为什么会这样?
编辑:如Ilya Bursov所说,我已将基准测试更改为100次迭代。现在,追加操作似乎只需要很少的时间即可完成,而读写操作则需要很长时间。然而,即使清除缓存,单个迭代仍然会产生奇怪的结果。我知道这可能受到许多因素的影响,甚至可能受到误差边界的影响,但希望能得到详细的答案。

清除了是什么意思? - hendr1x
文件变为空。 - Dharman
好像没有人回答你。我不知道你问题的答案,但我唯一能看到导致差异的原因是长脚本在开始写入之前必须将指针设置到文件末尾。 - hendr1x
1
文件丢失可能只是因为打开/关闭/保存重叠而导致冲突。不确定是否可以在错误日志中找到它。基本上,我不会建议使用文本文件来存储将以快速速率写入的数据。 - hendr1x
1
当我进行单次迭代时,我没有看到“奇怪的结果”。我使用了你发布的完全相同的代码,test.txt是一个50MB的文件,由/dev/urandom生成。读取+连接+写入大约需要500毫秒,附加模式下的file_put_contents大约需要50毫秒。我唯一能想到的是,在你的系统上,也许缓冲区仍在向文件刷新,当附加模式下的file_put_contents开始时,它必须等待前一个file_put_contents实际完成写入。(顺便说一句,你没有考虑这一点在你的基准测试中) - ilias
@ilias,你能详细解释一下PHP中缓冲区是如何工作的吗?它是在后台运行的,下一个操作必须等待它完成吗?我该如何在我的基准测试中避免这种情况? - Dharman
2个回答

4

实际上,您正在错误的环境中使用错误的方法测量错误的项目

  1. 对于第一个结果,您正在测量大数据的读取+写入
  2. 对于第二个结果,您正在测量小数据的变量初始化+追加
  3. 您只进行了1次迭代,在多线程环境下会得到非常不稳定的结果
  4. 您通过apache调用脚本,为了进行基准测试,最好从命令行调用它
  5. 磁盘/系统缓存缓冲区的不同设置确实可以改变结果

以下是我猜测时间如此不同的原因:

由于您只有1次进程迭代,因此可能出现以下情况:

  1. 读取文件(它在缓存中)- 我们可以认为这里是0时间
  2. 将文件与const连接 - 几乎为0时间
  3. 将数据写入输出缓冲区 - 不是写入磁盘,而是写入内存缓冲区,稍后将被刷新 - 几乎为0时间
  4. 现在您正在重新初始化变量 - 我希望是0时间,或者如果您正在重复使用变量,则可能较长,因为php可能在内存中重新排列一些内容
  5. 您正在写入文件,但您必须等待p3缓冲区实际刷新

您可以尝试稍微更改一下代码:

$start = microtime(true)*1000;
$log=file_get_contents('test.txt').'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'."\n";
file_put_contents('test.txt', $log, LOCK_EX);
clearstatcache(); // NOTE call here
$end=microtime(true)*1000;
$time = $end-$start;
echo 'Time : '.(int)$time.'ms<br />';
$start = microtime(true)*1000;
$log='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'."\n";
file_put_contents('test.txt', $log, LOCK_EX|FILE_APPEND);
clearstatcache(); // NOTE another call here
$end=microtime(true)*1000;
$time = $end-$start;
echo 'Time : '.(int)$time.'ms<br />';

现在你得到了不同的结果

因此

测试中的时间1:从缓存中读取和写入

时间2:实际写入磁盘,可能还有一些内存操作的开销

考虑以下代码:

$appendString = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
$log=file_get_contents('test.txt').$appendString."\n";
$iterations = 1000;

$start = microtime(true);
for ($i=0; $i<$iterations; $i++)
    file_put_contents('test.txt', $log, LOCK_EX);
$end=microtime(true);

$time = ($end*1000 - $start*1000) / $iterations;
echo 'Time : '.$time.'ms'. "\n";

$log=$appendString."\n";

$start = microtime(true);
for ($i=0; $i<$iterations; $i++)
    file_put_contents('test.txt', $log, LOCK_EX|FILE_APPEND);
$end=microtime(true);

$time = ($end*1000 - $start * 1000) / $iterations;
echo 'Time : '.$time.'ms' . "\n";

on windows:

Time : 0.22101293945313ms
Time : 0.039001953125ms

PHP 5.5.3 (cli) (built: Aug 20 2013 16:45:40)
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2013 Zend Technologies
    with Zend OPcache v7.0.3-dev, Copyright (c) 1999-2013, by Zend Technologies

在Linux上:

Time : 7.6823303222656ms
Time : 0.008222900390625ms

PHP 5.4.4-14+deb7u4 (cli) (built: Aug 23 2013 14:37:41)
Copyright (c) 1997-2012 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2012 Zend Technologies

这些数字看起来很合理,即使有两个不同的系统,因为在第一次循环中我们需要编写大字符串,在第二次循环中只需附加小部分,并且我们使用迭代来计算平均结果。
结论:正如预期的那样-将内容附加到文件比重写整个文件更快,至少是因为需要较少的磁盘IO量。

答案很好,但不够。您只比较了写操作而没有比较整个追加操作。问题在于哪一个更快、更好用。您在CLI中进行基准测试是正确的,但这不会给您真实的结果。用户将通过 Apache 访问站点,这才是我所担心的。 - Dharman
@Dharman 你说的"whole append"和"write"是什么意思?第一个测试是测量整个文件的重写,第二个测试只是将内容追加到该文件中。用户当然会使用Apache,并且你需要模拟同时访问,但不应通过浏览器和刷新来完成。 - Iłya Bursov
要追加内容,您需要先读取、连接再写入。在读取和连接后启动计时器。 - Dharman
是的,我知道这会使第一种方法变得更慢。只需在您的答案中添加一个结论,总结您发现了什么以及哪种方法更好使用。 - Dharman

4

Php的fopen函数调用open系统调用时会带上O_TRUNC标志,这意味着在非追加模式下写入文件之前会先截短文件。这个截短操作需要时间,所以写入速度会变慢。

在截短完成和真正的内容到达之间,你会看到一个空文件。

使用命令行中的strace可以验证这一点。


该文件似乎不是空的,实际上是空的。我猜测这是由于文件上的死锁引起的。 - Dharman
我的意思是写入进程将数据发送到文件,但在文件未关闭时,其他进程无法读取此数据,因为数据仅驻留在写缓冲区/高速缓存中。 - Lajos Veres

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接