File: NamedReturnValueOptimization.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (129 lines) | stat: -rw-r--r-- 4,688 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//===--- NamedReturnValueOptimization.swift --------------------------------==//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SIL

/// Removes a `copy_addr` to an indirect out argument by replacing the source of the copy
/// (which must be an `alloc_stack`) with the out argument itself.
///
/// The following SIL pattern will be optimized:
///
///   sil @foo : $@convention(thin) <T> () -> @out T {
///   bb0(%0 : $*T):
///     %2 = alloc_stack $T
///     ...
///     copy_addr %some_value to [init] %2       // or any other writes to %2
///     ...
///   bbN:
///     copy_addr [take] %2 to [init] %0 : $*T   // the only use of %0
///     ... // no writes
///     return
///
/// to:
///
///   sil @foo : $@convention(thin) <T> (@out T) -> () {
///   bb0(%0 : $*T):
///     %2 = alloc_stack $T                      // is dead now
///     ...
///     copy_addr %some_value to [init] %0
///     ...
///   bbN:
///     ...
///     return
///
/// This optimization can be done because we know that:
/// * The out argument dominates all uses of the copy_addr's source (because it's a function argument).
/// * It's not aliased (by definition). We can't allow aliases to be accessed between the initialization and the return.
///
/// This pass shouldn't run before serialization. It might prevent predictable memory optimizations
/// in a caller after inlining, because the memory location (the out argument = an alloc_stack in the caller)
/// might be written multiple times after this optimization.
///
let namedReturnValueOptimization = FunctionPass(name: "named-return-value-optimization") {
  (function: Function, context: FunctionPassContext) in

  for outArg in function.arguments[0..<function.numIndirectResultArguments] {
    if let copyToArg = findCopyForNRVO(for: outArg) {
      performNRVO(with: copyToArg, context)
    }
  }
}

/// Returns a copy_addr which copies from an alloc_stack to the `outArg` at the end of the function.
///
private func findCopyForNRVO(for outArg: FunctionArgument) -> CopyAddrInst? {
  guard let singleArgUse = outArg.uses.ignoreDebugUses.singleUse,
        let copyToArg = singleArgUse.instruction as? CopyAddrInst else {
    return nil
  }

  assert(singleArgUse == copyToArg.destinationOperand,
         "single use of out-argument cannot be the source of a copy")

  // Don't perform NRVO unless the copy is a [take]. This is the easiest way
  // to determine that the local variable has ownership of its value and ensures
  // that removing a copy is a reference count neutral operation. For example,
  // this copy can't be trivially eliminated without adding a retain.
  //   sil @f : $@convention(thin) (@guaranteed T) -> @out T
  //   bb0(%in : $*T, %out : $T):
  //     %local = alloc_stack $T
  //     store %in to %local : $*T
  //     copy_addr %local to [init] %out : $*T
  if !copyToArg.isTakeOfSrc {
    return nil
  }

  guard let sourceStackAlloc = copyToArg.source as? AllocStackInst else {
    return nil
  }

  // NRVO for alloc_stack [dynamic_lifetime] will invalidate OSSA invariants.
  if sourceStackAlloc.hasDynamicLifetime && copyToArg.parentFunction.hasOwnership {
    return nil
  }

  if !(copyToArg.parentBlock.terminator is ReturnInst) {
    return nil
  }

  // This check is overly conservative, because we only need to check if the source
  // of the copy is not written to. But the copy to the out argument is usually the last
  // instruction of the function, so it doesn't matter.
  if isAnyInstructionWritingToMemory(after: copyToArg) {
    return nil
  }

  return copyToArg
}

private func performNRVO(with copy: CopyAddrInst, _ context: FunctionPassContext) {
  copy.source.replaceAllUsesExceptDealloc(with: copy.destination, context)
  assert(copy.source == copy.destination)
  context.erase(instruction: copy)
}

private func isAnyInstructionWritingToMemory(after: Instruction) -> Bool {
  var followingInst = after.next
  while let fi = followingInst {
    if fi.mayWriteToMemory && !(fi is DeallocStackInst) {
      return true
    }
    followingInst = fi.next
  }
  return false
}

private extension Value {
  func replaceAllUsesExceptDealloc(with replacement: Value, _ context: some MutatingContext) {
    uses.lazy.filter{!($0.instruction is Deallocation)}.replaceAll(with: replacement, context)
  }
}