`Final` Optimization Recommendations

Hi there,

I’m just looking into recommendations for our team around optimizations.

In this optimization guide it mentions the recommendation to add the final keyword to reduce dynamic dispatch. Through other talks and threads, it has also been mentioned that final is inferred during Whole Module Optimization builds for non-open classes as there is no possibility of overriding these classes after the fact.

Considering that WMO builds are the default for Xcode release builds now, should this guidance be updated to reflect that this is unnecessary in these circumstances?

I’ve noticed many workplaces I’ve written Swift code require prepending all classes with final as a matter of policy for the optimization potential. Doesn’t this setting, however, make such requirements redundant? Also, doesn’t this negate the power of the keyword in actually enforcing at compile time a restriction when it’s actually important that a class, property or method should not be overridden by subclasses?

I’m curious what those here who work on the language think the best recommendations are, and whether we could update documentation to clear this up.

3 Likes

If preserving this optimization is important, then it is important that a class not be overridden. If a co-worker subclasses your class in a different part of the project, then you lose any inferred optimization with no warning. This would be suboptimal if you were relying on it.

Absolutely. This would be another good example of using this keyword to ensure a component is not overridden when it must not be for specific reason.

I think this recommendation however has been misunderstood and led to many organizations instituting a global “tag everything as final for great optimisation”. This then reduces the value of the keyword in these cases.

I would say you should be using final regardless of the optimizations it enables. Generally it’s always best to avoid subclassing in Swift unless you really need it. A lot of tasks that normally would be expressed by subclassing can be better represented through protocols.

Having a mindset of classes should be final first makes it very clear you’re using classes for their reference semantics and not their inheritance capabilities.

2 Likes

That’s a highly opinionated programming style that tends not to scale well when utilizing existing frameworks. Also, this doesn't delineate between "we discourage subclassing as a design paradigm" and "this must not be subclassed for performance or other reasons". One clearly should have compiler assistance. The other might better be served by linting with something like SwiftLint.

That said, if this is the default for Swift, we should make final the default, and subclassable the option, not overburden everyone’s code with tons of annotations as “policy”, especially considering we get the optimization either way with WMO.

2 Likes

It is opinionated but it is not accurate to say it doesn’t scale well when utilizing existing frameworks. It is accurate to say that subclassing is required, but only subclassing of framework classes, not your own classes. There is rarely a need to write your own superclasses.

There many of us who wish final was the default and argued for exactly that in the early days of Swift Evolution, but I think that ship has sailed at this point.

1 Like

I personally disagree with this on a philosophical level, but I see the point.

Nevertheless, could we look to clarify the documentation around this to make it clear WMO also gives you these optimizations? It can be a little confusing. Most developers I've quizzed about their "tag everything as final" rule say they do so for the optimisation benefit, and are confused when I say "that comes for free anyway with the default optimisation."

That will then leave it more fairly for the developer's team to create policy with a more clear viewpoint of the reasons one might use this keyword.

Note the preface to that document "The intended audience of this document is compiler and standard library developers."

Since the standard library is not a standalone binary like an app is, even WMO can't give the compiler full visibility into the set of types. For a concrete example, I've recently been looking into overhead of Dictionary bridging in JSON decoding, and was able to get a significant performance win by forcing the compiler to generate a specialization of the bridging function for the [String:Any] case. If this code was inlined into WMO-compiled apps, that would have been unnecessary, but in non-inline library code it was worthwhile.

6 Likes

Thanks for the pickup. That's a really good point that I didn't realise.

That’s a highly opinionated programming style

If an opinion is backed by some data or facts, it becomes an argument :slight_smile:

Here is my little test of compiling Swift source files from clean, based on ~400 classes:
Left side: all classes were marked as final
Right side: final removed from all classes

no final all final
49.6s 39.9s
41.5s 44.1s
50.6s 40.1s
49.3s 38.2s

Data set is small and I am not providing build configuration, etc, but that is intentional. All projects are different so results might vary. Making little test like the one above could drive the decision. There is no one size fits all.

1 Like

In order to narrow down the performance here, I adapted a previous benchmark to generate 1000 classes, either final or not, and compile them.

#!/usr/bin/env python3
import os

filenames = ["class", "final"]
code = [
  'class A{} {{}}',
  'final class B{} {{}}'
]

for (i, filename) in enumerate(filenames):
    with open(filename + ".swift", "w") as f:
        s = ""
        for j in range(1000):
            s += (code[i] + '\n').format(j)
        f.write(s)
    os.system("hyperfine 'xcrun swiftc {}' --warmup 2".format(filename + ".swift"))

Results say they're equivalent as far as compilation speed goes:

Benchmark #1: xcrun swiftc class.swift
  Time (mean ± σ):      1.285 s ±  0.015 s    [User: 1.210 s, System: 0.079 s]
  Range (min … max):    1.260 s …  1.314 s    10 runs
 
Benchmark #1: xcrun swiftc final.swift
  Time (mean ± σ):      1.276 s ±  0.011 s    [User: 1.201 s, System: 0.078 s]
  Range (min … max):    1.260 s …  1.293 s    10 runs

I wouldn't expect final to have a large impact on compile times. It'll eliminate some small overheads proportionate to the number of dynamically-dispatched methods, but that's not really where the compiler is spending most of its time; mostly it'll save time by decreasing code size and thus requiring less I/O. It can also create optimization opportunities, which can always be a compile-time trade-off: you do more work to do the optimization, but maybe it means you shrink the code and later stages go faster.

2 Likes

As mentioned in the guide " This mode is enabled using the swiftc command line flag -whole-module-optimization . Programs that are compiled in this mode will most likely take longer to compile, but may run faster."
So, why not add final manually and enjoy optimized compile time and run time :slight_smile:

Personally, I treat final as a default, to be removed as needed. To a lesser degree, I do the same with private.

As a rule, making stronger guarantees allows the compiler to optimize much more aggressively.

1 Like

FWIW, the reason final isn’t the default is mostly for modules with library evolution enabled, because clients will skip dynamic dispatch for those classes. But it matters even for regular public classes when it comes to adding protocols: a final class does not need required initializers to satisfy protocol requirements (because there are no subclasses). So removing final from a public class is a breaking change even for libraries without binary compatibility concerns.

5 Likes