IT

PHP Fiber를 이용하여 병렬연산하기 (예제포함)

번개애비 2022. 6. 15. 22:07

 

PHP8 이 새롭게 나오면서 JIT이라는 개념으로

기존 PHP 7 대비 성능이 크게 향상되었다고 주장했었으나,

사실, 큰 성능 향상을 기대하기 힘들었다.

(슬그머니 올려두었던 나머지 발 마저도 떼야되나...)

 

 

 

 

프레임워크에 동일한 성능의 클라우드에  PHP 8.1 과 PHP 8 을 올려두고 여러번 테스트했지만, 

사실 상 유의미한 성능변화폭이 없는것도 사실이다.  (기대말라... 💩)

22%의 성능향상이 있다고 하는데, 솔직히 PHP의 자체성능보다 TTFB와 같은 네트워크 반응속도가 더 크게 차이가 나는게 현실....

 

그리고, JIT을 Opcache와 활성화해두면 컴파일 캐싱을 위해

가끔 엄청나게 속도가 느려지는 현상이 발생되는 문제점도 존재함. 

(참고로 서버셋팅이 잘못될리 없음 ㅋㅋ)

(PHP JIT이 드디어 나왔다고 환호했던 예전의 나를 반성하게 만들어버림....)

 


작년에 PHP 8.1 이 새롭게 나오면서 새로운 기능을 이것 저것 맛보고 있는데,

그러던중, PHP 8.1 에서부터 Fiber(광섬유??) 라는 새로운 비동기연산? 을 지원하는 기능이 추가된 것을 발견했다.

 

 

 

참고로 PHP에서 비동기 혹은 병렬방식의 연산을 위해서는....

 

 

1. CURL을 이용한다. (완벽한 비동기 구현안됨)

2. pthead를 이용한다. (업데이트 중단 / FPM, FastCGI와 같은 NTS모드에서 작동불가능)

3. parallel를 이용한다. (FPM, FastCGI와 같은 NTS모드에서 작동불가능)

.....

 

 

 

와 같은 야매스러운 방법들이 있었는데,

이번에 새롭게 선보인 Fiber는 그나마 비동기를 제대로 활용할 수 있는 기능이었다.

 

 

PHP Fiber를 간단하게 테스트를 해보고 결과를 공유한다.

 

 

다른사람들은 블록킹 I/O를 테스트하기 위한 HTTP파싱이나 MySQL 쿼리 같은걸 주로 사용하는것 같은데,

나는 시간관계상 무식하게 2천만번의 거듭제곱 연산으로 대체한다. 

 

 

기존의 1천만번을 2번 순차적으로 거듭제곱을 연산하는 코드

function get_time(){
	$t = explode(' ',microtime()); 
	return (float)$t[0]+(float)$t[1]; 
}
	
$start = get_time(); // 속도 측정 시작
	
for($i=0;$i<10000000;$i++) {
	$a = pow($i, 20);
}
for($i=0;$i<10000000;$i++) {
	$a = pow($i, 20);
}
	
$end = get_time(); // 속도 측정 끝
$time = $end - $start;
echo number_format($time,6);

 

 

Fiber를 활용하여 1천만번을 2번 비동기로 거듭제곱을 연산하는 코드

function get_time(){
	$t = explode(' ',microtime()); 
	return (float)$t[0]+(float)$t[1]; 
}

$start = get_time(); // 속도 측정 시작
	
$fiber = new Fiber(function (){
	for($i=0;$i<10000000;$i++) {
		$a = pow($i, 20);
	}
	Fiber::suspend($a);

	$anotherFiber = new Fiber(function(){
		for($i=0;$i<10000000;$i++) {
			$a = pow($i, 20);
		}
		Fiber::suspend($a);
	});
		
	$anotherFiber->start();
});
	
$fiber->start();
	
$end = get_time(); // 속도 측정 끝
$time = $end - $start;
echo number_format($time,6);

 

 

 

결과는 어땠을까?

 

 

 

 

 

 

 

 

왼쪽 : Fiber / 오른쪽 : 기존의 순차방식

 

재미있는 결과가 나왔다.

 

다수의 테이블에서 비동기로 데이터를 가져오거나,

여러 사이트를 함께 파싱하거나 할때 유용하게 사용할 듯한데,

보통 그런경우는 PHP를 사용하지 않는것이 함정.. ㅋㅋㅋㅋ

 

 

간단한 예제로 Fiber를 활용하는 방법을 정리해봤다.

