When can the C++ compiler devirtualize a call?

· coding · Source ↗

TLDR

  • Modern C++ compilers reliably devirtualize calls to final methods but diverge significantly on corner cases involving dataflow, internal linkage, and conditional casts.

Key Takeaways

  • GCC, Clang, MSVC, and ICC all handle the trivial case (known concrete type on stack) but MSVC and ICC fail on simple Base* cast scenarios.
  • Marking a class or method final is the most portable devirtualization hint; GCC alone detects leafness via internal-linkage member types.
  • Anonymous namespace classes (internal linkage) can enable devirtualization within a single TU – a practical pattern for pImpl-style implementations.
  • GCC handles Derived::f defined directly but often misses inherited Derived::g – filed as GCC bug #99093.
  • LTO (link-time devirtualization) is explicitly out of scope; all results are single-TU, compiler-only analysis.

Hacker News Comment Review

  • Commenters agree devirtualization is too fragile to rely on in performance-critical paths; the practical advice is to avoid virtual calls there entirely or annotate explicitly with final.
  • A Clang crash was reported where profile-guided speculative devirtualization combined with BOLT’s identical code folding (ICF) produced incorrect vtable pointer validation – illustrating real production risk.
  • Rust’s dyn Trait devirtualization came up as a related open question; consensus is it’s less common in Rust due to ergonomic costs of Box<dyn>, reducing the problem surface compared to C++.

Notable Comments

  • @fibonacci112358: MSVC once had advanced inter-procedural devirtualization via LTO but it was disabled for being too buggy and slow, especially with DLLs injecting new derived classes.
  • @lowbloodsugar: Java’s JIT speculatively devirtualizes on call-site type profile, collapsing five or six virtual ByteBuffer calls into a single store instruction.

Original | Discuss on HN