Skip to content

Commit f2fa8fa

Browse files
committed
RFC: Add final class Collections\Deque to PHP
This has lower memory usage and better performance than SplDoublyLinkedList for push/pop operations. The API is as follows: ```php namespace Collections; /** * A double-ended queue (Typically abbreviated as Deque, pronounced "deck", like "cheque") * represented internally as a circular buffer. * * This has much lower memory usage than SplDoublyLinkedList or its subclasses (SplStack, SplStack), * and operations are significantly faster than SplDoublyLinkedList. * * See https://en.wikipedia.org/wiki/Double-ended_queue * * This supports amortized constant time pushing and popping onto the start (i.e. start, first) * or back (i.e. end, last) of the Deque. * * Method naming is based on https://www.php.net/spldoublylinkedlist * and on array_push/pop/unshift/shift/ and array_key_first/array_key_last. */ final class Deque implements IteratorAggregate, Countable, JsonSerializable, ArrayAccess { /** Construct the Deque from the values of the Traversable/array, ignoring keys */ public function __construct(iterable $iterator = []) {} /** * Returns an iterator that accounts for calls to shift/unshift tracking the position of the start of the Deque. * Calls to shift/unshift will do the following: * - Increase/Decrease the value returned by the iterator's key() * by the number of elements added/removed to/from the start of the Deque. * (`$deque[$iteratorKey] === $iteratorValue` at the time the key and value are returned). * - Repeated calls to shift will cause valid() to return false if the iterator's * position ends up before the start of the Deque at the time iteration resumes. * - They will not cause the remaining values to be iterated over more than once or skipped. */ public function getIterator(): \InternalIterator {} /** Returns the number of elements in the Deque. */ public function count(): int {} /** Returns true if there are 0 elements in the Deque. */ public function isEmpty(): bool {} /** Removes all elements from the Deque. */ public function clear(): void {} public function __serialize(): array {} public function __unserialize(array $data): void {} /** Construct the Deque from the values of the array, ignoring keys */ public static function __set_state(array $array): Deque {} /** Appends value(s) to the end of the Deque. */ public function push(mixed ...$values): void {} /** Prepends value(s) to the start of the Deque. */ public function unshift(mixed ...$values): void {} /** * Pops a value from the end of the Deque. * @throws \UnderflowException if the Deque is empty */ public function pop(): mixed {} /** * Shifts a value from the start of the Deque. * @throws \UnderflowException if the Deque is empty */ public function shift(): mixed {} /** * Peeks at the value at the start of the Deque. * @throws \UnderflowException if the Deque is empty */ public function first(): mixed {} /** * Peeks at the value at the end of the Deque. * @throws \UnderflowException if the Deque is empty */ public function last(): mixed {} /** * Returns a list of the elements from the start to the end. */ public function toArray(): array {} // Must be mixed for compatibility with ArrayAccess /** * Insert 0 or more values at the given offset of the Deque. * @throws \OutOfBoundsException if the value of $offset is not within the bounds of this Deque. */ public function insert(int $offset, mixed ...$values): void {} /** * Returns the value at offset (int)$offset (relative to the start of the Deque) * @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this vector */ public function offsetGet(mixed $offset): mixed {} /** * Returns true if `0 <= (int)$offset && (int)$offset < $this->count(). */ public function offsetExists(mixed $offset): bool {} /** * Sets the value at offset $offset (relative to the start of the Deque) to $value * @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this vector */ public function offsetSet(mixed $offset, mixed $value): void {} /** * Removes the value at (int)$offset from the deque. * @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this Deque. */ public function offsetUnset(mixed $offset): void {} /** * This is JSON serialized as a JSON array with elements from the start to the end. */ public function jsonSerialize(): array {} } ``` Earlier work on the implementation can be found at https://github.com/TysonAndre/pecl-teds (though `Teds\Deque` hasn't been updated with new names yet) This was originally based on spl_fixedarray.c and previous work I did on an RFC. Notable features of `Deque` - Significantly lower memory usage and better performance than `SplDoublyLinkedList` - Amortized constant time operations for push/pop/unshift/shift. - Reclaims memory when roughly a quarter of the capacity is used, unlike array, which never releases allocated capacity https://www.npopov.com/2014/12/22/PHPs-new-hashtable-implementation.html > One problem with the current implementation is that arData never shrinks > (unless explicitly told to). So if you create an array with a few million > elements and remove them afterwards, the array will still take a lot of > memory. We should probably half the arData size if utilization falls below a > certain level. For long-running applications when the maximum count of Deque is larger than the average count, this may be a concern. - Adds functionality that cannot be implemented nearly efficiently in an array. For example, shifting a single element onto an array (and making it first in iteration order) with `array_shift` would take linear time, because all elements in the array would need to be moved to make room for the first one - Support `$deque[] = $element`, like ArrayObject. - Having this functionality in php itself rather than a third party extension would encourage wider adoption of this
1 parent e45afbf commit f2fa8fa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+3021
-33
lines changed

ext/collections/collections_deque.c

Lines changed: 1307 additions & 0 deletions
Large diffs are not rendered by default.

ext/collections/collections_deque.h

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
+----------------------------------------------------------------------+
3+
| Copyright (c) The PHP Group |
4+
+----------------------------------------------------------------------+
5+
| This source file is subject to version 3.01 of the PHP license, |
6+
| that is bundled with this package in the file LICENSE, and is |
7+
| available through the world-wide-web at the following url: |
8+
| https://www.php.net/license/3_01.txt |
9+
| If you did not receive a copy of the PHP license and are unable to |
10+
| obtain it through the world-wide-web, please send a note to |
11+
| license@php.net so we can mail you a copy immediately. |
12+
+----------------------------------------------------------------------+
13+
| Author: Tyson Andre <tandre@php.net> |
14+
+----------------------------------------------------------------------+
15+
*/
16+
17+
#ifndef COLLECTIONS_DEQUE_H
18+
#define COLLECTIONS_DEQUE_H
19+
20+
extern zend_class_entry *collections_ce_Deque;
21+
22+
PHP_MINIT_FUNCTION(collections_deque);
23+
24+
#endif /* COLLECTIONS_DEQUE_H */
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/** @generate-class-entries */
4+
5+
namespace Collections;
6+
7+
/**
8+
* A double-ended queue (Typically abbreviated as Deque, pronounced "deck", like "cheque")
9+
* represented internally as a circular buffer.
10+
*
11+
* This has much lower memory usage than SplDoublyLinkedList or its subclasses (SplStack, SplStack),
12+
* and operations are significantly faster than SplDoublyLinkedList.
13+
*
14+
* See https://en.wikipedia.org/wiki/Double-ended_queue
15+
*
16+
* This supports amortized constant time pushing and popping onto the front or back of the Deque.
17+
*
18+
* Naming is based on https://www.php.net/spldoublylinkedlist
19+
* and on array_push/pop/unshift/shift and array_key_first.
20+
*/
21+
final class Deque implements \IteratorAggregate, \Countable, \JsonSerializable, \ArrayAccess
22+
{
23+
/** Construct the Deque from the values of the Traversable/array, ignoring keys */
24+
public function __construct(iterable $iterator = []) {}
25+
/**
26+
* Returns an iterator that accounts for calls to shift/unshift tracking the position of the start of the Deque.
27+
* Calls to shift/unshift will do the following:
28+
* - Increase/Decrease the value returned by the iterator's key()
29+
* by the number of elements added/removed to/from the start of the Deque.
30+
* (`$deque[$iteratorKey] === $iteratorValue` at the time the key and value are returned).
31+
* - Repeated calls to shift will cause valid() to return false if the iterator's
32+
* position ends up before the start of the Deque at the time iteration resumes.
33+
* - They will not cause the remaining values to be iterated over more than once or skipped.
34+
*/
35+
public function getIterator(): \InternalIterator {}
36+
/** Returns the number of elements in the Deque. */
37+
public function count(): int {}
38+
/** Returns true if there are 0 elements in the Deque. */
39+
public function isEmpty(): bool {}
40+
/** Removes all elements from the Deque. */
41+
public function clear(): void {}
42+
43+
/** @implementation-alias Collections\Deque::toArray */
44+
public function __serialize(): array {}
45+
public function __unserialize(array $data): void {}
46+
/** Construct the Deque from the values of the array, ignoring keys */
47+
public static function __set_state(array $array): Deque {}
48+
49+
/** Appends value(s) to the end of the Deque, like array_push. */
50+
public function push(mixed ...$values): void {}
51+
/** Prepends value(s) to the start of the Deque, like array_unshift. */
52+
public function unshift(mixed ...$values): void {}
53+
/**
54+
* Pops a value from the end of the Deque.
55+
* @throws \UnderflowException if the Deque is empty
56+
*/
57+
public function pop(): mixed {}
58+
/**
59+
* Shifts a value from the start of the Deque.
60+
* @throws \UnderflowException if the Deque is empty
61+
*/
62+
public function shift(): mixed {}
63+
64+
/**
65+
* Peeks at the value at the start of the Deque.
66+
* @throws \UnderflowException if the Deque is empty
67+
*/
68+
public function first(): mixed {}
69+
/**
70+
* Peeks at the value at the end of the Deque.
71+
* @throws \UnderflowException if the Deque is empty
72+
*/
73+
public function last(): mixed {}
74+
75+
/**
76+
* Returns a list of the elements from the start to the end.
77+
*/
78+
public function toArray(): array {}
79+
80+
/**
81+
* Insert 0 or more values at the given offset of the Deque.
82+
* @throws \OutOfBoundsException if the value of $offset is not within the bounds of this Deque.
83+
*/
84+
public function insert(int $offset, mixed ...$values): void {}
85+
// Must be mixed for compatibility with ArrayAccess
86+
/**
87+
* Returns the value at offset (int)$offset (relative to the start of the Deque)
88+
* @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this Deque.
89+
*/
90+
public function offsetGet(mixed $offset): mixed {}
91+
/**
92+
* Returns true if `0 <= (int)$offset && (int)$offset < $this->count()
93+
* AND the value at that offset is non-null.
94+
*/
95+
public function offsetExists(mixed $offset): bool {}
96+
/**
97+
* Sets the value at offset $offset (relative to the start of the Deque) to $value
98+
* @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this Deque.
99+
*/
100+
public function offsetSet(mixed $offset, mixed $value): void {}
101+
/**
102+
* Removes the value at (int)$offset from the deque.
103+
* @throws \OutOfBoundsException if the value of (int)$offset is not within the bounds of this Deque.
104+
*/
105+
public function offsetUnset(mixed $offset): void {}
106+
107+
/**
108+
* This is JSON serialized as a JSON array with elements from the start to the end.
109+
* @implementation-alias Collections\Deque::toArray
110+
*/
111+
public function jsonSerialize(): array {}
112+
}

