Skip to content

Commit 598e74a

Browse files
committed
Improved collections performance in common use cases
This improves the performance of most collection methods sometimes by making it 100% faster compared to the previous implementation. The improvement is not for free, with this change we are giving up the lazyness feature (not iterate unless requested). The giving up of the feature only happens when the collection has been initialized with an array, as oposed to being initialized with another iterator or generator; and this is the reson I consider this change a safe one. For the curious, the improvement comes from the (sad) fact that calling functions in php is extremenly expensive, specially when using iterators since each iteration will call at least 4 functions (valid, next, current, key). This becomes even worse as `IteratorIterator` does not have any optimizations, so for each wrapped iterator, the number of functions is multiplied by 2 for each iteration. The change proposed here will unwrap nested iterators as much as possible to avoid the function call explosion, and in some (safe) cases, will iterator the collection immediately as an array before wrapping it again in an iterator. I was inspired by Haskell when implementing this, as the language is lazy by default, but the compiler optimizes the cases where code is safe to be called strictly. Thats is called strictness analysis.
1 parent 67d6b55 commit 598e74a

File tree

7 files changed

+479
-86
lines changed

7 files changed

+479
-86
lines changed

src/Collection/CollectionTrait.php

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ trait CollectionTrait
4747
*/
4848
public function each(callable $c)
4949
{
50-
foreach ($this->unwrap() as $k => $v) {
50+
foreach ($this->optimizeUnwrap() as $k => $v) {
5151
$c($v, $k);
5252
}
5353

@@ -87,7 +87,7 @@ public function reject(callable $c)
8787
*/
8888
public function every(callable $c)
8989
{
90-
foreach ($this->unwrap() as $key => $value) {
90+
foreach ($this->optimizeUnwrap() as $key => $value) {
9191
if (!$c($value, $key)) {
9292
return false;
9393
}
@@ -101,7 +101,7 @@ public function every(callable $c)
101101
*/
102102
public function some(callable $c)
103103
{
104-
foreach ($this->unwrap() as $key => $value) {
104+
foreach ($this->optimizeUnwrap() as $key => $value) {
105105
if ($c($value, $key) === true) {
106106
return true;
107107
}
@@ -115,7 +115,7 @@ public function some(callable $c)
115115
*/
116116
public function contains($value)
117117
{
118-
foreach ($this->unwrap() as $v) {
118+
foreach ($this->optimizeUnwrap() as $v) {
119119
if ($value === $v) {
120120
return true;
121121
}
@@ -145,7 +145,7 @@ public function reduce(callable $c, $zero = null)
145145
}
146146

147147
$result = $zero;
148-
foreach ($this->unwrap() as $k => $value) {
148+
foreach ($this->optimizeUnwrap() as $k => $value) {
149149
if ($isFirst) {
150150
$result = $value;
151151
$isFirst = false;
@@ -205,7 +205,7 @@ public function groupBy($callback)
205205
{
206206
$callback = $this->_propertyExtractor($callback);
207207
$group = [];
208-
foreach ($this as $value) {
208+
foreach ($this->optimizeUnwrap() as $value) {
209209
$group[$callback($value)][] = $value;
210210
}
211211

@@ -219,7 +219,7 @@ public function indexBy($callback)
219219
{
220220
$callback = $this->_propertyExtractor($callback);
221221
$group = [];
222-
foreach ($this as $value) {
222+
foreach ($this->optimizeUnwrap() as $value) {
223223
$group[$callback($value)] = $value;
224224
}
225225

@@ -255,7 +255,7 @@ public function sumOf($matcher = null)
255255

256256
$callback = $this->_propertyExtractor($matcher);
257257
$sum = 0;
258-
foreach ($this as $k => $v) {
258+
foreach ($this->optimizeUnwrap() as $k => $v) {
259259
$sum += $callback($v, $k);
260260
}
261261

@@ -500,7 +500,7 @@ public function compile($preserveKeys = true)
500500
*/
501501
public function buffered()
502502
{
503-
return new BufferedIterator($this);
503+
return new BufferedIterator($this->unwrap());
504504
}
505505

506506
/**
@@ -534,7 +534,7 @@ public function stopWhen($condition)
534534
$condition = $this->_createMatcherFilter($condition);
535535
}
536536

537-
return new StoppableIterator($this, $condition);
537+
return new StoppableIterator($this->unwrap(), $condition);
538538
}
539539

540540
/**
@@ -550,7 +550,7 @@ public function unfold(callable $transformer = null)
550550

551551
return new Collection(
552552
new RecursiveIteratorIterator(
553-
new UnfoldIterator($this, $transformer),
553+
new UnfoldIterator($this->unwrap(), $transformer),
554554
RecursiveIteratorIterator::LEAVES_ONLY
555555
)
556556
);
@@ -571,7 +571,7 @@ public function through(callable $handler)
571571
*/
572572
public function zip($items)
573573
{
574-
return new ZipIterator(array_merge([$this], func_get_args()));
574+
return new ZipIterator(array_merge([$this->unwrap()], func_get_args()));
575575
}
576576

577577
/**
@@ -586,7 +586,7 @@ public function zipWith($items, $callable)
586586
$items = [$items];
587587
}
588588

589-
return new ZipIterator(array_merge([$this], $items), $callable);
589+
return new ZipIterator(array_merge([$this->unwrap()], $items), $callable);
590590
}
591591

592592
/**
@@ -640,7 +640,7 @@ public function chunkWithKeys($chunkSize, $preserveKeys = true)
640640
*/
641641
public function isEmpty()
642642
{
643-
foreach ($this->unwrap() as $el) {
643+
foreach ($this as $el) {
644644
return false;
645645
}
646646

@@ -657,6 +657,10 @@ public function unwrap()
657657
$iterator = $iterator->getInnerIterator();
658658
}
659659

660+
if ($iterator !== $this && $iterator instanceof CollectionInterface) {
661+
$iterator = $iterator->unwrap();
662+
}
663+
660664
return $iterator;
661665
}
662666

@@ -747,4 +751,21 @@ public function transpose()
747751

748752
return new Collection($result);
749753
}
754+
755+
/**
756+
* Unwraps this iterator and returns the simplest
757+
* traversable that can be used for getting the data out
758+
*
759+
* @return \Traversable|array
760+
*/
761+
protected function optimizeUnwrap()
762+
{
763+
$iterator = $this->unwrap();
764+
765+
if ($iterator instanceof ArrayIterator) {
766+
$iterator = $iterator->getArrayCopy();
767+
}
768+
769+
return $iterator;
770+
}
750771
}

src/Collection/Iterator/ExtractIterator.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
*/
1515
namespace Cake\Collection\Iterator;
1616

17+
use ArrayIterator;
1718
use Cake\Collection\Collection;
19+
use Cake\Collection\CollectionInterface;
1820

1921
/**
2022
* Creates an iterator from another iterator that extract the requested column
@@ -69,4 +71,37 @@ public function current()
6971

7072
return $extractor(parent::current());
7173
}
74+
75+
/**
76+
* {@inheritDoc}
77+
*
78+
* We perform here some stricness analysys so that the
79+
* iterator logic is bypassed entirely.
80+
*
81+
* @return \Iterator
82+
*/
83+
public function unwrap()
84+
{
85+
$iterator = $this->getInnerIterator();
86+
87+
if ($iterator instanceof CollectionInterface) {
88+
$iterator = $iterator->unwrap();
89+
}
90+
91+
if (!$iterator instanceof ArrayIterator) {
92+
return $this;
93+
}
94+
95+
// ArrayIterator can be traversed strictly.
96+
// Let's do that for performance gains
97+
98+
$callback = $this->_extractor;
99+
$res = [];
100+
101+
foreach ($iterator->getArrayCopy() as $k => $v) {
102+
$res[$k] = $callback($v);
103+
}
104+
105+
return new ArrayIterator($res);
106+
}
72107
}

src/Collection/Iterator/FilterIterator.php

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
*/
1515
namespace Cake\Collection\Iterator;
1616

17+
use ArrayIterator;
1718
use Cake\Collection\Collection;
19+
use Cake\Collection\CollectionInterface;
1820
use CallbackFilterIterator;
1921
use Iterator;
2022

@@ -26,6 +28,13 @@
2628
class FilterIterator extends Collection
2729
{
2830

31+
/**
32+
* The callback used to filter the elements in this collection
33+
*
34+
* @var callable
35+
*/
36+
protected $_callback;
37+
2938
/**
3039
* Creates a filtered iterator using the callback to determine which items are
3140
* accepted or rejected.
@@ -37,9 +46,50 @@ class FilterIterator extends Collection
3746
* @param \Iterator $items The items to be filtered.
3847
* @param callable $callback Callback.
3948
*/
40-
public function __construct(Iterator $items, callable $callback)
49+
public function __construct($items, callable $callback)
4150
{
51+
if (!$items instanceof Iterator) {
52+
$items = new Collection($items);
53+
}
54+
55+
$this->_callback = $callback;
4256
$wrapper = new CallbackFilterIterator($items, $callback);
4357
parent::__construct($wrapper);
4458
}
59+
60+
/**
61+
* {@inheritDoc}
62+
*
63+
* We perform here some stricness analysys so that the
64+
* iterator logic is bypassed entirely.
65+
*
66+
* @return \Iterator
67+
*/
68+
public function unwrap()
69+
{
70+
$filter = $this->getInnerIterator();
71+
$iterator = $filter->getInnerIterator();
72+
73+
if ($iterator instanceof CollectionInterface) {
74+
$iterator = $iterator->unwrap();
75+
}
76+
77+
if (!$iterator instanceof ArrayIterator) {
78+
return $filter;
79+
}
80+
81+
// ArrayIterator can be traversed strictly.
82+
// Let's do that for performance gains
83+
84+
$callback = $this->_callback;
85+
$res = [];
86+
87+
foreach ($iterator as $k => $v) {
88+
if ($callback($v, $k, $iterator)) {
89+
$res[$k] = $v;
90+
}
91+
}
92+
93+
return new ArrayIterator($res);
94+
}
4595
}

src/Collection/Iterator/ReplaceIterator.php

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
*/
1515
namespace Cake\Collection\Iterator;
1616

17+
use ArrayIterator;
1718
use Cake\Collection\Collection;
19+
use Cake\Collection\CollectionInterface;
1820

1921
/**
2022
* Creates an iterator from another iterator that will modify each of the values
@@ -24,7 +26,7 @@ class ReplaceIterator extends Collection
2426
{
2527

2628
/**
27-
* The callback function to be used to modify each of the values
29+
* The callback function to be used to transform values
2830
*
2931
* @var callable
3032
*/
@@ -67,4 +69,38 @@ public function current()
6769

6870
return $callback(parent::current(), $this->key(), $this->_innerIterator);
6971
}
72+
73+
74+
/**
75+
* {@inheritDoc}
76+
*
77+
* We perform here some stricness analysys so that the
78+
* iterator logic is bypassed entirely.
79+
*
80+
* @return \Iterator
81+
*/
82+
public function unwrap()
83+
{
84+
$iterator = $this->_innerIterator;
85+
86+
if ($iterator instanceof CollectionInterface) {
87+
$iterator = $iterator->unwrap();
88+
}
89+
90+
if (!$iterator instanceof ArrayIterator) {
91+
return $this;
92+
}
93+
94+
// ArrayIterator can be traversed strictly.
95+
// Let's do that for performance gains
96+
97+
$callback = $this->_callback;
98+
$res = [];
99+
100+
foreach ($iterator as $k => $v) {
101+
$res[$k] = $callback($v, $k, $iterator);
102+
}
103+
104+
return new ArrayIterator($res);
105+
}
70106
}

src/Collection/Iterator/SortIterator.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,10 @@ class SortIterator extends Collection
5959
*/
6060
public function __construct($items, $callback, $dir = SORT_DESC, $type = SORT_NUMERIC)
6161
{
62-
if (is_array($items)) {
63-
$items = new Collection($items);
62+
if (!is_array($items)) {
63+
$items = iterator_to_array((new Collection($items))->unwrap(), false);
6464
}
6565

66-
$items = iterator_to_array($items, false);
6766
$callback = $this->_propertyExtractor($callback);
6867
$results = [];
6968
foreach ($items as $key => $value) {
@@ -81,4 +80,14 @@ public function __construct($items, $callback, $dir = SORT_DESC, $type = SORT_NU
8180
}
8281
parent::__construct($results);
8382
}
83+
84+
/**
85+
* {@inheritDoc}
86+
*
87+
* @return \Iterator
88+
*/
89+
public function unwrap()
90+
{
91+
return $this->getInnerIterator();
92+
}
8493
}

0 commit comments

Comments
 (0)