Blog: Propel 1.6 Is Faster Than Ever
The upcoming Propel release, dubbed Propel 1.6.3, is faster than ever. Special optimizations were introduced in the Propel generator to make your entities blazingly fast when performing persistence actions.
What Is Fast?
ORMs have a bad reputation concerning performance. But complex models are often slow because the underlying database queries are slow. Before blaming the ORM, you must measure the share of the database in the processing time of a script. A slow SQL query can't be faster using an ORM.
That's why we, the Propel developers, measure the Propel performance relative to reference queries. Propel is not slow or fast per se. A Propel query is n times slower than the same query without an ORM, and we work hard to keep the n factor low.
In fact, our target is to keep the Propel ORM under a factor 4. That means that using Propel should not be more than four times slower than bare PDO.
In addition, Propel provides tools to minimize database queries, as explained in a previous post. Instance pooling, joined hydration, lazy loading, lazy hydration, all those features avoid a return trip to the database in many cases - and they can make Propel faster than raw PDO.
What Is Slow?
Four times slower than PDO can seem like a lot slower. It is not. Think about all the things that Propel has to do when you call save()
on an entity:
- Check if there are related objects to save (e.g. saving an
Author
also saves all relatedBook
s) - Determine if the object is new, to choose between an INSERT or an UPDATE statement (we'll consider an INSERT for the rest of this example)
- List the properties of the entity that were modified
- Craft a SQL query string using the names of the columns in the database matching the modified properties in the object
- Prepare the values for the query (rewind resources, format dates, etc.)
- Find the connection to use (and if there is a master and a slave, find the master connection)
- Start a database transaction
- If the table uses a sequence, get the value to use as primary key
- Bind the values of each modified properties to the query string using the PDO type corresponding to the column (e.g.
PDO::PARAM_BOOL
for a boolean column,PDO::PARAM_NULL
for a null value, etc.) - Execute the insertion query against the database
- If the table doesn't use sequences, retrieve the inserted id
- Save the related objects in the same transaction
- Commit the transaction
- Mark the entity as not new and not modified anymore
- Add the entity to the instance pool
- Return the number of modified objects (including all the related objects)
That's a lot of object manipulations, and the actual INSERT
SQL query is only one step in this long recipe.
What Is Faster?
In previous Propel versions, a lot of the operations described above were executed at runtime. Crafting the SQL query string, choosing the PDO type for a modified column, getting the primary key value either before the main query (for tables using sequences) or after (for tables using autoincrement), etc. There were really many things to do each time an object was saved.
In the next Propel version, all the computation necessary for these operations is now done at build time. The generated save()
method actually does exactly what is described above, in the fastest PHP code possible. If you wrote a generic PHP script to do all the steps just described, it would probably be slower than Propel. Because the Propel generated classes aren't generic: they are specific to your model, and take away all the heavy duty stuff to introspect your schema, and map the object oriented world with the relational world.
Upgrade to the Propel master, rebuild your model, and take a look at the generated code for the save()
method. You will understand all that it does, even without knowing how Propel works. It's because the save()
method contains nothing Propel-specific. It's pure PHP and PDO. It's pure speed.
How Much Faster Is It?
In Propel 1.6.2, the "n" factor of the insert operation was around 10. It used to take ten times longer to persist an entity using Propel than executing the insert statement with PDO alone. The "n" factor is now well under nine. It's also well under the target four.
We use a test project called php-orm-benchmark, released under the MIT license, to compare the speeds of some basic scenarios. In addition to a simple insertion, we measure the n factor for a database retrieval using a primary key, the execution of a complex query, the raw hydration of an object based on a resultset, and a joined hydration. Here are the results:
insert | find pk | complex | hydrate | with | |
---|---|---|---|---|---|
PDO | 111 | 109 | 95 | 106 | 99 |
Propel 1.4 | 1260 | 502 | 123 | 311 | 303 |
Propel 1.5 | 1050 | 522 | 165 | 414 | 602 |
Propel 1.6 | 363 | 198 | 176 | 423 | 466 |
The tests repeats the basic scenarios enough times to reach an approximate score of 100 for PDO. That's the reference. So a score of 300 means that, compared to PDO, the library has an overhead of factor 3 for this scenario. According to the load of the server used for testing, results may vary of about 10%. So any difference of less than 10% is not significant.
What this chart shows is that simple operations used to be quite heavy in Propel. An insertion, or a Pk find, are very fast database operations. The Propel overhead for these operations was important - even if that didn't mean Propel was slow. If the raw SQL score (using PDO) was of 10ms for an insertion, the same operation would take 100ms with Propel. That's not slow per se, but it's slower than PDO.
The chart also shows that the Propel overhead becomes much less noticeable when the SQL query is complex. That's because the Propel overhead depends on the number of objects to hydrate, while the SQL query time depends on the complexity of the query (and the pertinence of the indices).
Finally, the chart shows that the latest version of Propel 1.6 reaches the sweet spot of the "4 factor", and even goes beyond.
Are The Other ORMs Faster?
You will ask this question, so we'd better give you the answer. Here is how Doctrine compares with Propel on these tests:
insert | find pk | complex | hydrate | with | |
---|---|---|---|---|---|
PDO | 111 | 109 | 95 | 106 | 99 |
Doctrine 1.2 | 2187 | 3425 | 545 | 2276 | 2365 |
Doctrine 1.2 with cache | 2508 | 1500 | 665 | 1481 | 933 |
Doctrine 2 | 151 | 709 | 160 | 800 | 488 |
As already noted in a previous benchmark, Doctrine 1.2 is VERY slow. The overhead varies between 6 and 25, which is a lot. As for Doctrine 2, it performs a lot better. In fact, it even outperforms Propel in the insertion scenario - because Doctrine 2 has a special feature to accelerate mass insertion. So a scenario made to test raw insertion speed, and repeated to make the duration significant, becomes a mass insertion, and therefore takes advantage of the Doctrine optimization.
Overall, Doctrine 2 performs very well, and keeps the ORM overhead under a factor 8 all the time.
The Benefits of Code Generation
We could boost the Propel results even further by using a cache engine, just like Doctrine does. But having to use cache brings a lot of worries: you have to think about naming the cached queries, and invalidating the cache when the underlying data changes. Unfortunately, these two are the hardest things in Computer Science, according to Phil Karton. So if you can avoid using a cache engine, by all means, do it.
Propel's raw speed is enough to remove the need of a cache engine. That's because Propel uses code generation to prepare the base entity and query classes for runtime. The philosophy of code generation was only pushed a little further in Propel 1.6, so you can now use the Propel ORM without any afterthought about performance.