PHP Performance: Think out of the box

Coding CEO
3 min readMar 11, 2020

If you are a developer, more than once the user and/or product owner told you that some process was to slow.

When this occurs, most of the developers that I know take a bad approach and get lost in a road of micro-optimization and caches that don’t improve the performance too much. But what is really annoying for me is to listen “this process can not be improved more”.

I will put you two examples of thinking out of the box of increasing the performance of a process, and is really simple:

Don’t try to increase the performance by 30%-50%, jump to an increase of x10.

Example 1:

Each Order has a delivery estimated date, every night we check if the delivery company is delivering in time. This process takes 1.000 seconds over 1M orders.

This is the code written in Yii2.

/**
*
@param int $orderId
*
@throws ModelNotFoundException|ModelNotSavedException
*/
public function updateEstimateTimeIfNeeded(int $orderId): void
{
/** @var Order|null $order */
$order = Order::findOne($orderId);
if (null == $order) {
throw new ModelNotFoundException(Order::class, $orderId);
}
$deliveryCompany = DeliveryCompany::findOne($order->delivery_company_id);
if (null == $deliveryCompany) {
throw new ModelNotFoundException(DeliveryCompany::class, $order->delivery_company_id);
}
if ($deliveryCompany->hasDelays()) {
$order->estimate_delivery_date += $deliveryCompany->estimatedDelay();
$order->saveOrFail();
}
}

As you can see, for each order, we check if the delivery company has delays, and also we are calling twice the function, once to check if has delay and other for the delay. We may improve this with:

/**
*
@param int $orderId
*
@throws ModelNotFoundException|ModelNotSavedException
*/
public function updateEstimateTimeIfNeeded2(int $orderId): void
{
/** @var Order|null $order */
$order = Order::findOne($orderId);
if (null == $order) {
throw new ModelNotFoundException(Order::class, $orderId);
}
$delay = $this->hasDeliveryCompanyDelayCached($order->delivery_company_id);
if ($delay) {
$order->estimate_delivery_date += $delay;
$order->saveOrFail();
}
}

As we cached the call to the database and reduce the numbers of calls, we get a 35% performance increase (650 seconds), not bad?.

We can continue optimizing it more, for example, Yii2 can retrieve in the same query Order and DeliveryCompany, or we may retrieve only the delivery_company_id, etc…

All theses micro-optimizations can allow us to increase performance by 50% (500 seconds), but not more.

Let’s now forget this function and check the function that uses this function.

public function updateAllEstimateTimeIfNeeded()
{
/** @var array<int> $orderIds */
$orderIds = Order::find()->andWhere(['status' => self::STATUS_SHIPPED])->column('id');
foreach ($orderIds as $orderId) {
$this->updateEstimateTimeIfNeeded($orderId);
}
}

It retrieves all shipped orders (not delivered), and call the function to update the estimated delivery time. Do you see the problem now?

Let’s take this approach:

public function updateAllEstimateTimeIfNeeded2():void
{
/** @var DeliveryCompany[] $delayedCompanies */
$delayedCompanies = DeliveryCompany::find()->andWhere(['estimated_delay' >0])->all();
foreach ($delayedCompanies as $delayedCompany) {
$dbUpdateExpression=new Expression('estimate_delivery_date + ' . $delayedCompany->estimated_delay);
Order::updateAll([
'estimate_delivery_date' => $dbUpdateExpression,
'delivery_company_id' => $delayedCompany->id
]);
}
}

This solution takes 1 second. It’s x1000 faster!.

We retrieve all delayed companies and update all affected orders with the delay. On average, from 1M orders, it applies normally to 1k-10k. We have seen the problem from outside, found a better solution and make it incredibly fast.

Example 2:

We have one algorithm that center images. Detect borders, crop and center the product.

The process is fast, but when the image is large (>5Mb) it takes too much time and a lot of CPU.

How can we improve the process? To be honest, I had no idea, I tried with different image libraries, re-compiling ImageMagick with optimizations, etc… I only got a 5% performance increase.

I asked @kroketen that worked in the past with the images, and the solution was extremely simple:

Once we load the image, we re-scale the image to 400x400, we do all the calculation (border detection, centering, etc…) once we finish, we apply it to the large image.

For example:

  • Image is 4000x4000, we rescale to 400x400.
  • The calculated border is top 50, left 100, right 80 and bottom 50.
  • We apply to the original image 500, 100,800 and 500 crop.

Summary:

Relaying on micro-optimization, caches, etc.. are good some times, but you always try to ask your self: How can I improve this by x10.

--

--

Coding CEO

I fix things. I was CEO twice, and I missed too much coding. Back to CTO again.