While doing some profiling and benchmarking of Magento to test PHP-FPM I found that Magento was spending roughly a quarter of the run time for some page loads on comparing attributes.
This struck me as odd, and so I looked into it more and found that it was specifically on pages with several products (like category pages). Digging into the code, I found this (in app/code/core/Mage/Eav/Model/Entity/Abstract.php
):
[php]
//in the getSortedAttributes method
uasort($attributes, array($this, ‘attributesCompare’));
//then we have
public function attributesCompare($attribute1, $attribute2)
{
$sortPath = sprintf(‘attribute_set_info/%s/sort’, $this->_sortingSetId);
$groupSortPath = sprintf(‘attribute_set_info/%s/group_sort’, $this->_sortingSetId);
$sort1 = ($attribute1->getData($groupSortPath) * 1000) + ($attribute1->getData($sortPath) * 0.0001);
$sort2 = ($attribute2->getData($groupSortPath) * 1000) + ($attribute2->getData($sortPath) * 0.0001);
if ($sort1 > $sort2) {
return 1;
} elseif ($sort1 < $sort2) {
return -1;
}
return 0;
}
[/php]
This seems pretty straightforward but I wondered why Magento was spending so much time on it, so I looked into what sort algorithm PHP uses for uasort
which lead me to the sort
page which states that PHP uses Quicksort for most sort operations. Quicksort is good sort algorithm but the problem in this context is that each comparison requires 4 getData
calls and some (simple) math, but each element can be compared multiple times so it wastes time doing the exact same operation if it looks at an element more than once.
Since we know that every element will be compared at least once, and that some will be compared multiple times the solution is to pre-compute the comparison values and then sort based on those. This patch does exactly that:
[patch]— magento.orig/app/code/core/Mage/Eav/Model/Entity/Abstract.php 2011-07-26 05:39:19.000000000 -0400
+++ magento/app/code/core/Mage/Eav/Model/Entity/Abstract.php 2012-04-05 15:44:18.012612944 -0400
@@ -549,16 +549,24 @@
Mage::getSingleton(‘eav/entity_attribute_set’)
->addSetInfo($this->getEntityType(), $attributes, $setId);
+ $sortList = array();
+ $sortPath = sprintf(‘attribute_set_info/%s/sort’, $setId);
+ $groupSortPath = sprintf(‘attribute_set_info/%s/group_sort’, $setId);
foreach ($attributes as $code => $attribute) {
/* @var $attribute Mage_Eav_Model_Entity_Attribute_Abstract */
if (!$attribute->isInSet($setId)) {
unset($attributes[$code]);
+ } else {
+ $sortList[$code] = array($attribute,
+ ($attribute->getData($groupSortPath) * 1000) +
+ ($attribute->getData($sortPath) * 0.0001));
}
}
$this->_sortingSetId = $setId;
– uasort($attributes, array($this, ‘attributesCompare’));
– return $attributes;
+ uasort($sortList, create_function(‘$a,$b’,
+ ‘return $a[1] > $b[1] ? ($a[1] < $b[1] ? -1 : 0) : 1;’));
+ return array_map(create_function(‘$a’, ‘return $a[0];’), $sortList);
}
/**[/patch]
Disclaimer: I have not rigorously tested this patch. It is very simple and doesn’t change the return value of the getSortedAttributes
method so it shouldn’t cause any problems but use is at your own risk.
In the regular Magento sample data I was testing against this resulted in a run time reduction of ~1/10 of a second (0.3s to 0.2s) on the /apparel.html
category page. This doesn’t sound like much but that page takes about 3 times longer to load than other pages, and on a production site with real data the time savings could be significant since it affects pages with lots of products. For reference, here is the call time graph with the patch (compare the highlighted area to the unpatched graph above):
In other terms, this results in reliable a ~2 transaction/second increase on the Magento Speed Test with the sample data.