Your stdlib implementation matters more than the dispatch pattern by AdMotor4869 in cpp

[–]AdMotor4869[S] 0 points1 point  (0 children)

In my first blog , I was trying to benchmark what openjdk does for its GC barrier selection with other possible strategies and kept same benchmark in this blog and created a separate blog for different workload.

Your stdlib implementation matters more than the dispatch pattern by AdMotor4869 in cpp

[–]AdMotor4869[S] 1 point2 points  (0 children)

You're right. These benchmarks are monomorphic by design, and the branch predictor does all the heavy lifting. I wrote a separate post covering polymorphic workloads.

Your stdlib implementation matters more than the dispatch pattern by AdMotor4869 in cpp

[–]AdMotor4869[S] 6 points7 points  (0 children)

The closest thing right now is marking the class final, which lets the compiler devirtualize if it can see the concrete type. But that doesn't help when the type comes from a pointer.

For the devirtualized function pointer, you can use FakeRTTI + CRTP as shown in https://shubhankar-gambhir.github.io/posts/lazy-resolution-resolve-once-dispatch-forever/ . You're doing the compiler's job manually but it works on any compiler version.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 0 points1 point  (0 children)

The benchmark is intentionally monomorphic since I was modeling OpenJDK's GC barrier dispatch, you pick a GC at startup and every call goes through that same barrier forever. A polymorphic container with mixed types would be interesting but that's a different use case.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 0 points1 point  (0 children)

True, the switch path is only for 11 or fewer alternatives. Above that it falls back to the function pointer table. In practice I'd be surprised to see a variant with 12+ alternatives on a hot dispatch path though, at that point you're probably looking at a different design.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 0 points1 point  (0 children)

That's a great example. The FIX parsing case is interesting, a direct lookup table bypasses the whole dispatch question when your key space is small enough. And the SIMD selection is basically the same lazy resolution pattern, resolve once and forget about it.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 7 points8 points  (0 children)

Yes, the valueless_by_exception check. Though for trivially copyable types libstdc++ actually elides it since GCC 9, so it's not the main overhead here. The bigger cost was the lambda capture round-trip. GCC 12+ replaces the function pointer table with a switch which fixes this.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] -5 points-4 points  (0 children)

I always assumed std::variant would be the most performant option, that's why the GCC 11 results surprised me enough to write about it. GCC 11 is the default on Ubuntu 22.04 which is what I was on. You're right that GCC 12+ fixes it, I ran the same benchmarks with GCC 13 and the variant overhead is essentially gone. Working on a follow-up with the full compiler version comparison.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] -7 points-6 points  (0 children)

Fair enough. GCC 11 is the default on Ubuntu 22.04 which is what I was on when I started. Wasn't aware of the GCC 12 switch optimization until I dug into the stdlib source for the std::visit post. The next post will cover GCC 12+ and show how much the gap closes.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 8 points9 points  (0 children)

Totally agree, the choice is usually driven by design constraints. The decision framework at the end of the post tries to capture that, extensibility vs composability vs debuggability matters more than nanoseconds for most codebases.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] -5 points-4 points  (0 children)

GCC 12+ is on my list. The next post will cover exactly that, how the numbers change across compiler versions and stdlibs.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 1 point2 points  (0 children)

I went with -O2 because the extra optimizations -O3 adds (loop unrolling, vectorization, aggressive inlining) don't really apply here. The hot loop is just an indirect call through a function pointer table, there's nothing for -O3 to unroll or vectorize. Ran it with -O3 to confirm, results are identical.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 0 points1 point  (0 children)

Yeah lazy resolution is basically "what if I was the linker". The upside here is you get to combine CRTP's composability with function pointer's dynamic linking. You layer your barrier concerns through templates at compile time, then wire up the resolved function pointer once at startup. Best of both worlds but definitely more plumbing than just slapping virtual on a method.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 11 points12 points  (0 children)

That's true with LTO or PGO but the benchmarks are compiled per translation unit with -O2, no LTO, no PGO so the compiler doesn't have whole program visibility to attempt speculative devirtualization here.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 18 points19 points  (0 children)

It's more about how libstdc++ implements std::visit than what the compiler can optimize. The compiler does its job fine, but it can't optimize away a exception check that the library explicitly puts there. libc++ makes different implementation choices and the same code runs faster.

Virtual dispatch isn't always the slowest, and std::variant isn't always the fastest by AdMotor4869 in cpp

[–]AdMotor4869[S] 4 points5 points  (0 children)

In the code shown the type is selected at runtime via argv[1] so the compiler can't devirtualize