$fiber = new Fiber(function(): void {
	/***********
	연산코드 블라블라
	************/
	//Fiber 내부에서 생성한 데이터를 Fiber 외부로 전송
	Fiber::suspend($연산결과변수);
});

//Fiber 동작시작, $연산결과변수가 $output으로 복사됨
$output = $fiber->start();

//Fiber상태가 완료될때까지 반복문
while(!$fiber->isTerminated()){
	//Fiber 실행을 재개
	$fiber->resume();
}

 

 

 

 

Fiber 내부에 변수를 전달하는 방법들은 다음과 같다.

본인이 각자 편한 방식으로 쓰면 될듯...

$fiber = new Fiber(function($변수명): void {

});
$fiber = new Fiber(function (){
	global $변수명
});
$fiber = new Fiber(function() use ($변수명){

});

 

 

 

또다른 예제....

CURL http code를 병렬로 반환하기

<?php
	declare(ticks=1);
	class Async{
		protected static $names = [];
		protected static $fibers = [];
		protected static $params = [];
		public static function register(string|int $name, callable $callback, array $params){
			self::$names[] = $name;
			self::$fibers[] = new Fiber($callback);
			self::$params[] = $params;
		}
		public static function run(){
			$output = [];
			while(self::$fibers){
				foreach (self::$fibers as $i => $fiber) {
					try{
						if(!$fiber->isStarted()){
							register_tick_function('Async::scheduler');
							$fiber->start(...self::$params[$i]);
						}elseif($fiber->isTerminated()){
							$output[self::$names[$i]] = $fiber->getReturn();
							unset(self::$fibers[$i]);
						}elseif($fiber->isSuspended()){
							$fiber->resume();
						}
					}catch(Throwable $e){
						$output[self::$names[$i]] = $e;
					}
				}
			}
			return $output;
		}
		public static function scheduler (){
			if(Fiber::getCurrent() === null){
				return;
			}
			if(count(self::$fibers) > 1){
				try {
					Fiber::suspend();
				}
				catch (Exception $ex) {
					echo '[Asyc ERROR!]'.$ex->getMessage();
				}
			}
		}
	}
	
	//CURL HTTP CODE반환 함수
	function curltest($url){
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
		
		$response = curl_exec($ch);
		$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		curl_close($ch);
		return $httpcode;
	}
	
	//속도측정용
	function get_time(){
		$t = explode(' ',microtime()); 
		return (float)$t[0]+(float)$t[1]; 
	}
	
	unset($target);
	$target['naver'] = 'https://www.naver.com';
	$target['google'] = 'https://www.google.com';
	$target['kakao'] = 'https://www.kakao.com';
	$target['burndogfather'] = 'https://www.burndogfather.com';
	$target['zum'] = 'https://zum.com/';
	$target['daum'] = 'https://www.daum.net/';
	$target['uptimerobot'] = 'https://uptimerobot.com/';
	$target['accessibility'] = 'https://accessibility.kr/';
	$target['php'] = 'https://www.php.net';
	print_r($target);
	echo '<br /><br />';
	
	
	$start = get_time(); // 속도 측정 시작
	
	$outputs = [];
	foreach($target as $key => $value) {
		$outputs[$key] = curltest($value);
	}
	print_r($outputs);
	
	$end = get_time(); // 속도 측정 끝
	$time = $end - $start;
	echo '<br />Loop: '. number_format($time,6);
	
	echo '<br /><br />';
	
	$start = get_time(); // 속도 측정 시작
	
	foreach($target as $key => $value) {
		Async::register($key, 'curltest', [$value]);
	}
	$outputs = Async::run();
	print_r($outputs);
	
	$end = get_time(); // 속도 측정 끝
	$time = $end - $start;
	echo '<br />Aysnc: '. number_format($time,6);
?>

 

실행결과 : Fiber가 일반 Loop에 비해 미세하게 빠르다...

 

 

 

 

결론 :)

 

상대적으로 시간이 필요로 하는 A로직과

그 사이 사이에 짧게 연산하는 다른 로직들이 함께 처리하는 목적에서는 Fiber가 약간의 성능향상을 가져다 줄수 있다.

다만, 서로 로직이 비슷한 처리시간이라면 그냥 Loop랑 성능면에서 큰 차이가 없음.

 

Hack lang에서는 aync를 지원해주는데,

왜 아직도 지원하지 않는것이냐! ㅠㅠ