How effective is final for code optimization?

Hello, I want to ask how effective is final for code optimization?

Within a module, many of the things final does for you are also done by whole module optimization. Across modules or in non-wmo mode it can be a significant win.

1 Like

Within a module, it only helps if whole module optimization is disabled. (Though it might allow the optimization to be done faster during the build.)

Across module boundaries the keyword makes no difference, since anything not open is implicitly final. (Back in the early days, open was implicit, so final was much more important.)

[Edit: The crossed out statement turned out to be inaccurate. See later comments.]

The main advantage is to prevent you or another developer on your team from accidentally subclassing it in the same module when it wasnā€™t designed to be subclassed. Even if the logic is compatible, the existence of a subclass causes method calls to need to switch to slower dynamic dispatch.

1 Like

That's a fair point, I was more thinking of final the semantic behavior (e.g. not-open), rather than final the keyword.

Not quite. Anything not open is not externally subclassable. If it's not final, it may still have subclasses in the defining module, which may or may not be public themselves, and future versions of the library are allowed to turn the class into an open one while maintaining API and ABI compatibility. Code outside the module has to treat a public method as if it may be overridden.

5 Likes

Does that only apply to library evolution mode? Or does it also suffer the performance hit between modules built together from source, such as package targets?

Without library evolution mode, I think we would still be more conservative across modules, since there's still no cross-module "whole program" optimization level and the compilation model for modules is therefore still completely independent. That could change in the future, though, and without library evolution mode, you'd get somewhat lower overhead for dynamic dispatch since the compiler will make stronger assumptions about the layout of dispatch tables.

3 Likes

I donā€™t mean from the library side. Removing a final keyword is ABIā€breaking (library evolution level) but API compatible (normal mode level). (Isnā€™t it?) Why does a normal module not just automatically add final to its public interface where possible as part of optimization?

Never mind. Because that could enable it to silently do the reverseā€”add a final keywordā€”which is not API compatible.

Yes it is API compatible. It would have to also remove open to cause an APIā€breaking change. (Wouldnā€™t it?)

Removing final is not API-compatible, since you're weakening a promise to your clients that there are no subclasses of the class in question.

But there is no exhaustive switch over subclasses or anything like that could actually cause source code to break. It seems more akin to removing an @inlinable attribute to meā€”which also weakens your promises about how much optimization is available, but cannot break client sourceā€”is that an APIā€breaking change too?

One specific thing that can break is that x is/as T is an exact type equality check if T is final, but requires a runtime call to check for subclasses of T if T is not known to be final. Furthermore, protocol conformances will allow non-covariant exact type witnesses for final classes, whereas for a nonfinal class, you would need to use Self returns or required inits to satisfy Self or init protocol requirements.

1 Like

It makes my head spin, but youā€™re right. Oh well, I guess the takeaway is that:

  • final does unblock a lot more optimization than I realized,
  • but it should be used with care, since it has farā€flung and nonā€obvious API consequences.