문제

CSV 처럼 쉼표를 구분자로 하는 여러 문자열의 집합을 만들고 싶다고 할 때, 쉼표를 #X 라는 문자열로 이스케이프 하고 # 문자 자체를 ## 으로 이스케이프 해서 저장한다고 가정하면

#AB#XY, #AB#CD

위와 같은 문자열은

##AB##XY#X ##AB##CD

이렇게 표현할 수 있다. 이렇게 인코딩 된 문자열을 다시 디코딩 할 경우에 PHP에서 사용할 수 있는 여러가지 방법을 알아본다.

각 함수별 비교

preg_replace

$string = str_repeat('##AB##XY#X ##AB##CD ', 500000);

echo 'Memory1: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;

function replacer($modifier) {
	switch ($modifier) {
		case '##': return '#';
		case '#X': return ',';
	}
	return $modifier;
};

$time = microtime(true);
$string = preg_replace('/#./e', "replacer('\\0')", $string);

echo 'Memory2: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;
echo 'Replaced in ', round(microtime(true) - $time, 2), ' sec', PHP_EOL;
echo 'Result: ', substr($string, 0, 14), PHP_EOL;


Memory1: 10.14MB
Memory2: 19.67MB
Replaced in 13.14 sec
Result: #AB#XY, #AB#CD

preg_callback

$string = str_repeat('##AB##XY#X ##AB##CD ', 500000);

echo 'Memory1: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;

function replacer($matches) {
	$modifier = $matches[0];
	switch ($modifier) {
		case '##': return '#';
		case '#X': return ',';
	}
	return $modifier;
};

$time = microtime(true);
$string = preg_replace_callback('/#./', 'replacer', $string);

echo 'Memory2: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;
echo 'Replaced in ', round(microtime(true) - $time, 2), ' sec', PHP_EOL;
echo 'Result: ', substr($string, 0, 14), PHP_EOL;
Memory1: 10.14MB
Memory2: 19.67MB
Replaced in 4.25 sec
Result: #AB#XY, #AB#CD

callback 파라미터로 PHP 5.3.0 부터 지원하는 anonymous function을 사용하면 약간이나마 더 빠르게 실행할 수 있다.

$string = str_repeat('##AB##XY#X ##AB##CD ', 500000);

echo 'Memory1: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;

$replacer = function($matches) {
	$modifier = $matches[0];
	switch ($modifier) {
		case '##': return '#';
		case '#X': return ',';
	}
	return $modifier;
};

$time = microtime(true);
$string = preg_replace_callback('/#./', $replacer, $string);

echo 'Memory2: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;
echo 'Replaced in ', round(microtime(true) - $time, 2), ' sec', PHP_EOL;
echo 'Result: ', substr($string, 0, 14), PHP_EOL;
Memory1: 10.14MB
Memory2: 19.67MB
Replaced in 3.89 sec
Result: #AB#XY, #AB#CD

str_replace

$string = str_repeat('##AB##XY#X ##AB##CD ', 500000);

echo 'Memory1: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;

$time = microtime(true);
$string = str_replace(array('##', '#X'), array('#', ','), $string);

echo 'Memory2: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;
echo 'Replaced in ', round(microtime(true) - $time, 2), ' sec', PHP_EOL;
echo 'Result: ', substr($string, 0, 14), PHP_EOL;
Memory1: 10.14MB
Memory2: 7.27MB
Replaced in 0.26 sec
Result: #AB,Y, #AB#CD 

strtr

$string = str_repeat('##AB##XY#X ##AB##CD ', 500000);

echo 'Memory1: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;

$map = array(
	'##' => '#',
	'#X' => ','
);

$time = microtime(true);
$string = strtr($string, $map);

echo 'Memory2: ', round(memory_get_usage() / 1024 / 1024, 2), 'MB', PHP_EOL;
echo 'Replaced in ', round(microtime(true) - $time, 2), ' sec', PHP_EOL;
echo 'Result: ', substr($string, 0, 14), PHP_EOL;
Memory1: 10.13MB
Memory2: 7.75MB
Replaced in 0.33 sec
Result: #AB#XY, #AB#CD

결과

preg_replace 함수에서 e modifier를 사용하는 경우와 preg_replace_callback 함수를 사용하는 경우 모두 변환 과정에서 많은 메모리를 소비하고 이후에도 release 하지 않는데 메모리가 제한된 환경에서 큰 용량의 텍스트를 처리해야 하는 경우 메모리 부족 에러가 일어나기 쉽다. PHP의 PCRE 소개 페이지를 보면 정규 표현식에 대해 최대 4096개의 캐시를 유지한다고 하는데 이런 작용의 부산물로 메모리 문제가 발생한다고 한다.

속도 차이를 보면 preg_replace_callback 쪽이 월등히 빠른데 preg_replace 함수에 e modifier를 써야하는 모든 경우에 preg_replace_callback 대체가 가능하므로 이 쪽을 사용하는 편이 더 좋다고 할 수 있다. 또 가능하면 함수 이름을 파라미터로 넘기는 것 보다는 anonymous function을 만들어 넘기는 쪽이 미미하나마 속도 향상을 기대할 수 있다.

str_replace 함수의 경우는 나머지 3개의 함수들과는 다르게 이미 치환된 결과에 대해서도 다시 치환을 하게 된다. 그래서 이 테스트의 경우 str_replace 함수의 결과만 결과가 다른 것을 볼 수 있다. 반면에 strtr 함수는 이미 치환된 결과는 다시 치환하지 않는다. 두 함수의 쓰임새가 각각 다르므로 경우에 맞게 사용하면 된다. 두 가지 함수를 모두 사용할 수 있는 경우라면 str_replace 쪽이 메모리 사용량과 속도 면에서 앞서므로 이 쪽을 사용하는게 더 좋다.