[00:02] write fast code intuitively. And in this video, I want to formalize some of the best practices I use every day based on a real world example, the JSON logic PHP library that I recently optimized while consulting for a tideways customer. [00:17] These spectrices are meant to go beyond micro optimization character and I will share the preconditions that will make them useful compared to when they are not. Mo, I am Benjamin and I'm working on PHP performance topics for the last [00:32] 10 years, helping thousands of developers along the way. The JSON logic PHP library is a paser that accepts JSON logic rules and executes them in PHP. So what are JSON logic rules? This is a language um that uh allows you to [00:49] language um that uh allows you to express a formula um either um a boolean express a formula um either um a boolean condition or a calculation using a JSON condition or a calculation using a JSON language and uh then allows you to pass [01:01] in data at runtime against those formulas to compute them. So uh this is formulas to compute them. So uh this is useful if you want to uh have um sort of non-developers or external systems [01:16] um provide business logic for example for price calculations to an application sandbox um according to strict rules. So how [01:29] um according to strict rules. So how does it work? Um a simple rule is um always using an an operator as the key of a JSON object and then the operand as of a JSON object and then the operand as um an array of values. So for an equal [01:44] sort of operator you have two operants the left side and the right side and the left side and the right side and these are um defined as two um values of these are um defined as two um values of of an array in the JSON object. Um so if [01:58] you have this JSON formula you can decode it and then you put it into this JSON logic apply function to um sort of get back this this value. And uh we'll see a simple example how to use that. Now after installing the library with [02:12] Now after installing the library with composer we can um uh write a simple test script here. So I want to implement a calculator. So I use the plus operand a calculator. So I use the plus operand um and two variables um as uh operants [02:27] and then I call JSON logic apply with this rule and then I pass in sort of A and B as data and I could build it in a way where this is dynamic from from a product for example or from user input. So I can run this um JSON logic simple [02:45] So I can run this um JSON logic simple PHP. So we can see the output is three and uh this is sort of my very simple calculator. The customer case was um a more complex real world scenario where the formulas were quite big and they [03:00] were assigned to products as price calculation rules. And the products are extremely complex in this customer's case. They have tons of different rules, case. They have tons of different rules, price groups and u add-ons, specific [03:14] conditions and everything. So this is represented as a complex JSON formula represented as a complex JSON formula with uh which can be created um by a third party system. So for the customers real world use case we had a much more [03:27] complex calculation um uh specifically a price calculation in their e-commerce price calculation in their e-commerce system. The uh products in this store um were quite complex with many different varants, many different rules and um [03:41] configuration ability making it quite complex to compute the prices based on all the various inputs that the users could make. So you can see here based on if conditions on the price group different prices are being used and this [03:58] different prices are being used and this is quite a deep uh formula then um the different prices of the product of add-on calculations are added. The product can be assembled on site at the customer and then based on the different [04:13] price groups, there are different prices based on the assembly and um this is performed for many different uh products um in the customer store. So we have an [04:25] um in the customer store. So we have an example data object here and then u we example data object here and then u we try to calculate this for 10,000 times. So th this assembles sort of the real world example of the customer where um [04:40] this pricing calculation could roughly take around 400 to 600 milliseconds take around 400 to 600 milliseconds um and um in every request that like sort of showed a product and we want to optimize this by using a profiler [04:56] looking at uh what we can do to improve this. So the first step is running a profiler. In this case here, I'm uh using um uh the titrace profiler [05:10] using um uh the titrace profiler uh CLI. I run this. Um I see here that I uh CLI. I run this. Um I see here that I have a trace 66 milliseconds. Uh I'm going to look at that. We can see in the core graph [05:27] We can see in the core graph what sort of is slow in this library. So we can make out two basic functions that are called quite a lot of times. So we have this is logic function being called 8003,000 [05:42] 8003,000 times for a total of 110 milliseconds and then we have the apply function being called um a lot of different times at different recursion levels. So if we can see here apply is uh at an recursion [05:57] level or recursion depth of one. So the recursion goes quite deep here. Recursion depths of six we see here seven. Um so the the way the JSON logic [06:09] library works it recursively applies the rules based on the depths of the JSON document. Um let's see how this looks in code. [06:36] we have the function is logic that we saw before and we have the function apply which is sort of the the the entry point into this API. So [06:51] um let's look at what we can do and what can we optimize here. So the first thing is something I saw on um the GitHub page already in the pull um the GitHub page already in the pull requests. There was an open pull request [07:05] that said optimize apply by moving operators into a static array where operators into a static array where possible. So um what does this change possible. So um what does this change do? We can look at the code here. The [07:17] apply function defines um a list of operators that we use. So the equals equals we saw we also saw the plus operator in our calculator [07:29] example. So where is that here? So the plus example and uh what we can see is that the apply function always redefineses this [07:43] operators here. And because these are closures and we saw that we recursively call them. That means that every um calling of sort of this apply function recursively will always [07:57] reinitialize this um data even though it doesn't change from call to call. So what we can do here instead is we make sure that um [08:09] we have the operators initialized only once and then reuse them um from from this initialized state. This patch is quite complex. Um, [08:21] so I'm going to apply it as quickly as possible here. So what you can do in possible here. So what you can do in GitHub is to append the patch. I'm taking the patch um to my local system. [08:37] um to my local system. I store it [08:55] to patch I'm going in the library itself JSON logic JSON logic uh vendor [09:16] and then I'm going to patch and then [09:28] and that succeeded. So let's take a look. Yes. So we have the build look. Yes. So we have the build operators now uh function. Now we save that and [09:40] we look at the profile to see what changed. So titrates run PHP changed. So titrates run PHP JSON logic complex [09:57] milliseconds. and and looking at the trace. [10:09] again sort of the time from it logic is still the same around 111 milliseconds still the same around 111 milliseconds to before I think that was yeah oh 110 to before I think that was yeah oh 110 so exactly but um the ch the time [10:22] so exactly but um the ch the time timings changed quite a lot here. So um the 200 milliseconds are saved just by not reinitializing this operator again. This is especially powerful if you initialize closures which are expensive [10:37] initialize closures which are expensive to set up uh in PHP. optimize? So we see JSON logic is logic here. Uh this is something we will look [10:50] at a little bit later. But let's look at the call graph from the top first and see sort of like what takes time and what can we look at and [11:02] improve. So we have this topmost JSON logic apply call and this um calls a closure on line from defined in line [11:19] a closure on line from defined in line 344 map. So there's an array map call here in between JSON apply and calling this [11:31] closure. Let's look at the code a little bit. Where does this happen? [11:58] This is the recursive applying of the uh the rule logic. And what it does here is the rule logic. And what it does here is it's using array map to do that. And um [12:10] because we called this recursively. So array map again um recursively calling apply. And we saw in the core graph apply is being called up to a depth of [12:22] seven. This means we have sort of a lot of array map calls here. And this is a very tight loop. I would call this the hot path of this code. And whenever you have a hot pass and you are performing [12:37] an iteration then array map is actually a very bad way to do this and um it takes a lot of time because it has the function call overhead um that you don't [12:50] have if you make this a for each and this is um a regular performance optimization that I can see um I also saw this um in doctrine before which I saw this um in doctrine before which I maintain So in [13:05] um doctrine we had this problem as well where we had an array map in a very where we had an array map in a very tight um loop that is the hot pass of tight um loop that is the hot pass of doctrine and we modified that to become [13:19] um for each loop in uh instead and that improved the performance massively. The same is a problem here in this library where we have the hot pass of this where we have the hot pass of this computation [13:36] this with the for each. The patch for this is much smaller than the previous one. So I can copy paste this here. It's I just see it's a a different line actually. So there are multiple places that are doing array map. So this is the [13:51] that are doing array map. So this is the one that is um um the regular pass in one that is um um the regular pass in the code. So let's replace that. the code. So let's replace that. So um I already put a patch on um to [14:04] make this change for the um customer consulting project. We saw a 267 consulting project. We saw a 267 millisecond drop by applying this. I'm going to replicate it um and building this out uh manually because it's just [14:20] like changing these lines. I will I will for each of the values as key value [14:32] for each of the values as key value and then I will and then I will replace key with self apply value and replace key with self apply value and data. So this is uh changing the array [14:46] map to an for each. So we run the profiler again. [15:01] And compared to the 400 milliseconds um before we now see 357 before we now see 357 milliseconds [15:17] we see like a 43 millisecond improvement. improvement. um by replacing the for each with the array map with a for each. So the next thing that we can see in the [15:31] code and that this is something recurring for that we also talked about recurring for that we also talked about in previous videos is not using compiler um optimized functions um uh an optimization that opcache uh and the Z [15:45] engine provide. And this is only necessary if there are functions that are called a lot of times and they have compiler optimizations. This means the function itself is not called uh directly anymore but the [16:00] engine calls sort of a more optimized version of it. So we see here is array is one case of that. Tideways even shows this that you can rely on compiler optimizations and um the way to fix that um is to use [16:17] function imports and we can see here in this example is array array keys and count. Count is also a compiler optimized function. So we can um import these three functions um uh is object also compiler optimized. [16:33] maybe this for functions and then we can see how um the performance fares after that. So this is a rather simple change at the top we can just say use function count use function is array [16:57] think not compile optimized so it might not change a lot. And the is object was also one. So let's have those. We run the uh profiler again. We can see time [17:11] the uh profiler again. We can see time now reduced quite a bit 265. So that's that's an improvement. Um, Um, and we can see here, okay, error keys [17:25] was not optimized, but we don't see the count uh function anymore. So, we see a string. So, this one can actually be optimized as well. So, um, that shouldn't be a lot. Let's also optimize this one. [17:42] Let's also optimize this one. Use function Use function is string. [17:57] this big improvement I believe. So it's it's rather a lie to some degree um because of variations. If we run this more often, it would um would be just [18:09] the two two milliseconds we saw before. But as you can see um sort of it it's But as you can see um sort of it it's much faster than before now already. So what is the last thing that we can u maybe look at optimizing? [18:25] So PHP um becomes slower in the number of function calls that you have. So if you have functions that are called being called like a lot of times uh in a very tight loop um in the hot path of your [18:40] application then it might make sense to inline those functions into the code that uses it. So in this case we still see JSON logic get operator and is logic. These are called like this is 1 million times 830,000 [18:56] times. So this is really a lot of times and we might benefit from inlinining. So and we might benefit from inlinining. So let's look at the code. [19:11] this code here. So maybe let's just like do this instead. Let's see if we It's used in [19:25] different places. Is logic also uses it and is logic as you remember is called quite a lot. Let's run again. [19:42] more time than before. Looking at the call graph get operator, anymore. So we have is logic and get values now. So let's take a look at is logic. Where's that one called? And can we [19:58] Where's that one called? And can we inline it? So get values is also called a lot, but we wanted to look at um is logic now. So [20:13] is logic is quite a simple function. It checks for is array then if the count is checks for is array then if the count is one and if the key of the array is one and if the key of the array is um just zero um [20:29] is a string. So where is is logic called in our um example? So in the apply function, it's called right at the beginning. So we [20:43] could inline that one here. Let's try because we know apply is Let's try because we know apply is called so often. [21:09] And this is something that I wish PHP had or become will become at some point. had or become will become at some point. Um allowing the engine to inline Um allowing the engine to inline functions that are pure functions. They [21:21] don't have side effects, don't change state. Like if PHP could detect them and state. Like if PHP could detect them and uh inline them, that would be awesome. [21:33] And we will check what this has for an effect on the performance. So again like uh the 200 20 milliseconds So again like uh the 200 20 milliseconds down here [21:52] we see only get values is now mentioned as a function that takes a lot of time. as a function that takes a lot of time. So let's find out where it's called. Get values is called from apply. So let's look at [22:13] sort of the places where get values is called. Okay. So in apply right after called. Okay. So in apply right after this. So let's see get values how is it defined? So it again calls [22:28] uh get operator. So we already inlined that. that. And then [22:47] So it seems for our at least for our formula is no it's still called quite a lot of time here. time here. So this function for my taste is too big [23:01] So this function for my taste is too big to inline because um it makes it a little bit more difficult to make sure that this is the correct inlining especially because it reuses op and values as names which I [23:17] remember are being used in the in tob inline function as well. So there might inline function as well. So there might be an overwriting of this values. So um not sure if that makes sense here but um you'll see my um sort of like applying [23:32] logic here. So I see that I have code that is run in the hot pass. Um I run an that is run in the hot pass. Um I run an optimization um the profiler looking for which sort of like small pure functions are in the hot pass that I might be able [23:47] to inline without making the code much more unreadable and then I just inline more unreadable and then I just inline them or don't based on this information. So we started at a point where our complex JSON logic calculation took 600 [24:02] milliseconds and we optimized it down to just 190 milliseconds. So it's just 30% just 190 milliseconds. So it's just 30% 33% of the original uh runtime. It's quite a big optimization by not doing a lot of things. So we only refactored the [24:17] code. uh we made use of a few optimizations that um good PHP programmers do automatically. So do not like reinitialized state that stays the [24:30] same over and over again was the first optimization. The second optimization optimization. The second optimization was in the hot pass of uh code don't use was in the hot pass of uh code don't use array map but use for each and then also [24:43] array map but use for each and then also in the hot pass of code try to inline um compiler optimized functions as much as possible and then the last one in the as possible and then the last one in the hot pass of code uh inline PHP functions [24:56] that you need helper functions inline them into the main method as well if they are called um a lot of times. So let's say at a threshold of 100,000 [25:08] times um before thinking about inlining them. And all in all, these them. And all in all, these optimizations for our customer made up u a huge like three-digit um milliseconds optimization for their [25:23] pricing calculation and made the experience for their customers much faster. If you like my content about PHP performance, please subscribe to this channel or to our newsletter. The link is in the description.