php8 에 도입되는 annotation (attributes)
php 8 부터는 attrubutes 라는 기능을 사용할 수 있습니다. 다른 많은 언어에서는 annontation 이라고 불리는 것이죠.
기본적으로 PHP 의 배열은 일반적인 ArrayList 구현이 아니라, Hash Table 입니다.
그러다보니 php 개발자들은 배열을 배열처럼 쓰지 않고 Hash Table 처럼 이용하는 분들이 많습니다.
(나쁜거 아니에요!)
$a = ['q_lazzarus' => '킹왕짱'];
echo $a['q_lazzarus'];
다시 기초로 돌아가자면, array 는 동일한 자료구조의 반복 입니다.

메모리 단위에서 생각해보면 동일한 크기의 방이 주루룩 있는 구조이죠.

!!
그렇다면, string 하나에 integer 몰빵해서 넣으면 되자너?

5개의 원소가 있는 배열이 있다고 가정하고 데이터를 읽는 간단한 함수를 만들어 보겠습니다.
function array_get($i) {
$data = '12345';
return substr($data, $i, 1);
}
echo array_get(3);
3
짜잔! 이렇게 string 변수를 참조하여, 값을 리턴하는 간단한 코드가 만들어졌습니다.
하지만 당연하게도 이건 못 씁니다.
왜냐하면 원소의 데이터가 0~9 까지만 허용하는 말도 안되는 구현이기 때문입니다.
가만히 다시 생각해 봅시다. 설마 integer 데이터를 메모리에 string 처럼 저장하지 않겠죠…
보통의 경우 integer 자료형은 4 Bytes 를 차지하고 있습니다.
| Data Type | Size (in bytes) | Range |
|---|---|---|
| short int | 2 | -32,768 to 32,767 |
| unsigned short int | 2 | 0 to 65,535 |
| unsigned int | 4 | 0 to 4,294,967,295 |
| int | 4 | -2,147,483,648 to 2,147,483,647 |
| long int | 4 | -2,147,483,648 to 2,147,483,647 |
| unsigned long int | 4 | 0 to 4,294,967,295 |
| long long int | 8 | -(2^63) to (2^63)-1 |
| unsigned long long int | 8 | 0 to 18,446,744,073,709,551,615 |
| singed char | 1 | -128 to 127 |
| unsigned char | 1 | 0 to 255 |
호오 그렇다면? 데이터를 변환해서 넣도록 하겠습니다.
다행히 php 에서는 pack 이라는 함수가 이러한 변환을 편하게 도와줍니다.
$data = str_repeat(pack('I', null), 5);
function array_get($i) {
global $data;
$binary = substr($data, $i * 4, 4);
$unpack = unpack('I', $binary);
return $unpack[1];
}
function array_set($i, $value) {
global $data;
$binary = pack('I', $value);
$data = substr_replace($data, $binary, $i * 4, 4);
}
array_set(3, 65535);
echo array_get(3);
65535
오 이번에는 10 을 넘어서 심지어 65535 도 잘 나오고 있습니다.
하지만 아직 문제가 있습니다.
echo array_get(2);
0
아니 어떻게 된 일이죠?? 분명히 공간만 만들어주기 위해서 null 을 넣어주었는데?
사실은 integer 자료형에서는 null 은 없습니다.
왜냐하면 null 은 null 이기 때문입니다. (자료형도 달라요)
그렇다보니 저희가 만든 함수에서는 null 은 처리가 불가능 합니다.
그럼 어떻게 해야할까요?

