提高本地服务器上PHP的性能

9
我有一个XAMPP安装,基本上是默认配置。
通常情况下,性能不是什么问题,因为我主要使用PHP来运行网页和小型Web应用程序。等待几秒钟的页面不是不寻常的。
然而,最近我开始尝试解决Project Euler的问题,并决定用PHP来解决它们。
无论如何,我都无法使我的代码在1分1秒之内运行(从将近3分钟优化),我感到非常尴尬,特别是考虑到Pjt Euler的大多数帖子报告的时间为1-3秒。(#7,找到第10001个质数)
我将我的代码移植到C#中,完成同样的任务只需要0.4秒。相同的算法,代码中唯一显着的区别是我在C#中使用了List来替换我在PHP中使用的数组。
虽然我确实希望C#能够胜过php,但这种差异让我怀疑存在严重的配置问题,但我不知道该去哪里找。
这种低性能的原因可能是什么?
编辑:以下是代码: 在PHP中:
/*
  * Project Euler #7:
  * By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, we can see that the 6th prime is 13.
  * What is the 10001st prime number?
  */

ini_set('max_execution_time', 300);  
echo "start time:" . date("i:s:u") . "<br />";
function isPrime($number, $prevPrimes)
{       
    foreach ($prevPrimes as $key =>$prime)
    {
        if ($prime == 1)
        {
            continue;
        }
        elseif ($number % $prime == 0)
        {
            return 0;
        }
    }
    // If we get to here, $number is prime
    return $number; 
}
$primes = array();
$i = 0;
$nbPrimes = 0;
while ($nbPrimes <10001)
{
    $i++;
    if ($i % 2 != 0)
    {
        $result = isPrime($i, $primes);

        if ($result != 0)
        {
            $primes[] = $i;
            $nbPrimes++;
        }
    }
}
echo "#$nbPrimes: $result<br>";

echo "End time:" . date("i:s:u") . "<br />";

在C#中:

public static void RunSnippet()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();

    List<int> primes = new List<int>();
    int i = 0;
    int nbPrimes = 0;
    int result =0;
    while (nbPrimes <10001)
    {
        i++;
        if (i % 2 != 0)
        {
            result = isPrime(i, primes);

            if (result != 0)
            {
                primes.Add(i);
                nbPrimes++;
            }
        }
    }
    stopwatch.Stop();
    Console.WriteLine("Time elapsed: {0}",
    stopwatch.Elapsed);
    Console.WriteLine ("#" + nbPrimes + ": " + result.ToString());
}
public static int isPrime(int number, List<int> prevPrimes)
{
    foreach (int prime in prevPrimes)
    {
        if (prime == 1)
        {
            continue;
        }
        else if (number % prime == 0)
        {
            return 0;
        }
    }
    // If we get to here, number is prime
    return number;  
}   

1
@G Molvi:我认为这不适合在Serverfault上发布。@Sylverdrag,我刚刚在我的Linux上通过PHP CLI测试了这段代码(PHP版本),执行时间大约是30秒左右,误差在5秒左右。将Apache排除在外可能会导致更好的时间,但我也没有运行XAMP。至于C#能够更快地完成它,那是因为它是一种编译语言,但差距相当大,我很想看看PHP是否能够接近它。 - Jim
3
@Sylverdrag,您可以通过将==更改为===轻微提高性能,通过将$prevPrimes作为引用传递来大幅提高性能。http://pastebin.com/C2z7vUj5 - Hannes
1
@Sylverdrag 不用谢 - 如果有人发布了的话:D 啊,没事,Brad F Jacobs 需要这些积分(开玩笑) - Hannes
1
@Salman A:这更糟糕了。从6秒变成了36秒。 - Jim
1
我看到的基准测试将foreach作为最佳性能的数组迭代循环。 - dqhendricks
显示剩余14条评论
3个回答

5
"利用数学的力量! 不只是毫无意义的编码。以下是一些可以提高性能的要点。"
  • 为什么要使用数组来匹配数字?
  • 因此,foreach函数是无效的-循环应该在floor(sqrt(number))结束

    例如:sqrt(64) = 8-> 所有素数除数将从1到8。其他人将是它们的产品(32 = 4 x 8 = 2x2x2x2x2)

  • 使用公式跳到下一个可能的质数

    数学:

    可被2整除的数字-2、4、6、8、10、12-> 2k+1 = 2x1+1 = 3,5,......

    可被3整除的数字-3、6、9、12->我们已经有了6和12,所以3、9、15、21-> 3(2k-1) = 3(2x1-1) = 3,9,...

