[php] PHP 성능 튜닝 관련 글
며칠 전에 "PHP에서 성능 개선을 위한 팁"이라는 글을 읽고 몇 가지 테스트를 해보았다.
그러면서 몇가지 잘못 알려진 부분과 새로 알게된 사실이 있어서 공유하고자 글을 남긴다.
참고. 테스터 코드 소스는 제일 밑에 붙여놓았다.
테스트 환경은 다음과 같다.
PHP 5.1.2 (cli)
eAccelerator 0.9.5
Linux
1. 최대한 쌍따옴표 대신 일반따옴표를 사용하라고?
<?php
function string1() {
for ($i=0; $i<1000000; $i++)
$str = "This is a message.";
}
function string2() {
for ($i=0; $i<1000000; $i++)
$str = 'This is a message.';
}
?>
결과
:!php tester.php 1 string1
Elasped Time: TOTAL 347.93 msec, USR 347.95 msec (100.0%), SYS 0.00 msec (0.0%)
:!php tester.php 1 string2
Elasped Time: TOTAL 359.78 msec, USR 357.95 msec (99.5%), SYS 2.00 msec (0.6%)
위에서 보다시피 쌍따옴표와 일반따옴표는 속도 차이가 거의 없다.
그래서 이번에는 문자열 사이에 변수가 들어가는 경우를 테스트 해보았다.
<?php
function string5() {
$title = 'message';
for ($i=0; $i<1000000; $i++)
$str = "This is a {$title}.";
}
function string6() {
$title = 'message';
for ($i=0; $i<1000000; $i++)
$str = "This is a ".$title.".";
}
function string7() {
$title = 'message';
for ($i=0; $i<1000000; $i++)
$str = 'This is a '.$title.'.';
?>
결과
:!php tester.php 1 string5
Elasped Time: TOTAL 1,756.88 msec, USR 1,755.73 msec (99.9%), SYS 0.00 msec (0.0%)
:!php tester.php 1 string6
Elasped Time: TOTAL 649.77 msec, USR 648.90 msec (99.9%), SYS 0.00 msec (0.0%)
:!php tester.php 1 string7
Elasped Time: TOTAL 634.70 msec, USR 634.90 msec (100.0%), SYS 0.00 msec (0.0%)
"...{$var}..." 형식을 쓴 string5() 함수가 시간이 상당히 많이 걸린 것을 볼 수 있었다.
하지만, 그것은 쌍따옴표 안에서 {} 를 이용하여 변수를 넣었을 때에 해당되는 것이고,
string6() 과 string7() 에서 보듯이 쌍따옴표와 일반따옴표의 차이는 없었다.
결국, 성능은 쌍따옴표를 쓰냐 안 쓰냐가 아니라 문자열 중간에 변수를 어떻게 처리하는지에 문제인 것이다.
오히려 일반따옴표에서는 \n 처리가 애매하기 때문에 대신 PHP_EOL 을 사용했었는데,
"\n" 와 PHP_EOL 의 속도를 비교해보면 "\n" 가 더 빨랐다.
2. 레퍼런스 파라미터의 함정
PHP5 에서부터는 객체를 함수의 파라미터로 전달할 때 인스턴스의 주소를 넘기도록 수정되었다.(PHP4 에서는 객체를 복사해서 넘김) 하지만, 배열은 PHP4 와 마찬가지로 복사해서 넘긴다.
그럼 다음의 코드 중에 어떤 코드가 더 빠를 것이라고 생각하는가?
<?php
function ref1() {
$arr = array_pad(array(), 1000, 1);
for ($i=0; $i<5000; $i++)
$cnt = ref1_count($arr);
}
function ref1_count(&$arr) {
return count($arr);
}
function ref2() {
$arr = array_pad(array(), 1000, 1);
for ($i=0; $i<5000; $i++)
$cnt = ref2_count($arr);
}
function ref2_count($arr) {
return count($arr);
}
?>
결과
:!php tester.php 1 ref1
Elasped Time: TOTAL 920.32 msec, USR 918.86 msec (99.8%), SYS 0.00 msec (0.0%)
:!php tester.php 1 ref2
Elasped Time: TOTAL 8.45 msec, USR 8.00 msec (94.7%), SYS 1.00 msec (11.8%)
ref1() 함수에서는 $arr 변수를 레퍼런스로 넘기고, ref2() 함수에서는 $arr 변수를 VALUE로 넘긴다.
ref2() 에서는 VALUE 로 전달하기 때문에 ref2() 가 시간이 더 걸릴 것 같지만 실제로는 반대로 ref1() 함수가 훨씬 더 많은 시간이 걸린다.
그 원인을 분석하기 위하여 추가로 다음과 같은 테스트를 더 해보았다.
<?php
function ref3() {
global $arr2;
for ($i=0; $i<5000; $i++)
$cnt = ref2_count($arr2);
}
function ref4() {
$arr =& $GLOBALS['arr2'];
for ($i=0; $i<5000; $i++)
$cnt = ref2_count($arr);
}
?>
결과
:!php tester.php 1 ref3
Elasped Time: TOTAL 1,013.52 msec, USR 1,012.85 msec (99.9%), SYS 1.00 msec (0.1%)
:!php tester.php 1 ref4
Elasped Time: TOTAL 964.66 msec, USR 964.85 msec (100.0%), SYS 0.00 msec (0.0%)
이번에는 ref2() 함수에서 사용한 ref2_count() 를 사용했음에도 불구하고 ref3() 와 ref4() 는 ref1() 결과와 거의 유사하다. ref1() 과 ref3(), ref4() 의 공통점은 모두 넘기는 변수인 $arr과 $arr2 가 레퍼런스로 전달한다는 것이다.
그럼 이런 차이가 생기는가? 이는 PHP Zend 엔진을 이해해야 한다. ref2() 에서 $arr 를 ref2_count() 함수에 VALUE 로 전달하나 실제 변수의 복사가 이루어지는 시점은 그 변수를 수정할 때이다. 다시 말해, PHP Zend 엔진에서는 VALUE 로 값을 전달받았다고 하더라도 굳이 복사가 필요없는 경우(함수 내부에서 파라미터를 수정하지 않는 경우)에는 값을 복사하지 않는다. 대신 파라미터 값이 바뀌는 시점에 복사를 시작하게 된다.
하지만, 레퍼런스 변수를 다시 다른 함수에 전달하게 되면 값의 수정이 필요하든 없든 상관없이 그 순간에 복사가 이루어진다. 위의 코드에서는 "count($arr)" 부분에서 복사가 이루어지게 된다. 위에서 ref_countX() 함수들은 단순히 변수값을 참조하기만 하기 때문에 복사가 필요없지만 쓸데없이 복사가 이루어지느라 많은 시간이 걸리게 되는 것이다.
결론. 레퍼런스 파라미터는 함수내에서 수정이 필요한 경우에만 사용하는 것이 좋다.
3. 비교문의 성능과 올바른 습관
이번에는 IF 문에서 문자열 비교 구문을 테스트 해보았다.
<?php
function ifstring1() {
$a = "abcd";
for ($i=0; $i<1000000; $i++)
if (!$a);
}
function ifstring2() {
$a = "abcd";
for ($i=0; $i<1000000; $i++)
if ($a === "");
}
function ifstring3() {
$a = "abcd";
for ($i=0; $i<1000000; $i++)
if ($a == "");
}
function ifstring4() {
$a = "abcd";
for ($i=0; $i<1000000; $i++)
if (strlen($a) <= 0);
}
?>
결과
:!php tester.php 1 ifstring1
Elasped Time: TOTAL 242.82 msec, USR 242.96 msec (100.1%), SYS 0.00 msec (0.0%)
:!php tester.php 1 ifstring2
Elasped Time: TOTAL 240.84 msec, USR 240.96 msec (100.1%), SYS 0.00 msec (0.0%)
:!php tester.php 1 ifstring3
Elasped Time: TOTAL 425.76 msec, USR 422.94 msec (99.3%), SYS 0.00 msec (0.0%)
:!php tester.php 1 ifstring4
Elasped Time: TOTAL 610.61 msec, USR 610.91 msec (100.0%), SYS 0.00 msec (0.0%)
ifstring4() 의 경우에는 함수에 대한 시간 손실 때문에 제일 많은 시간이 걸렸다.
ifstring3() 의 경우에는 변수 Type Conversion 때문에 시간 손실이 발생한다.
성능상 제일 좋은 방법은 ifstring1() 과 ifstring2() 의 경우이다.
여기서 내가 추천하는 방법은 ifstring2() 이다. ifstring1() 의 경우에는 다음과 같은 경우가 버그가 생길 수 있다.
<?php
$a = "0";
if ($a) echo "Success"; else "Failed";
?>
위의 코드에서 어떤 값이 출력될 것 같은가? 정답은 "Failed" 이다. $a 가 자동으로 integer 로 변환되고, 숫자 0 이 자동으로 FALSE 로 변환되기 때문이다.
결론. 가능한한 "===" 연산자를 사용하여 비교하는 것이 좋다.
4. switch 와 if
가끔 switch 와 if 중에 뭘 쓸까 고민할 때가 있다. 예전에 PHP.net 에서 switch 가 더 빠르다는 글을 읽고 지금까지 주로 switch 를 사용했는데 이번에 테스트해보았다.
<?php
function switch1() {
$a = "delete";
for ($i=0; $i<1000000; $i++) {
switch ($a) {
case "insert":
break;
case "update":
break;
case "delete":
break;
case "select":
break;
default:
break;
}
}
}
function switch2() {
$a = "delete";
for ($i=0; $i<1000000; $i++) {
if ($a === "insert") {
}
else if ($a === "update") {
}
else if ($a === "delete") {
}
else if ($a === "select") {
}
else {
}
}
}
?>
결과
:!php tester.php 1 switch1
Elasped Time: TOTAL 975.25 msec, USR 970.85 msec (99.5%), SYS 0.00 msec (0.0%)
:!php tester.php 1 switch2
Elasped Time: TOTAL 547.84 msec, USR 544.92 msec (99.5%), SYS 0.00 msec (0.0%)
이 결과는 IF 문에서 "==" 을 사용할 것인가 "===" 사용할 것인가와 같은 문제이다. switch 에서는 기본적으로 "==" 을 사용하는 것으로 보인다. 따라서 위와 같은 결과가 나왔다.
결론. switch 와 if 문 중에서는 if 문이 더 좋다. 단, === 연산자를 사용한다는 전제하에...
5. 테스터 코드
<?php
require_once "func.inc.php";
if (isset($_SERVER['argv'][1]))
$try = $_SERVER['argv'][1];
else
$try = 1;
if (isset($_SERVER['argv'][2]))
$funcname = $_SERVER['argv'][2];
else
exit;
init();
for ($n=0; $n<$try; $n++) {
$dblTotTime1 = microtime(TRUE);
$dictResUsage = getrusage();
$dblUsrTime1 = $dictResUsage['ru_utime.tv_sec'] + $dictResUsage['ru_utime.tv_usec'] / 1000000;
$dblSysTime1 = $dictResUsage['ru_stime.tv_sec'] + $dictResUsage['ru_stime.tv_usec'] / 1000000;
$funcname();
$dblTotTime2 = microtime(TRUE);
$dictResUsage = getrusage();
$dblUsrTime2 = $dictResUsage['ru_utime.tv_sec'] + $dictResUsage['ru_utime.tv_usec'] / 1000000;
$dblSysTime2 = $dictResUsage['ru_stime.tv_sec'] + $dictResUsage['ru_stime.tv_usec'] / 1000000;
$dblTotTime = $dblTotTime2 - $dblTotTime1;
$dblUsrTime = $dblUsrTime2 - $dblUsrTime1;
$dblSysTime = $dblSysTime2 - $dblSysTime1;
$strTotTime = number_format($dblTotTime * 1000, 2);
$strUsrTime = number_format($dblUsrTime * 1000, 2);
$strSysTime = number_format($dblSysTime * 1000, 2);
$strUsrPer = number_format(($dblUsrTime / $dblTotTime) * 100, 1);
$strSysPer = number_format(($dblSysTime / $dblTotTime) * 100, 1);
echo 'Elasped Time: TOTAL '.$strTotTime.' msec, USR '.$strUsrTime.' msec ('.$strUsrPer.'%), SYS '.$strSysTime.' msec ('.$strSysPer.'%)'.PHP_EOL;
}
?>
http://teeroz.egloos.com/2703307
'IT-개발,DB' 카테고리의 다른 글
익스트림 프로그래밍 (XP)의 '명암'(1) (0) | 2016.10.27 |
---|---|
그리스 문자 및 발음 표기 (0) | 2016.10.24 |
[PHP]] 연산자 (0) | 2016.10.05 |
[javascript] 금액에 콤마붙이기 (0) | 2016.10.03 |
SOA란 무엇인가 (0) | 2016.10.03 |
댓글