생각해보면 데이터를 넣을때 한칸씩 미루면 되지 않을까요?
$data = str_repeat(pack('I', 0), 5);
function array_get($i) {
global $data;
$binary = substr($data, $i * 4, 4);
$unpack = unpack('I', $binary);
$result = $unpack[1];
if (0 === $result) {
return null;
}
return $result - 1;
}
function array_set($i, $value) {
global $data;
if (null !== $value) {
$value = $value + 1;
}
$binary = pack('I', $value);
$data = substr_replace($data, $binary, $i * 4, 4);
}
var_dump(array_get(3));
NULL
이렇게 되면 데이터 제한이 4,294,967,295 에서 하나 소모가 되지만,
이 정도면 괜찮은 것 같습니다.
어느정도 된 것 같습니다. 하지만 이것을 실제 배열 처럼 쓰기에는 아직 부족합니다.
그래서 php 에서는 Array 구현하기 위한 interface 를 제공합니다.
ArrayAccess {
/* Methods */
abstract public offsetExists ( mixed $offset ) : bool
abstract public offsetGet ( mixed $offset ) : mixed
abstract public offsetSet ( mixed $offset , mixed $value ) : void
abstract public offsetUnset ( mixed $offset ) : void
}
클래스를 마치 php 기본 배열 처럼 접근 가능하게 하는 구현체이며
제한적인 의미로 accessor overloading 입니다.
$obj = new obj;
$obj[] = 'hello';
$obj[] = 'world';
echo $obj[0] . ' ' . $obj[1];
hello world
class FixedUnsignedIntegerArray implements Iterator, ArrayAccess, Countable
{
const BINARY_FORMAT = 'I';
private $position = 0;
private $length = 0;
private $binaryLength = 0;
private $data = null;
public function __construct($length)
{
$null = $this->convertBinary(null);
$this->position = 0;
$this->length = $length;
// because binary length is machine dependency
$this->binaryLength = strlen($null);
$this->data = str_repeat($null, $length);
}
private function getEntry($position)
{
$position = $position * $this->binaryLength;
$unpack = unpack(self::BINARY_FORMAT, substr($this->data, $position, $this->binaryLength));
$result = false !== $unpack ? $unpack[1] : null;
if (0 === $result) {
return null;
} else {
return $result - 1;
}
}
private function setEntry($position, $value)
{
$position = $position * $this->binaryLength;
$this->data = substr_replace($this->data, $this->convertBinary($value), $position, $this->binaryLength);
}
private function convertBinary($value)
{
if (null !== $value) {
$value = $value + 1;
}
return pack(self::BINARY_FORMAT, $value);
}
public function rewind()
{
$this->position = 0;
}
public function current()
{
return $this->getEntry($this->position);
}
public function key()
{
return $this->position;
}
public function next()
{
$this->position = $this->position + 1;
}
public function valid()
{
return $this->offsetExists($this->position);
}
public function offsetSet($offset, $value)
{
if (is_integer($offset) && $offset < $this->length && $offset >= 0) {
$this->setEntry($offset, $value);
} else {
throw new InvalidArgumentException('overflow array offset');
}
}
public function offsetExists($offset)
{
return ($offset < $this->length && $offset >= 0);
}
public function offsetUnset($offset)
{
$this->setEntry($offset, null);
}
public function offsetGet($offset)
{
return $this->getEntry($offset);
}
public function count()
{
return $this->length;
}
}
pack 함수의 설명을 다시 보자면, 환경에 따라 다른 byte 수가 나올 수 있습니다.
unsigned integer (machine dependent size and byte order)
따라서 integer byte 수를 저희가 알고 있어야 잘못된 주소를 참조하지 않도록 하겠습니다.
실제 구현에서는 Iterator, ArrayAccess, Countable 을 구현하여 좀 더 효용성을 높였습니다.
이제 구현했으니, 성능을 테스트 해보겠습니다.
테스트는 다음과 같이 진행하였습니다.
define('ARRAY_LENGTH', 10000);
$start_memory = memory_get_usage();
$vanilla = [];
for ($i = 0; $i < ARRAY_LENGTH; $i++) {
$vanilla[] = mt_rand(0, 255);
}
$end_memory = memory_get_usage() - $start_memory;
echo "legacy array memory usage : {$end_memory}\n";
$start = microtime(true);
$result = 0;
for ($i = 0; $i < ARRAY_LENGTH; $i++) {
$result += $vanilla[$i];
}
$time_elapsed_secs = microtime(true) - $start;
echo "for iterator legacy array : {$time_elapsed_secs}\n";
$start_memory = memory_get_usage();
$entries = new \Monoless\Arrays\FixedUnsignedIntegerArray(ARRAY_LENGTH);
for ($i = 0; $i < ARRAY_LENGTH; $i++) {
$entries[$i] = mt_rand(0, 255);
}
$end_memory = memory_get_usage() - $start_memory;
echo "fixed unsigned integer array memory usage : {$end_memory}\n";
$time_total = 0;
$interval = 100;
for ($j = 0; $j < $interval; $j++) {
$start = microtime(true);
$result = 0;
for ($i = 0; $i < ARRAY_LENGTH; $i++) {
$result += $entries[$i];
}
$time_elapsed_secs = microtime(true) - $start;
$time_total += $time_elapsed_secs;
}
$time_average = $time_total / $interval;
echo "iterator speed average : {$time_average}\n";
legacy array memory usage : 528440
for iterator legacy array : 0.00035810470581055
fixed unsigned integer array memory usage : 41072
iterator speed average : 0.0068418407440186
메모리 사용량은 약 90% 개선이 되었으나 속도는 기존 배열을 이길 수가 없었네요…

역시 기존껄 쓰는거 좋은거라고 배우고… 오늘도 맥주와 치킨을 시키며 잡니다.
그래도 제 삽질이 좋다면, github 와 이 패키지를 참고하세요
혹시나 싶어서 php://memory 에 fwrite / fseek 를 구현해봤는데 더 느렸습니다…
for iterator legacy array : 0.0013089179992676
string speed average : 0.0084279131889343
php://memory speed average : 0.0098107981681824
php 8 부터는 attrubutes 라는 기능을 사용할 수 있습니다. 다른 많은 언어에서는 annontation 이라고 불리는 것이죠.
가상 버튼을 기본으로, 기울기 조향은 선택으로. Apex Seoul의 모바일 입력과 권한 처리, 화면 회전 시 일시정지, 설치형 PWA까지 앞으로 구현할 방향을 정리했다.
세 차량의 코너링 설정을 프로파일로 분리했다. 터보 차량의 변속과 RPM 컷을 수정하고, Retry 뒤 나무가 사라지던 Scene 수명주기 버그를 정리했다.
코너 충돌 뒤 가속해도 움직이지 못하는 상황에 자동 복귀를 붙였다. 정상 정차와 고착을 구분하는 조건, 도로 중앙 배치와 차량 상태 초기화, 깜박임 연출, 타임어택 기록을 지키는 처리까지 정리했다.
최고 시간 하나만 저장하던 레이싱 게임에 차량별 PB와 최근 완주 이력을 붙였다. localStorage 저장부터 내부 배열 기반 기본값, 리셋 후 기록이 되살아나지 않게 하는 처리까지 정리했다.