这里是来自hk管理员在项目欧拉中的伪代码

isPrime ( number )
{
    if ( number == 1 )      return false
    elseif ( number < 4 )       return true
    elseif ( number % 2 == 0 )  return false
    elseif ( number < 9 )       return true
    elseif ( number % 3 == 0 )  return false
    else
        r = floor ( sqrt ( number ) ) 
    f = 5
    while ( f <= r )
    {
        if ( number % f == 0 ) return false
        if ( number % ( f + 2 ) == 0 ) return false
        f = f + 6
    }
    return true
}

PS

关于执行速度的差异 - PHP是解释型语言,在浏览器中查看结果需要运行3个程序 - 浏览器,服务器和PHP解释器。您发出HTTP请求,服务器调用PHP(可能还有其他一堆东西,例如日志记录),PHP读取脚本并执行它。比C#多了很多步骤。

C#中执行编译代码。


@Bakudan:谢谢。我会研究一下。然而,这里真正的问题是关于在php和C#中运行相同代码的性能差异。我并不特别担心找到计算质数最有效的方法,我更感兴趣的是找出是什么让PHP表现得更快或者至少不那么慢。 - Sylverdrag
@Bakudan:谢谢。据我所知,浏览器向Apache服务器发送请求。然后将请求发送到PHP解释器。 PHP解释器然后运行脚本(包括计时器调用),然后将结果发送回Apache,Apache再将其发送到浏览器。换句话说,计时仅在php解释器中发生,并且不包括http往返所需的时间。如果是这样,我不明白浏览器-Apache序列如何增加脚本运行时间。我是否在wamp堆栈上运行PHP方面漏掉了什么重要的东西? - Sylverdrag
@Bakudan:哦,那个。但是对于C#脚本也是一样的。相同的程序运行。无论如何,在任何时候CPU或内存都没有被充分使用,因此不应该是限制因素。PHP会导致相当大的峰值,但它不会在任何核心上达到最大值。C#甚至没有注册。 - Sylverdrag
@Bakudan: 你说的“每次运行”是指什么?如果CPU没有被充分利用,我不明白为什么CPU会成为一个限制。如果计时器只在脚本运行时启动并在脚本结束时停止,我不明白为什么浏览器和服务器会添加延迟。无论服务器和浏览器需要多长时间来完成他们的工作,应该在计时器启动之前或计时器停止之后发生,因此永远不会进入计算中。我在这里缺少了什么?如你所见,我还有很多问题不理解。 - Sylverdrag
@Bakudan:抱歉,看起来你错了。我得到的结果是23秒,无论怎样。我想这意味着PHP并不适合处理这种事情。 - Sylverdrag
显示剩余6条评论

3

最终编辑

这是Bakudan逻辑中返回此结果的PHP代码:

start time:44:25:000000
#10001: 104759
End time:44:26:000000

代码:

<?php
echo "start time:" . date("i:s:u") . "\n";

function isPrime($number, &$primes)
{
    if ($number === 1) return false;
    elseif ($number %2 === 0) return false;
    elseif ($number < 4) return true;
    elseif ($number < 9) return true;
    elseif ($number %3 === 0) return false;
    else $r = floor(sqrt($number));

    $f = 5;
    while ($f <= $r) {
        if ($number % $f ===0) return false;
        if ($number % ($f+2) === 0) return false;
        $f = $f + 6;
    }

    return true;
}

$primes = array();
$nbPrimes = $i = 0;
while ($nbPrimes < 10001)
{
    $i++;
    if (isPrime($i, $primes) !== false)
    {
        $primes[] = $i;
        $nbPrimes++;
    }
}
echo "#$nbPrimes: " . end($primes) . "\n";
echo "End time:" . date("i:s:u") . "\n";
给了我伪代码,我只是翻译并将其写成了上述 OP 的脚本。

编辑2

我稍微整理了一下代码,没有改进什么,可能增强了“可读性”。但是,是的,我认为这是使用 PHP 可以得到的最好结果,在 i7 上没有 Apache 的情况下需要 5 秒。

    <?php
    echo "start time:" . date("i:s:u") . "\n";

    function isPrime($number, &$primes)
    {
        foreach($primes as $prime) {
            if ($number % $prime === 0 && $prime > 1)
                    return false;
        }
    }

    $primes = array();
    $nbPrimes = $i = 1;
    while ($nbPrimes <= 10001)
    {
        if ($i % 2 !== 0 && isPrime($i, $primes) !== false)
        {
            $primes[] = $i;
            $nbPrimes++;
        }
        $i++;
    }
    echo "#$nbPrimes: " . end($primes) . "\n";
    echo "End time:" . date("i:s:u") . "\n";

