Dynamic Dispatch and Whole Module Optimization

A quick follow up on my earlier test of Whole Module Optimization that used a generic function. Nikolaj commented that the benefit would be even greater for method calls of a class. I found some time this week to run some tests.

Test Code

To test the impact of whole module optimization I have a simple Employee class with a function didSomething:

// Employee.swift
class Employee {
  var name: String    

  init(withName name: String) {
    self.name = name
  }

  func didSomething() -> Bool {
    // Some complicated implementation
    // details go here
    return false
  }    
}

The calling code to exercise the function is as follows:

let tim = Employee(withName:"Tim Cook")    
let startTime = CFAbsoluteTimeGetCurrent()
for _ in 1...iterations {
  if tim.didSomething() {
    print("Yes!")
  }
}
let endTime = CFAbsoluteTimeGetCurrent()
let result = endTime - startTime

Test Results

As before I ran the test on an old 5th generation iPod Touch with varying levels of compiler optimization. The test function has almost no work to do so I ran it for 1,000,000 iterations so it would take a measurable amount of time.

Optimization Level: None [-ONone]

  • 1,000,000 iterations: 40 milliseconds

Optimization Level: Fast [-O]

  • 1,000,000 iterations: 17 milliseconds

Optimization Level: Fast,WMO [-O -whole module optimization]

  • 1,000,000 iterations: 0.02 milliseconds

What Does It Mean?

This was necessarily a somewhat contrived test but it does show how dramatic the improvement can be for time sensitive code when you turn on whole module optimization. (0.02 ms vs 17ms).

To understand why whole module optimization has such an impact remember what is happening at runtime when you call a method on an object. The object could be a subclass which has overriden the doSomething method. To allow for this the code must lookup the method at runtime in a method table to then indirectly call the base or overriden method.

This dynamic dispatch technique is powerful but not without performance cost. With default optimization the compiler works on a single source file at a time. The compiler cannot see the class definition when it is compiling the calling code so must rely on dynamic dispatch.

With whole module optimization the compiler can see both the calling code and the class definition even if they are in separate source files (technically they must be in the same module). Now it can check for sure if a method is overriden and replace the dynamic dispatch with a much faster direct method call.

Further Reading

See my earlier post for details on turning on whole module optimization:

The Apple Swift blog went into more detail on using final, private and Whole Module Optimization to reduce dynamic dispatch: