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

陌路散爱 提交于 2019-12-22 08:26:04

问题


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?


回答1:


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.



来源:https://stackoverflow.com/questions/52374392/does-the-swift-compiler-linker-automatically-remove-unused-methods-classes-exten

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!