编辑

通过将$prime === 1移动到同一个if语句中$number % $prime检查之后,再次减少了一秒钟的时间。

start time:29:40:000000
#10001: 104743
End time:29:45:000000

采用Hannes的建议,进行严格检查并将数组作为引用传递,并加入自己的一些调整(在函数内部修改数组):

ini_set('max_execution_time', 300);
echo "start time:" . date("i:s:u") . "\n";

function isPrime($number, &$prevPrimes)
{
   foreach ($prevPrimes as $prime) {
        if ($number % $prime === 0 && $prime !== 1)
        {
            return false;
        }
    }

    // If we get to here, $number is prime
    $prevPrimes[] = $number;
    return $number;
}

$primes = array();
$i = 0;
$nbPrimes = 0;
while ($nbPrimes < 10001)
{
    $i++;
    if ($i % 2 !== 0)
    {
        $result = isPrime($i, $primes);

        if ($result !== 0)
        {
            $nbPrimes++;
        }
    }
}
echo "#$nbPrimes: $result\n";

echo "End time:" . date("i:s:u") . "\n";

最终结果是:
start time:52:08:000000
#10001: 104743
End time:52:15:000000

VS你的代码:

start time:50:44:000000
#10001: 104743
End time:51:17:000000

一个很好的改进,但没有C#那样强大,这只是展示了编译语言的威力 :)

谢谢。将$prime === 1与“if”结合起来是一个很好的优化。现在它只需要23秒(通过Apache)。在C#中,它也略微提高了速度(0.41秒=>0.40秒),而通过引用传递则会得到相同的结果(可能已经被编译器优化)。我打算等一天左右再批准答案,以防有其他建议的优化。到目前为止,这个问题对我来说是一个金矿。 - Sylverdrag
没关系,这只是让我感到好奇,觉得很有趣 :) 很高兴它能帮到你,我仍在努力改进它,但是想不出更多的点子了。如果我找到了更多的东西,我会更新的。 - Jim

2
虽然我预计C#会比php表现更好,但这种差异让我怀疑存在严重的配置问题,但我不知道该从哪里入手。
启动PHP引擎会给Web服务器带来一些开销。PHP的加载方式(例如作为服务器启动时的模块加载或对每个.php请求进行按需加载)决定了涉及多少开销。在Windows上有两个可用的PHP变体:线程安全和非线程安全,后者被认为更快。
如果这是XAMPP的配置问题,我认为您可以通过在Web服务器上运行测试3次并记录平均时间来隔离它。然后通过PHP CLI 3次运行相同的脚本并记录平均值。如果差异明显,那么您可能要责备XAMPP。您应该能够在XAMPP安装文件夹的某个位置找到PHP CLI二进制文件。
在我的系统上,我得到了以下结果:
PHP-CLI:            #10001: 104743 -- Time taken: 30.25 second(s)
PHP on IIS/FastCGI: #10001: 104743 -- Time taken: 29.89 second(s)
PHP on Apache/CGI:  #10001: 104743 -- Time taken: 29.93 second(s)

没有太大的区别--我更愿意优化代码。

编辑

使用这个修改后的代码,执行时间从约30秒降至约5.85秒,机器和一切都相同。唯一值得一提的是,在isPrime函数被调用104743次时,我使用了全局数组而不是每次传递它的值。通过引用传递数组也会产生类似的执行时间,差别在1秒左右。比较运算符只能节省一两秒,但并不多。

/*
 * Project Euler #7:
 * By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, we can see that the 6th prime is 13.
 * What is the 10001st prime number?
 */
ini_set('max_execution_time', 300);  
$t0 = microtime(true);
$primes = array();
function isPrime($number)
{       
    global $primes;
    foreach ($primes as $prime)
    {
        if ($prime === 1)
        {
            continue;
        }
        elseif ($number % $prime === 0)
        {
            return 0;
        }
    }
    return $number; 
}
$i = 0;
$nbPrimes = 0;
while ($nbPrimes < 10001)
{
    $i++;
    if ($i % 2 !== 0)
    {
        $result = isPrime($i);
        if ($result !== 0)
        {
            $primes[] = $i;
            $nbPrimes++;
        }
    }
}
$t1 = microtime(true);
echo sprintf('#%d: %d -- Time taken: %.2f second(s)', $nbPrimes, $result, $t1 - $t0);

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