google / fruit

Fruit, a dependency injection framework for C++

Home Page:https://github.com/google/fruit/wiki

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Difference with [Boost].DI and Wallaroo

maaikez opened this issue · comments

Hello,

I'm looking for a good dependency injection framework for C++. I currently found some interesting frameworks, I think the best known frameworks are Wallaroo, [Boost].DI and your framework google/fruit.

Wallaroo's last update was somewhere in January 2018, so I don't know if it is still maintained. So maybe I can better choose between [Boost].DI and google/fruit.

What are the main differences between them?
I saw something about runtime injection with XML, but I did not find an example yet, is that indeed a difference with the Boost version and where can I find how that works?
Did anyone a benchmark to show the differences in performance? I found this presentation, which shows huge performance differences (in favour of [Boost].DI, from page 4.9 down), but I don't know if they are still accurate because this presentation is almost four years old?

Thanks!

Thanks, I indeed already thought the comparison might not be ok if you look at the differences.

Well, the link you give shows very few differences between the two. Were you able to create the documentation page you mentioned? Or what are the biggest (and most 'useful') differences between the two?

Yes clear and understandable :). I will dive into it and see what fits better, taking the benchmark into account if that's ready as well (if you have time for that).

Thanks for developing Fruit anyway!

Hi, I (finally!) finished gathering data from various benchmarks and I now updated the benchmarks page:
https://github.com/google/fruit/wiki/Benchmarks

Please take a look and let me know if anything is unclear, or if I'm missing some benchmarks that you'd be interested in.

Thanks, looks good and complete for now.
Fruit is a lot faster and also smaller. That's nice.

One thing I don't like of Fruit is the use of INJECT, while wich Boost.DI, you don't have to change anything to your normal classes. Maybe compiling is faster also because of this.
But the executable size and compile speed are interesting indeed.

Thank you for making the benchmark!

Ah ok, I didn't see that yet.
Thanks!

@poletti-marco I have several questions about the benchmark : https://github.com/google/fruit/wiki/Benchmarks

Did you used Boost.DI constructor deduction, or did you used the BOOST_DI_INJECT macro ? I guess there is a measurable difference between the two (i've never measured, it's just a guess) and because Fruit doesn't have this deduction mechanism i don't think it's 100% fair to only benchmark Boost.DI with it.

If i correctly understood you said that "Simple DI" code was all in a main.cpp unlike Fruit that was split across multiple translation units. I'm not sure to understand why you didn't put everything in the main.cpp for Fruit as well, or why you didn't split the "Simple DI" across multiple files. And what about Boost.DI ? Was it in a single file as well ?

Otherwise i think that the benchmark is well explained and goes into details, thanks for it !

Hi, the benchmarks try to be as fair as possible so e.g. both Fruit and Boost.DI use split files and I've tried to use similar APIs.

Since you have many questions (and I imagine that after those you might rightfully have some more :-) ), it's probably easier for you to look at the code.

Here's the code that does the generation:
https://github.com/google/fruit/blob/master/extras/benchmark/generate_benchmark.py
(it imports other python scripts in the same dir, so that code is relevant too)

You can run that to obtain the example code for the "100 classes" sample codebase with commands like:

cd ~/projects/fruit/extras/benchmarks

./generate_benchmark.py --di-library fruit --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/fruit-runtime-startup --use-normalized-component=false --cxx-std=c++11 --generate-runtime-bench-code true --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library fruit --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/fruit-runtime-per-request --use-normalized-component=true --cxx-std=c++11 --generate-runtime-bench-code true --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library fruit --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/fruit-compile-time --cxx-std=c++11 --generate-runtime-bench-code false --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library boost_di --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/boost_di-runtime --cxx-std=c++14 --generate-runtime-bench-code true --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library boost_di --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/boost_di-compile-time --cxx-std=c++14 --generate-runtime-bench-code false --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library none --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/no_di_library-runtime --use-new-delete=false --use-interfaces=false --cxx-std=c++11 --generate-runtime-bench-code true --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library none --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/no_di_library-compile-time --use-new-delete=false --use-interfaces=false --cxx-std=c++11 --generate-runtime-bench-code false --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library none --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/no_di_library_with_interfaces-runtime --use-new-delete=false --use-interfaces=true --cxx-std=c++11 --generate-runtime-bench-code true --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library none --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/no_di_library_with_interfaces-compile-time --use-new-delete=false --use-interfaces=true --cxx-std=c++11 --generate-runtime-bench-code false --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library none --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/no_di_library_with_new_delete-runtime --use-new-delete=true --use-interfaces=true --cxx-std=c++11 --generate-runtime-bench-code true --use-exceptions true --use-rtti true
./generate_benchmark.py --di-library none --compiler g++ --fruit-sources-dir /home/marco/projects/fruit --fruit-build-dir /home/marco/projects/fruit/build --boost-di-sources-dir /home/marco/projects/boost-di --num-components-with-no-deps 10 --num-components-with-deps 90 --num-deps 10 --output-dir /tmp/benchmark/no_di_library_with_new_delete-compile-time --use-new-delete=true --use-interfaces=true --cxx-std=c++11 --generate-runtime-bench-code false --use-exceptions true --use-rtti true

I attach a tarball with the code resulting from running those commands, so that you can just look there at the generated code if you don't want to check out Fruit.