ext/collections/collections_deque_arginfo.h

Lines changed: 129 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
+----------------------------------------------------------------------+
3+
| Copyright (c) The PHP Group |
4+
+----------------------------------------------------------------------+
5+
| This source file is subject to version 3.01 of the PHP license, |
6+
| that is bundled with this package in the file LICENSE, and is |
7+
| available through the world-wide-web at the following url: |
8+
| https://www.php.net/license/3_01.txt |
9+
| If you did not receive a copy of the PHP license and are unable to |
10+
| obtain it through the world-wide-web, please send a note to |
11+
| license@php.net so we can mail you a copy immediately. |
12+
+----------------------------------------------------------------------+
13+
| Author: Tyson Andre <tandre@php.net> |
14+
+----------------------------------------------------------------------+
15+
*/
16+
17+
#ifndef COLLECTIONS_INTERNALITERATOR_H
18+
#define COLLECTIONS_INTERNALITERATOR_H
19+
20+
typedef struct _collections_intrusive_dllist_node {
21+
struct _collections_intrusive_dllist_node *prev;
22+
struct _collections_intrusive_dllist_node *next;
23+
} collections_intrusive_dllist_node;
24+
25+
typedef struct _collections_intrusive_dllist {
26+
struct _collections_intrusive_dllist_node *first;
27+
} collections_intrusive_dllist;
28+
29+
static zend_always_inline void collections_intrusive_dllist_prepend(collections_intrusive_dllist *list, collections_intrusive_dllist_node *node) {
30+
collections_intrusive_dllist_node *first = list->first;
31+
ZEND_ASSERT(node != first);
32+
node->next = first;
33+
node->prev = NULL;
34+
list->first = node;
35+
36+
if (first) {
37+
ZEND_ASSERT(first->prev == NULL);
38+
first->prev = node;
39+
}
40+
}
41+
42+
static zend_always_inline void collections_intrusive_dllist_remove(collections_intrusive_dllist *list, const collections_intrusive_dllist_node *node) {
43+
collections_intrusive_dllist_node *next = node->next;
44+
collections_intrusive_dllist_node *prev = node->prev;
45+
ZEND_ASSERT(node != next);
46+
ZEND_ASSERT(node != prev);
47+
ZEND_ASSERT(next != prev || next == NULL);
48+
if (next) {
49+
next->prev = prev;
50+
}
51+
if (list->first == node) {
52+
list->first = next;
53+
ZEND_ASSERT(prev == NULL);
54+
} else if (prev) {
55+
prev->next = next;
56+
}
57+
}
58+
#endif

