Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does the swift compiler/linker automatically remove unused methods/classes/extensions, etc.?

We have a lot of code which is usable in any iOS application we write. Things such as:

  • Custom/Common Controls
  • Extensions on common objects like UIView, UIImage and UIViewController
  • Global utility functions
  • Global constants
  • Related sets of files that make up common 'features' like a picker screen that can be used with anything that can be enumerated.

For reasons unrelated to this question, we cannot use static or dynamic libraries. These must be included in the project as actual source files.

There are several hundred of these 'core' files so what I've been doing is adding all the files to the project (referencing a shared location on disk), but only adding them to specific targets as they are used/needed.

The problem is this becomes quite tedious to do, especially when there are sets of related files which all reference others. Tracking them down one by one is a real pain!

What I'm wondering is if I can simply include everything in the target, then count on the compiler to remove all the unused code.

For instance, I have several extension methods on UIView. If I don't use them in a particular target, will the compiler exclude that code from the compiled binary, or will it be compiled in, unreachable, bloating the code size for no reason?

like image 574
Mark A. Donohoe Avatar asked Sep 17 '18 19:09

Mark A. Donohoe


People also ask

Does linker remove unused functions?

So the linker is able to remove each individual function because it is in its own section. So enabling this for your library will allow the linker to remove unused functions from the library.

How do I remove unused codes in Swift?

Those who are looking for a way to remove unused code from Swift project, it can be done easily using Periphery. Periphery is a tool to remove unused code from Swift projects. It can detect unused function, struct, class, protocols etc.

How do I find unused files in Xcode?

xcutility is a tool to find and delete unused files from Xcode projects. It recursively searches through a path to find all of the path's Xcode projects and files, and will tell you which files are not referenced or built in any of your Xcode projects.


1 Answers

Dead functions

The compiler's SILOptimizer has a dead function elimination pass, which eliminates functions and methods that are known not to be called (along with any associated vtable/witness table entries).

To fully take advantage of this, you'll want to be using whole module optimisation (-wmo) in order to ensure that the compiler can analyse whether internal functions are called or not from within the same module.

You can quite easily test this out for yourself, for example using the following code:

class C {}
extension C {
  @inline(never) func foo() {}
  @inline(never) func bar() {}
}

@inline(never) func foo() {}
@inline(never) func bar() {}
bar()

let c = C()
c.bar()

(I'm using the unofficial @inline(never) here to ensure the functions aren't optimised away by inlining)

If we run xcrun swiftc -emit-sil -O -wmo main.swift | xcrun swift-demangle, we can see the generated SIL:

sil_stage canonical

import Builtin
import Swift
import SwiftShims

class C {
  init()
  deinit
}

extension C {
  @inline(never) func foo()
  @inline(never) func bar()
}

@inline(never) func foo()

@inline(never) func bar()

let c: C

// c
sil_global hidden [let] @main.c : main.C : $C

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  // function_ref bar()
  %2 = function_ref @main.bar() -> () : $@convention(thin) () -> () // user: %3
  %3 = apply %2() : $@convention(thin) () -> ()
  alloc_global @main.c : main.C                  // id: %4
  %5 = global_addr @main.c : main.C : $*C        // user: %8
  %6 = alloc_ref $C                               // users: %8, %7
  debug_value %6 : $C, let, name "self", argno 1  // id: %7
  store %6 to %5 : $*C                            // id: %8
  // function_ref specialized C.bar()
  %9 = function_ref @function signature specialization <Arg[0] = Dead> of main.C.bar() -> () : $@convention(thin) () -> () // user: %10
  %10 = apply %9() : $@convention(thin) () -> ()
  %11 = integer_literal $Builtin.Int32, 0         // user: %12
  %12 = struct $Int32 (%11 : $Builtin.Int32)      // user: %13
  return %12 : $Int32                             // id: %13
} // end sil function 'main'

// C.__deallocating_deinit
sil hidden @main.C.__deallocating_deinit : $@convention(method) (@owned C) -> () {
// %0                                             // users: %3, %2, %1
bb0(%0 : $C):
  debug_value %0 : $C, let, name "self", argno 1  // id: %1
  debug_value %0 : $C, let, name "self", argno 1  // id: %2
  dealloc_ref %0 : $C                             // id: %3
  %4 = tuple ()                                   // user: %5
  return %4 : $()                                 // id: %5
} // end sil function 'main.C.__deallocating_deinit'

// bar()
sil hidden [noinline] @main.bar() -> () : $@convention(thin) () -> () {
bb0:
  %0 = tuple ()                                   // user: %1
  return %0 : $()                                 // id: %1
} // end sil function 'main.bar() -> ()'

// specialized C.bar()
sil shared [noinline] @function signature specialization <Arg[0] = Dead> of main.C.bar() -> () : $@convention(thin) () -> () {
bb0:
  %0 = tuple ()                                   // user: %1
  return %0 : $()                                 // id: %1
} // end sil function 'function signature specialization <Arg[0] = Dead> of main.C.bar() -> ()'

sil_vtable C {
  #C.deinit!deallocator: @main.C.__deallocating_deinit  // C.__deallocating_deinit
}

You'll note that only the bar function and method have had their bodies emitted (near the bottom). While there are still definitions for foo at the top of the SIL, they get removed as the SIL is lowered to LLVM IR.

I was wondering if you could mark-up/attribute a method saying 'Don't remove this!' like you can in other languages

There isn't currently an official attribute for this, but there is an underscored @_optimize(none) attribute which tells the optimiser not to touch something:

@_optimize(none) func foo() {}

Though given the attribute is underscored, use at your own risk.


Dead type metadata

Unfortunately, the compiler currently (as of Swift 4.2) doesn't appear to have an optimisation pass that eliminates the associated metadata for types that are known to not be used.

like image 142
Hamish Avatar answered Sep 28 '22 02:09

Hamish