As mentioned I tried to be fair in the comparison, but if you do find some discrepancies (e.g. if you believe I'm using Boost.DI in a way that gives it an unfair disadvantage) please let me know and we can discuss.
My intent is to do an apples-to-apples comparison as much as possible.

benchmark.tar.gz

I don't know very well any of the libraries, i just try to make an opinion on both of them.

Looks like the code generated for Boost.DI and Fruit is very similar, I agree. But because the injector / component is type erased in Fruit and not in Boost.DI you end up with different file structure. And you used shared_ptr (with <memory> header) for Boost.DI but i'm not sure that removing it will reduce significantly compilation time.

I understand that benchmarking two different libraries is hard, you cannot apply exactly the same design to both. I'm not even sure that trying to apply the same design to both is relevant because if the library isn't used that way in real code you benchmark something that will never be written.

So, anyway, thanks for the benchmark !

But because the injector / component is type erased in Fruit and not in Boost.DI you end up with different file structure.

That's true.
Unfortunately Boost.DI only has a non-type-erased equivalent of Fruit's components.
If Fruit didn't exist and I was using Boost.DI, that's what I'd have to use.

If the benchmarks used the type-erased version (in Fruit terminology, using injectors instead of components at each level) I imagine the Boost.DI compilation time would get better but the runtime performance would get much much worse since it would create an exponential number of instances of the low-level classes.
And it's also semantically wrong, e.g. if you have a Logger component that opens a log file and that various components in your system use to log, you really want a single one in the injection graph and not one for each class using it.

I think doing that would put Boost.DI at an unfair disadvantage, I don't think as a user I'd choose to do that.

And you used shared_ptr (with header) for Boost.DI but i'm not sure that removing it will reduce significantly compilation time.

Fruit's header ends up including that anyway so that shouldn't make a difference.

Unfortunately Boost.DI only has a non-type-erased equivalent of Fruit's components.

Maybe by returning a factory instead of an injector ?

If the benchmarks used the type-erased version (in Fruit terminology, using injectors instead of components at each level) I imagine the Boost.DI compilation time would get better but the runtime performance would get much much worse since it would create an exponential number of instances of the low-level classes.

With factories and good scope management i'm not sure that it would impact this.

And it's also semantically wrong, e.g. if you have a Logger component that opens a log file and that various components in your system use to log, you really want a single one in the injection graph and not one for each class using it.

In Boost.DI you can use singleton scope, it create an instance shared across all injectors.

EDIT: You can also use session to have more control over lifetime of instances. Every binding that use the same session (even across several injectors) share the same instance.

And you used shared_ptr (with header) for Boost.DI but i'm not sure that removing it will reduce significantly compilation time.

Fruit's header ends up including that anyway so that shouldn't make a difference.

On compile time maybe, on runtime each shared_ptr is copied two times (because you didn't use std::move), i don't know how the compiler optimize it but i don't think it's as fast as assigning references.

How would this look like?

Something like this : https://gist.github.com/FranckRJ/c045b559ae15e80c08fe5b8084561cd4 . But you're right, you lose fine tuning of scopes, you can't build two completely different tree that have only one instance of each dependency each. You can either use unique_scope (each instance are only used one time) or singleton_scope (each instance is only built one time). You can mix them by using unique_ptr / shared_ptr as dependency, instead of shared_ptr for everything.

Global state is considered a bad practice, e.g. it's hard to test.

It's global on the release build, but when you test you inject a mock of each dependency, it's not hard to test.

If (as I imagine) you'd put getFoo in foo.h, getBar in bar.h, etc and make
the constructor visible only to the get* function, you could argue that's
not even DI because you're depending on a specific impl of all your
dependencies.
At that point it's no more modular than:
[...]

I don't really understand, in one case you have the dependency injected in the constructor, meaning that you can put what you want, in the other you have no control over it, how it's the same ?

Ok, that's true, but it's an increment/decrement of a recount that's
already in cache at that point (since it was incremented just before the
function call).
That overhead is so small it's negligible.
I don't think it would have any effect on the bench results.

Don't know, it's an atomic counter, meaning it's a memory barrier. But you may be right.


So, it's not possible to have the same file structure and features as Fruit in Boost.DI, it's one or the other. Considering this I have no idea how to write a better benchmark than what you did.

@poletti-marco I would have a question about your benchmark and Boost.DI benchmarks.

In both benchmarks, the authors show that their DI is much better. Do you think you could look at the Boost.DI benchmark code and discover the reasons why Google Fruit is much worse in all theirs metrics? As author of this library you should be the best for this task.

Thanks in advance.

Hi, sorry for the delay, I was on vacation.

The benchmarks in the Boost.DI wiki are older, based on Fruit 2.0.2 (for reference, 2.0.3 was released in May 2016) and Boost DI 1.0.1, while the ones in the Fruit wiki use Fruit 3.5.0 (released in April 2020) and Boost DI 1.0.1 (that was still the latest version at the time).

Also, the Boost.DI benchmarks build Fruit with assertions enabled while Boost.DI is built with assertions disabled.
TBH this was easy to do accidentally back then (if running cmake without specifying the CMAKE_BUILD_TYPE flag), so I don't think it was intentional but nevertheless it gives Fruit an unfair disadvantage.
Later versions of Fruit make it harder to do this mistake when building it.

I haven't looked at the Boost.DI benchs in a while TBH but from what I recall the benchmarked codebase is less modular (all headers included in a single source file) than the codebase benchmarked in the Fruit benchs.
This would cause a big hit in terms of incremental build time, but the Boost.DI benchs only measure the cold build time (while the Fruit benchs have both).

Fruit is designed to write more modular code and works better in that use case (in larger codebases, including all headers that define the classes in the system in main.cpp is just not an option if you want a reasonable compile time).
Boost.DI instead focuses on the "all headers together" use case and can provide better runtime performance there, but at the price of not scaling well to larger codebases.