ext/collections/collections_util.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
+----------------------------------------------------------------------+
3+
| Copyright (c) The PHP Group |
4+
+----------------------------------------------------------------------+
5+
| This source file is subject to version 3.01 of the PHP license, |
6+
| that is bundled with this package in the file LICENSE, and is |
7+
| available through the world-wide-web at the following url: |
8+
| https://www.php.net/license/3_01.txt |
9+
| If you did not receive a copy of the PHP license and are unable to |
10+
| obtain it through the world-wide-web, please send a note to |
11+
| license@php.net so we can mail you a copy immediately. |
12+
+----------------------------------------------------------------------+
13+
| Author: Tyson Andre <tandre@php.net> |
14+
+----------------------------------------------------------------------+
15+
*/
16+
17+
#include "collections_util.h"
18+
19+
/* Override get_properties_for and use the default implementation of get_properties. See https://github.com/php/php-src/issues/9697#issuecomment-1273613175 */
20+
HashTable* collections_noop_empty_array_get_properties_for(zend_object *obj, zend_prop_purpose purpose) {
21+
(void)obj;
22+
(void)purpose;
23+
return NULL;
24+
}
25+
26+
HashTable* collections_noop_get_gc(zend_object *obj, zval **table, int *n) {
27+
/* Zend/zend_gc.c does not initialize table or n. So we need to set n to 0 at minimum. */
28+
*n = 0;
29+
(void) table;
30+
(void) obj;
31+
/* Nothing needs to be garbage collected */
32+
return NULL;
33+
}
34+
35+
HashTable *collections_internaliterator_get_gc(zend_object_iterator *iter, zval **table, int *n)
36+
{
37+
*table = &iter->data;
38+
*n = 1;
39+
return NULL;
40+
}
41+

0 commit comments

Comments
 (0)