diff --git a/README.md b/README.md index 9633b2ce5..c3112a3ff 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,16 @@ class for different scopes like: - string wrappers; - etc. +## Benchmarks + +We provided some benchmarks for zend-stdlib in the folder [/benchmark](/benchmark). +We used [athletic](https://github.com/polyfractal/athletic) framework to write the benchmark tests. +You can execute the benchmarks running the following command: + +``` +vendor/bin/athletic -p benchmark +``` + - - File issues at https://github.com/zendframework/zend-stdlib/issues - Documentation is at http://framework.zend.com/manual/current/en/index.html#zend-stdlib diff --git a/benchmark/PriorityQueue.php b/benchmark/PriorityQueue.php new file mode 100644 index 000000000..ef0b5afab --- /dev/null +++ b/benchmark/PriorityQueue.php @@ -0,0 +1,69 @@ +splPriorityQueue = new \Zend\Stdlib\SplPriorityQueue(); + $this->fastPriorityQueue = new \Zend\Stdlib\FastPriorityQueue(); + $this->priorityQueue = new \Zend\Stdlib\PriorityQueue(); + + for($i=0; $i<5000; $i++) { + $priority = rand(1,100); + $this->splPriorityQueue->insert('foo', $priority); + $this->fastPriorityQueue->insert('foo', $priority); + $this->priorityQueue->insert('foo', $priority); + } + } + + /** + * @iterations 5000 + */ + public function insertSplPriorityQueue() + { + $this->splPriorityQueue->insert('foo', rand(1,100)); + } + + /** + * @iterations 5000 + */ + public function extractSplPriorityQueue() + { + $this->splPriorityQueue->extract(); + } + + /** + * @iterations 5000 + */ + public function insertPriorityQueue() + { + $this->priorityQueue->insert('foo', rand(1,100)); + } + + /** + * @iterations 5000 + */ + public function extractPriorityQueue() + { + $this->priorityQueue->extract(); + } + + /** + * @iterations 5000 + */ + public function insertFastPriorityQueue() + { + $this->fastPriorityQueue->insert('foo', rand(1,100)); + } + + /** + * @iterations 5000 + */ + public function extractFastPriorityQueue() + { + $this->fastPriorityQueue->extract(); + } +} diff --git a/composer.json b/composer.json index 056ce3f59..ee6fd3505 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "zendframework/zend-servicemanager": "~2.5", "zendframework/zend-filter": "~2.5", "fabpot/php-cs-fixer": "1.7.*", - "phpunit/PHPUnit": "~4.0" + "phpunit/PHPUnit": "~4.0", + "athletic/athletic": "~0.1" }, "suggest": { "zendframework/zend-eventmanager": "To support aggregate hydrator usage", @@ -41,7 +42,8 @@ }, "autoload-dev": { "psr-4": { - "ZendTest\\Stdlib\\": "test/" + "ZendTest\\Stdlib\\": "test/", + "ZendBench\\Stdlib\\": "benchmark/" } } } diff --git a/src/FastPriorityQueue.php b/src/FastPriorityQueue.php new file mode 100644 index 000000000..b9388aa3d --- /dev/null +++ b/src/FastPriorityQueue.php @@ -0,0 +1,298 @@ +values[$priority][] = $value; + if (!isset($this->priorities[$priority])) { + $this->priorities[$priority] = $priority; + $this->max = max($priority, $this->max); + } + ++$this->tot; + } + + /** + * Extract an element in the queue according to the priority and the + * order of insertion + * + * @return mixed + */ + public function extract() + { + if (!$this->valid()) { + return false; + } + $value = $this->current(); + $this->nextAndRemove(); + return $value; + } + + /** + * Get the total number of elements in the queue + * + * @return integer + */ + public function count() + { + return $this->tot; + } + + /** + * Get the current element in the queue + * + * @return mixed + */ + public function current() + { + switch ($this->extractFlag) { + case self::EXTR_DATA: + return current($this->values[$this->max]); + case self::EXTR_PRIORITY: + return $this->max; + case self::EXTR_BOTH: + return [ + 'data' => current($this->values[$this->max]), + 'priority' => $this->max + ]; + } + } + + /** + * Get the index of the current element in the queue + * + * @return integer + */ + public function key() + { + return $this->index; + } + + /** + * Set the iterator pointer to the next element in the queue + * removing the previous element + */ + protected function nextAndRemove() + { + if (false === next($this->values[$this->max])) { + unset($this->priorities[$this->max]); + unset($this->values[$this->max]); + $this->max = empty($this->priorities) ? 0 : max($this->priorities); + } + ++$this->index; + --$this->tot; + } + + /** + * Set the iterator pointer to the next element in the queue + * without removing the previous element + */ + public function next() + { + if (false === next($this->values[$this->max])) { + unset($this->subPriorities[$this->max]); + $this->max = empty($this->subPriorities) ? 0 : max($this->subPriorities); + } + ++$this->index; + --$this->tot; + } + + /** + * Check if the current iterator is valid + * + * @return boolean + */ + public function valid() + { + return isset($this->values[$this->max]); + } + + /** + * Rewind the current iterator + */ + public function rewind() + { + $this->subPriorities = $this->priorities; + } + + /** + * Serialize to an array + * + * Array will be priority => data pairs + * + * @return array + */ + public function toArray() + { + $array = []; + foreach (clone $this as $item) { + $array[] = $item; + } + return $array; + } + + /** + * Serialize + * + * @return string + */ + public function serialize() + { + $clone = clone $this; + $clone->setExtractFlags(self::EXTR_BOTH); + + $data = []; + foreach ($clone as $item) { + $data[] = $item; + } + + return serialize($data); + } + + /** + * Deserialize + * + * @param string $data + * @return void + */ + public function unserialize($data) + { + foreach (unserialize($data) as $item) { + $this->insert($item['data'], $item['priority']); + } + } + + /** + * Set the extract flag + * + * @param integer $flag + */ + public function setExtractFlags($flag) + { + switch ($flag) { + case self::EXTR_DATA: + case self::EXTR_PRIORITY: + case self::EXTR_BOTH: + $this->extractFlag = $flag; + break; + } + } + + /** + * Check if the queue is empty + * + * @return boolean + */ + public function isEmpty() + { + return empty($this->values); + } + + /** + * Does the queue contain the given datum? + * + * @param mixed $datum + * @return bool + */ + public function contains($datum) + { + foreach ($this->values as $values) { + if (in_array($datum, $values)) { + return true; + } + } + return false; + } + + /** + * Does the queue have an item with the given priority? + * + * @param int $priority + * @return bool + */ + public function hasPriority($priority) + { + return isset($this->values[$priority]); + } +} diff --git a/test/FastPriorityQueueTest.php b/test/FastPriorityQueueTest.php new file mode 100644 index 000000000..e7f89e695 --- /dev/null +++ b/test/FastPriorityQueueTest.php @@ -0,0 +1,181 @@ +queue = new FastPriorityQueue(); + $this->insertDataQueue($this->queue); + $this->expected = [ + 'test1', + 'test2', + 'test3', + 'test4', + 'test5', + 'test6' + ]; + } + + protected function getDataPriorityQueue() + { + return [ + 'test3' => 2, + 'test5' => 1, + 'test1' => 5, + 'test2' => 3, + 'test4' => 2, + 'test6' => 1 + ]; + } + + protected function insertDataQueue($queue) + { + foreach ($this->getDataPriorityQueue() as $value => $priority) { + $queue->insert($value, $priority); + } + } + + /** + * Test the insert and extract operations for the queue + * We test that extract() function remove the elements + */ + public function testInsertExtract() + { + foreach ($this->expected as $value) { + $this->assertEquals($value, $this->queue->extract()); + } + // We check that the elements are removed from the queue + $this->assertTrue($this->queue->isEmpty()); + } + + public function testIteratePreserveElements() + { + $i = 0; + foreach ($this->queue as $value) { + $this->assertEquals($this->expected[$i++], $value); + } + // We check that the elements still exist in the queue + $i = 0; + foreach ($this->queue as $value) { + $this->assertEquals($this->expected[$i++], $value); + } + } + + public function testMaintainsInsertOrderForDataOfEqualPriority() + { + $queue = new FastPriorityQueue(); + $queue->insert('foo', 1000); + $queue->insert('bar', 1000); + $queue->insert('baz', 1000); + $queue->insert('bat', 1000); + + $expected = ['foo', 'bar', 'baz', 'bat']; + $test = []; + foreach ($queue as $datum) { + $test[] = $datum; + } + $this->assertEquals($expected, $test); + } + + public function testSerializationAndDeserializationShouldMaintainState() + { + $s = serialize($this->queue); + $unserialized = unserialize($s); + $count = count($this->queue); + $this->assertSame($count, count($unserialized), 'Expected count ' . $count . '; received ' . count($unserialized)); + + $expected = []; + foreach ($this->queue as $item) { + $expected[] = $item; + } + $test = []; + foreach ($unserialized as $item) { + $test[] = $item; + } + $this->assertSame($expected, $test, 'Expected: ' . var_export($expected, 1) . "\nReceived:" . var_export($test, 1)); + } + + public function testCanRetrieveQueueAsArray() + { + $test = $this->queue->toArray(); + $this->assertSame($this->expected, $test, var_export($test, 1)); + } + + public function testIteratorFunctions() + { + $this->queue->rewind(); + + $i = 0; + while ($this->queue->valid()) { + $key = $this->queue->key(); + $value = $this->queue->current(); + $this->assertEquals($this->expected[$i], $value); + $this->queue->next(); + ++$i; + } + $this->assertFalse($this->queue->valid()); + } + + public function testNoRewindOperation() + { + $this->assertEquals(0, $this->queue->key()); + $this->queue->next(); + $this->assertEquals(1, $this->queue->key()); + $this->queue->rewind(); + $this->assertEquals(1, $this->queue->key()); + } + + public function testSetExtractFlag() + { + $this->queue->setExtractFlags(FastPriorityQueue::EXTR_DATA); + $this->assertEquals($this->expected[0], $this->queue->extract()); + $this->queue->setExtractFlags(FastPriorityQueue::EXTR_PRIORITY); + $this->assertEquals(3, $this->queue->extract()); + $this->queue->setExtractFlags(FastPriorityQueue::EXTR_BOTH); + $expected = [ + 'data' => $this->expected[2], + 'priority' => 2 + ]; + $this->assertEquals($expected, $this->queue->extract()); + } + + public function testIsEmpty() + { + $queue = new FastPriorityQueue(); + $this->assertTrue($queue->isEmpty()); + $queue->insert('foo', 1); + $this->assertFalse($queue->isEmpty()); + $value = $queue->extract(); + $this->assertTrue($queue->isEmpty()); + } + + public function testContains() + { + foreach ($this->expected as $value) { + $this->assertTrue($this->queue->contains($value)); + } + $this->assertFalse($this->queue->contains('foo')); + } + + public function testHasPriority() + { + foreach ($this->getDataPriorityQueue() as $value => $priority) { + $this->assertTrue($this->queue->hasPriority($priority)); + } + $this->assertFalse($this->queue->hasPriority(10000)); + } +}