google / fruit

Fruit, a dependency injection framework for C++
https://github.com/google/fruit/wiki
Apache License 2.0
1.81k stars 200 forks source link

Difference with [Boost].DI and Wallaroo #113

Closed maaikez closed 4 years ago

maaikez commented 4 years ago

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!

poletti-marco commented 4 years ago

Hi, there are some differences in features and performance. A little bit of discussion in terms of features is in https://github.com/google/fruit/issues/5.

I started working on benchmarks some time ago but I didn't have time to complete them. I'll see what I can do in the next week or so. AFAIR last time I checked Fruit was a bit slower to compile but faster to run. The binding loop checks that fruit does (and boost.di doesn't) add some compile time cost.

AFAIR the old benchmarks that show a large gap in favour of boost.di were done by compiling fruit in debug mode (and unstripped) vs boost.di in release mode, (and stripped), so I wouldn't put much weight on those, it's not a fair comparison.

On Mon, 30 Mar 2020, 08:56 Maaike Zijderveld, notifications@github.com wrote:

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 https://boost-experimental.github.io/di/cppnow-2016/#/, which shows huge performance differences (in favour of [Boost].DI, from page 4.9 https://boost-experimental.github.io/di/cppnow-2016/#/3/8 down), but I don't know if they are still accurate because this presentation is almost four years old?

Thanks!

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/google/fruit/issues/113, or unsubscribe https://github.com/notifications/unsubscribe-auth/AALZ4F2KFHRKKOYJPDAG3OTRKC6MHANCNFSM4LWWKCUA .

maaikez commented 4 years ago

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?

poletti-marco commented 4 years ago

I'm afraid in terms of feature comparison that's all I got (and you're right, it's not much).

I never got around to writing that comparison page, it would require a lot of investigation in boost.di functionality and it could easily go out of date, so the bang/buck is relatively low imo compared to other Fruit work, so it never got to the top of my priority list.

It's also not clear if people would trust it since it comes from a potentially biased source (author of 1 of the 2 libraries). Benchmarks should be more objective and more likely to be trusted, and once I have the infra set up they should be repeatable with fairly low effort (while comparing the new functionality in the 2 libs after X months isn't).

If you do come up with a comparison I'd be happy to add it to the wiki though.

On Tue, 31 Mar 2020, 00:29 Maaike Zijderveld, notifications@github.com wrote:

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?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/google/fruit/issues/113#issuecomment-606451599, or unsubscribe https://github.com/notifications/unsubscribe-auth/AALZ4F42MZQKIIO5QT4PEZDRKGLWXANCNFSM4LWWKCUA .

maaikez commented 4 years ago

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!

poletti-marco commented 4 years ago

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.

maaikez commented 4 years ago

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!

poletti-marco commented 4 years ago

FYI, you don't have to use INJECT if you don't want to modify the classes, you can use registerConstructor instead as you'd do for types you don't own. INJECT is just a convenience.

On Wed, 15 Apr 2020, 06:19 Maaike Zijderveld, notifications@github.com wrote:

Closed #113 https://github.com/google/fruit/issues/113.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/google/fruit/issues/113#event-3235448497, or unsubscribe https://github.com/notifications/unsubscribe-auth/AALZ4FYIO75VG6SBKIP7BD3RMWX57ANCNFSM4LWWKCUA .

maaikez commented 4 years ago

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

FranckRJ commented 4 years ago

@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 !

poletti-marco commented 4 years ago

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

FranckRJ commented 4 years ago

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 !

poletti-marco commented 4 years ago

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.

FranckRJ commented 4 years ago

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.

poletti-marco commented 4 years ago

On Tue, 9 Jun 2020, 00:07 Franck W., notifications@github.com wrote:

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

Maybe by returning a factory instead of an injector ?

How would this look like?

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 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.

Yeah, but IIUC that's equivalent to global variables:

Foo getFoo() { static Foo foo = new Foo(...); return foo; }

Bar getBar() { static Bar bar = new Bar(getFoo(), ...); return bar; }

Global state is considered a bad practice, e.g. it's hard to test. But if that's really what you want you might as well do it explicitly. No need to use a DI library for these. So what you'd then use the DI library for is everything else.

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:

class Bar { private: Foo* foo = getFoo(); ... Bar() = default; };

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.

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.

Boost.DI forces you to use shared pointers in that case and I think it's normal for people to then copy them (relying on the refcount, not an actual copy of the underlying object ofc).

FranckRJ commented 4 years ago

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.

JarekParal commented 3 years ago

@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.

poletti-marco commented 3 years ago

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.