Skip to content

Commit

Permalink
Improved collections performance in common use cases
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lorenzo committed Jun 24, 2017
1 parent 67d6b55 commit 598e74a
Show file tree
Hide file tree
Showing 7 changed files with 479 additions and 86 deletions.
49 changes: 35 additions & 14 deletions src/Collection/CollectionTrait.php
Expand Up @@ -47,7 +47,7 @@ trait CollectionTrait
*/
public function each(callable $c)
{
foreach ($this->unwrap() as $k => $v) {
foreach ($this->optimizeUnwrap() as $k => $v) {
$c($v, $k);
}

Expand Down Expand Up @@ -87,7 +87,7 @@ public function reject(callable $c)
*/
public function every(callable $c)
{
foreach ($this->unwrap() as $key => $value) {
foreach ($this->optimizeUnwrap() as $key => $value) {
if (!$c($value, $key)) {
return false;
}
Expand All @@ -101,7 +101,7 @@ public function every(callable $c)
*/
public function some(callable $c)
{
foreach ($this->unwrap() as $key => $value) {
foreach ($this->optimizeUnwrap() as $key => $value) {
if ($c($value, $key) === true) {
return true;
}
Expand All @@ -115,7 +115,7 @@ public function some(callable $c)
*/
public function contains($value)
{
foreach ($this->unwrap() as $v) {
foreach ($this->optimizeUnwrap() as $v) {
if ($value === $v) {
return true;
}
Expand Down Expand Up @@ -145,7 +145,7 @@ public function reduce(callable $c, $zero = null)
}

$result = $zero;
foreach ($this->unwrap() as $k => $value) {
foreach ($this->optimizeUnwrap() as $k => $value) {
if ($isFirst) {
$result = $value;
$isFirst = false;
Expand Down Expand Up @@ -205,7 +205,7 @@ public function groupBy($callback)
{
$callback = $this->_propertyExtractor($callback);
$group = [];
foreach ($this as $value) {
foreach ($this->optimizeUnwrap() as $value) {
$group[$callback($value)][] = $value;
}

Expand All @@ -219,7 +219,7 @@ public function indexBy($callback)
{
$callback = $this->_propertyExtractor($callback);
$group = [];
foreach ($this as $value) {
foreach ($this->optimizeUnwrap() as $value) {
$group[$callback($value)] = $value;
}

Expand Down Expand Up @@ -255,7 +255,7 @@ public function sumOf($matcher = null)

$callback = $this->_propertyExtractor($matcher);
$sum = 0;
foreach ($this as $k => $v) {
foreach ($this->optimizeUnwrap() as $k => $v) {
$sum += $callback($v, $k);
}

Expand Down Expand Up @@ -500,7 +500,7 @@ public function compile($preserveKeys = true)
*/
public function buffered()
{
return new BufferedIterator($this);
return new BufferedIterator($this->unwrap());
}

/**
Expand Down Expand Up @@ -534,7 +534,7 @@ public function stopWhen($condition)
$condition = $this->_createMatcherFilter($condition);
}

return new StoppableIterator($this, $condition);
return new StoppableIterator($this->unwrap(), $condition);
}

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

return new Collection(
new RecursiveIteratorIterator(
new UnfoldIterator($this, $transformer),
new UnfoldIterator($this->unwrap(), $transformer),
RecursiveIteratorIterator::LEAVES_ONLY
)
);
Expand All @@ -571,7 +571,7 @@ public function through(callable $handler)
*/
public function zip($items)
{
return new ZipIterator(array_merge([$this], func_get_args()));
return new ZipIterator(array_merge([$this->unwrap()], func_get_args()));
}

/**
Expand All @@ -586,7 +586,7 @@ public function zipWith($items, $callable)
$items = [$items];
}

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

/**
Expand Down Expand Up @@ -640,7 +640,7 @@ public function chunkWithKeys($chunkSize, $preserveKeys = true)
*/
public function isEmpty()
{
foreach ($this->unwrap() as $el) {
foreach ($this as $el) {
return false;
}

Expand All @@ -657,6 +657,10 @@ public function unwrap()
$iterator = $iterator->getInnerIterator();
}

if ($iterator !== $this && $iterator instanceof CollectionInterface) {
$iterator = $iterator->unwrap();
}

return $iterator;
}

Expand Down Expand Up @@ -747,4 +751,21 @@ public function transpose()

return new Collection($result);
}

/**
* Unwraps this iterator and returns the simplest
* traversable that can be used for getting the data out
*
* @return \Traversable|array
*/
protected function optimizeUnwrap()
{
$iterator = $this->unwrap();

if ($iterator instanceof ArrayIterator) {
$iterator = $iterator->getArrayCopy();
}

return $iterator;
}
}
35 changes: 35 additions & 0 deletions src/Collection/Iterator/ExtractIterator.php
Expand Up @@ -14,7 +14,9 @@
*/
namespace Cake\Collection\Iterator;

use ArrayIterator;
use Cake\Collection\Collection;
use Cake\Collection\CollectionInterface;

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

return $extractor(parent::current());
}

/**
* {@inheritDoc}
*
* We perform here some stricness analysys so that the
* iterator logic is bypassed entirely.
*
* @return \Iterator
*/
public function unwrap()
{
$iterator = $this->getInnerIterator();

if ($iterator instanceof CollectionInterface) {
$iterator = $iterator->unwrap();
}

if (!$iterator instanceof ArrayIterator) {
return $this;
}

// ArrayIterator can be traversed strictly.
// Let's do that for performance gains

$callback = $this->_extractor;
$res = [];

foreach ($iterator->getArrayCopy() as $k => $v) {
$res[$k] = $callback($v);
}

return new ArrayIterator($res);
}
}
52 changes: 51 additions & 1 deletion src/Collection/Iterator/FilterIterator.php
Expand Up @@ -14,7 +14,9 @@
*/
namespace Cake\Collection\Iterator;

use ArrayIterator;
use Cake\Collection\Collection;
use Cake\Collection\CollectionInterface;
use CallbackFilterIterator;
use Iterator;

Expand All @@ -26,6 +28,13 @@
class FilterIterator extends Collection
{

/**
* The callback used to filter the elements in this collection
*
* @var callable
*/
protected $_callback;

/**
* Creates a filtered iterator using the callback to determine which items are
* accepted or rejected.
Expand All @@ -37,9 +46,50 @@ class FilterIterator extends Collection
* @param \Iterator $items The items to be filtered.
* @param callable $callback Callback.
*/
public function __construct(Iterator $items, callable $callback)
public function __construct($items, callable $callback)
{
if (!$items instanceof Iterator) {
$items = new Collection($items);
}

$this->_callback = $callback;
$wrapper = new CallbackFilterIterator($items, $callback);
parent::__construct($wrapper);
}

/**
* {@inheritDoc}
*
* We perform here some stricness analysys so that the
* iterator logic is bypassed entirely.
*
* @return \Iterator
*/
public function unwrap()
{
$filter = $this->getInnerIterator();
$iterator = $filter->getInnerIterator();

if ($iterator instanceof CollectionInterface) {
$iterator = $iterator->unwrap();
}

if (!$iterator instanceof ArrayIterator) {
return $filter;
}

// ArrayIterator can be traversed strictly.
// Let's do that for performance gains

$callback = $this->_callback;
$res = [];

foreach ($iterator as $k => $v) {
if ($callback($v, $k, $iterator)) {
$res[$k] = $v;
}
}

return new ArrayIterator($res);
}
}
38 changes: 37 additions & 1 deletion src/Collection/Iterator/ReplaceIterator.php
Expand Up @@ -14,7 +14,9 @@
*/
namespace Cake\Collection\Iterator;

use ArrayIterator;
use Cake\Collection\Collection;
use Cake\Collection\CollectionInterface;

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

/**
* The callback function to be used to modify each of the values
* The callback function to be used to transform values
*
* @var callable
*/
Expand Down Expand Up @@ -67,4 +69,38 @@ public function current()

return $callback(parent::current(), $this->key(), $this->_innerIterator);
}


/**
* {@inheritDoc}
*
* We perform here some stricness analysys so that the
* iterator logic is bypassed entirely.
*
* @return \Iterator
*/
public function unwrap()
{
$iterator = $this->_innerIterator;

if ($iterator instanceof CollectionInterface) {
$iterator = $iterator->unwrap();
}

if (!$iterator instanceof ArrayIterator) {
return $this;
}

// ArrayIterator can be traversed strictly.
// Let's do that for performance gains

$callback = $this->_callback;
$res = [];

foreach ($iterator as $k => $v) {
$res[$k] = $callback($v, $k, $iterator);
}

return new ArrayIterator($res);
}
}
15 changes: 12 additions & 3 deletions src/Collection/Iterator/SortIterator.php
Expand Up @@ -59,11 +59,10 @@ class SortIterator extends Collection
*/
public function __construct($items, $callback, $dir = SORT_DESC, $type = SORT_NUMERIC)
{
if (is_array($items)) {
$items = new Collection($items);
if (!is_array($items)) {
$items = iterator_to_array((new Collection($items))->unwrap(), false);
}

$items = iterator_to_array($items, false);
$callback = $this->_propertyExtractor($callback);
$results = [];
foreach ($items as $key => $value) {
Expand All @@ -81,4 +80,14 @@ public function __construct($items, $callback, $dir = SORT_DESC, $type = SORT_NU
}
parent::__construct($results);
}

/**
* {@inheritDoc}
*
* @return \Iterator
*/
public function unwrap()
{
return $this->getInnerIterator();
}
}

0 comments on commit 598e74a

Please sign in to comment.