Often while working with python I miss having a DI framework (sadly this is not very common in the python world). There are multiple blog posts on the benefits of DI, so I won’t go into that here.
Either way, I was on the lookout for a DI framework for python and this one caught my eye: Dependency Injector. Mainly because of this listed feature:
“Performance. Fast. Written in Cython.”
Which peaked my interest, how much overhead does a DI framework add to your code? So I wrote a small benchmark to test this.
For the benchmark we are requesting the containers to give us an instance of FooService
, with the following dependency tree, with Cache, HTTPClient and Config being singletons and the rest being transient:
FooService- Config- Database- Config- Cache- Config- HttpClient- Config- BarService- Database- Config- Cache- Config- HTTPClient- ConfigFooService - Config - Database - Config - Cache - Config - HttpClient - Config - BarService - Database - Config - Cache - Config - HTTPClient - ConfigFooService - Config - Database - Config - Cache - Config - HttpClient - Config - BarService - Database - Config - Cache - Config - HTTPClient - Config
Enter fullscreen mode Exit fullscreen mode
And these were the results:
$ python3.11 benchmark/run_benchmark.pyDirect (No framework) 563.216755 nsDependency Injector Container (Cython) 1183.705037 nsRodi Container (Pure Python) 1983.972198 ns$ python3.11 benchmark/run_benchmark.py Direct (No framework) 563.216755 ns Dependency Injector Container (Cython) 1183.705037 ns Rodi Container (Pure Python) 1983.972198 ns$ python3.11 benchmark/run_benchmark.py Direct (No framework) 563.216755 ns Dependency Injector Container (Cython) 1183.705037 ns Rodi Container (Pure Python) 1983.972198 ns
Enter fullscreen mode Exit fullscreen mode
Ok, so compared to directly instantiating the objects ourselves it seems a pure python container is ~4x slower and the cython implementation is ~2x slower.
Which makes sense, since a normal container has some overhead to get a service:
- Check if service is a singleton or transient
- If its a singleton check if its already instantiated
- Find the dependencies for the service and get them (recursively)
Meta programming to the rescue
So how can we make this faster?
After a few failed attempts using closures and meta classes I had the idea to use meta programming and generate the code for the container at runtime. If we already know all services beforehand, we can generate a class with specialized methods for each service we need to instantiate.
Here’s the optimizations we can perform on each methods:
- No need to check if the service is a singleton or transient
- No need to find the dependencies for the service at runtime
- Inlining the code to instantiate the dependencies
- This saves us the overhead of recursively calling the container to get the dependencies
- Preloading singletons also avoids the need to check if they are already instantiated
Here’s a snippet of the generated code for the FooService
method with all optimizations mentioned above:
<span>def</span> <span>get</span><span>(</span><span>self</span><span>,</span> <span>service_id</span><span>):</span> <span># to provide a clean interface we still have this method as overhead </span> <span>return</span> <span>self</span><span>.</span><span>_service_getter_map</span><span>[</span><span>service_id</span><span>]()</span><span>def</span> <span>get_services_FooService</span><span>(</span><span>self</span><span>):</span><span>return</span> <span>services</span><span>.</span><span>FooService</span><span>(</span><span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span> <span># singleton instance is preloaded and inlined </span> <span>database</span><span>=</span><span>services</span><span>.</span><span>Database</span><span>(</span> <span># transient service is inlined </span> <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span><span>),</span><span>cache</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Cache</span><span>],</span><span>http_client</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>HTTPClient</span><span>],</span><span>bar_service</span><span>=</span><span>services</span><span>.</span><span>BarService</span><span>(</span><span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span><span>database</span><span>=</span><span>services</span><span>.</span><span>Database</span><span>(</span><span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span><span>),</span><span>cache</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Cache</span><span>],</span><span>http_client</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>HTTPClient</span><span>],</span><span>),</span><span>)</span><span>def</span> <span>get</span><span>(</span><span>self</span><span>,</span> <span>service_id</span><span>):</span> <span># to provide a clean interface we still have this method as overhead </span> <span>return</span> <span>self</span><span>.</span><span>_service_getter_map</span><span>[</span><span>service_id</span><span>]()</span> <span>def</span> <span>get_services_FooService</span><span>(</span><span>self</span><span>):</span> <span>return</span> <span>services</span><span>.</span><span>FooService</span><span>(</span> <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span> <span># singleton instance is preloaded and inlined </span> <span>database</span><span>=</span><span>services</span><span>.</span><span>Database</span><span>(</span> <span># transient service is inlined </span> <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span> <span>),</span> <span>cache</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Cache</span><span>],</span> <span>http_client</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>HTTPClient</span><span>],</span> <span>bar_service</span><span>=</span><span>services</span><span>.</span><span>BarService</span><span>(</span> <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span> <span>database</span><span>=</span><span>services</span><span>.</span><span>Database</span><span>(</span> <span>config</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Config</span><span>],</span> <span>),</span> <span>cache</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>Cache</span><span>],</span> <span>http_client</span><span>=</span><span>self</span><span>.</span><span>_singleton_instances</span><span>[</span><span>services</span><span>.</span><span>HTTPClient</span><span>],</span> <span>),</span> <span>)</span>def get(self, service_id): # to provide a clean interface we still have this method as overhead return self._service_getter_map[service_id]() def get_services_FooService(self): return services.FooService( config=self._singleton_instances[services.Config], # singleton instance is preloaded and inlined database=services.Database( # transient service is inlined config=self._singleton_instances[services.Config], ), cache=self._singleton_instances[services.Cache], http_client=self._singleton_instances[services.HTTPClient], bar_service=services.BarService( config=self._singleton_instances[services.Config], database=services.Database( config=self._singleton_instances[services.Config], ), cache=self._singleton_instances[services.Cache], http_client=self._singleton_instances[services.HTTPClient], ), )
Enter fullscreen mode Exit fullscreen mode
Results
So, did it work? Here’s the results:
$ python3.11 benchmark/run_benchmark.pyDirect 563.216755 nsMetaDI Container 792.128003 nsDependency Injector Container (Cython) 1183.705037 nsRodi Container 1983.972198 ns$ python3.11 benchmark/run_benchmark.py Direct 563.216755 ns MetaDI Container 792.128003 ns Dependency Injector Container (Cython) 1183.705037 ns Rodi Container 1983.972198 ns$ python3.11 benchmark/run_benchmark.py Direct 563.216755 ns MetaDI Container 792.128003 ns Dependency Injector Container (Cython) 1183.705037 ns Rodi Container 1983.972198 ns
Enter fullscreen mode Exit fullscreen mode
Which are pretty good! We managed to be faster than the cython implementation while using pure python.
Conclusion
Even though I believe this performance gain is irrelevant for most applications, it was a fun experiment and I learned a lot about meta programming. I also think this is a good example of how meta programming can be used to optimize code, and I will definitely keep this in mind for future projects.
Source code can be found on Github
暂无评论内容