Uploaded image for project: 'Swift'
  1. Swift
  2. SR-7541

Closure not optimized by closure specializer

    Details

    • Type: Bug
    • Status: Open
    • Priority: Medium
    • Resolution: Unresolved
    • Component/s: Compiler
    • Labels:
      None

      Description

      This program

      public struct SadByteBuffer {
          public init() {}
          public var x: Int = 3
      
          private func foo() {}
      
          @inline(never) // commenting out this line stops the allocations from happening
          public mutating func set(staticString string: StaticString, at index: Int) -> Int {
              self.x = string.withUTF8Buffer { ptr -> Int in
                  self.foo()
                  return 8
              }
              return self.x
          }
      }
      
      @inline(never)
      func foo(x: StaticString) -> SadByteBuffer {
          var y = SadByteBuffer()
          for _ in 0..<1_000_000 {
              _ = y.set(staticString: x, at: 0)
          }
          return y
      }
      
      let y = foo(x: "hello world this is some StaticString")
      print(y.x)
      

      in my mind shouldn't allocate for each call to y.set but it does. The self capture within

              self.x = string.withUTF8Buffer { ptr -> Int in
                  self.foo()
                  return 8
              }
      

      seems to cause it to allocate.

      Oddly enough withUTF8Buffer doesn't seem to be inlined into set and I don't really understand why. Interestingly, if you make the SadByteBuffer a class the allocation goes away! Kevin Sweeney suggested it might be an artefact of the exclusivity checking which sounds plausible.

      Regarding the @inline(never) that I have in there: This is just to simulate a cross-module call which is where I actually saw this. But I don't understand why this needs to be inlined, the set function doesn't use any generics or closures so it should just work fine without.

      This is the SIL btw:

      sil shared [noinline] @function signature specialization <Arg[0] = Exploded, Arg[1] = Dead> of test.SadByteBu
      ffer.set(staticString: Swift.StaticString, at: Swift.Int) -> Swift.Int : $@convention(method) (Builtin.Word, 
      Builtin.Word, Builtin.Int8, @inout SadByteBuffer) -> Int {
      // %0                                             // user: %9
      // %1                                             // user: %9
      // %2                                             // user: %9
      // %3                                             // users: %11, %6, %4
      bb0(%0 : $Builtin.Word, %1 : $Builtin.Word, %2 : $Builtin.Int8, %3 : $*SadByteBuffer):
        debug_value_addr %3 : $*SadByteBuffer, var, name "self", argno 3 // id: %4
        // function_ref closure #1 in SadByteBuffer.set(staticString:at:)
        %5 = function_ref @closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> Swift.Int in test.SadByteBuffer.set(staticString: Swift.StaticString, at: Swift.Int) -> Swift.Int : $@convention(thin) (UnsafeBufferPointer<UInt8>, @inout_aliasable SadByteBuffer) -> Int // user: %6
        %6 = partial_apply [callee_guaranteed] %5(%3) : $@convention(thin) (UnsafeBufferPointer<UInt8>, @inout_aliasable SadByteBuffer) -> Int // users: %10, %7
        %7 = convert_escape_to_noescape %6 : $@callee_guaranteed (UnsafeBufferPointer<UInt8>) -> Int to $@noescape @callee_guaranteed (UnsafeBufferPointer<UInt8>) -> Int // user: %9
        // function_ref specialized StaticString.withUTF8Buffer<A>(_:)
        %8 = function_ref @function signature specialization <Arg[0] = Exploded> of function signature specialization <Arg[0] = [Closure Propagated : reabstraction thunk helper from @callee_guaranteed (@unowned Swift.UnsafeBufferPointer<Swift.UInt8>) -> (@unowned Swift.Int) to @escaping @callee_guaranteed (@unowned Swift.UnsafeBufferPointer<Swift.UInt8>) -> (@out Swift.Int), Argument Types : [@callee_guaranteed (@unowned Swift.UnsafeBufferPointer<Swift.UInt8>) -> (@unowned Swift.Int)]> of generic specialization <Swift.Int> of Swift.StaticString.withUTF8Buffer<A>((Swift.UnsafeBufferPointer<Swift.UInt8>) -> A) -> A : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Int8, @noescape @callee_guaranteed (UnsafeBufferPointer<UInt8>) -> Int) -> Int // user: %9
        %9 = apply %8(%0, %1, %2, %7) : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Int8, @noescape @callee_guaranteed (UnsafeBufferPointer<UInt8>) -> Int) -> Int // users: %13, %12
        strong_release %6 : $@callee_guaranteed (UnsafeBufferPointer<UInt8>) -> Int // id: %10
        %11 = struct_element_addr %3 : $*SadByteBuffer, #SadByteBuffer.x // user: %12
        store %9 to %11 : $*Int                         // id: %12
        return %9 : $Int                                // id: %13
      } // end sil function 'function signature specialization <Arg[0] = Exploded, Arg[1] = Dead> of test.SadByteBuffer.set(staticString: Swift.StaticString, at: Swift.Int) -> Swift.Int'
      

      and here the IR

      ; Function Attrs: noinline
      define linkonce_odr hidden swiftcc i64 @"function signature specialization <Arg[0] = Exploded, Arg[1] = Dead> of test.SadByteBuffer.set(staticString: Swift.StaticString, at: Swift.Int) -> Swift.Int"(i64, i64, i8, %T4test13SadByteBufferV* nocapture swiftself dereferenceable(8)) local_unnamed_addr #3 {
      entry:
        %4 = tail call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i64 0, i32 2), i64 24, i64 7) #5
        %5 = getelementptr inbounds %swift.refcounted, %swift.refcounted* %4, i64 1
        %6 = bitcast %swift.refcounted* %5 to %T4test13SadByteBufferV**
        store %T4test13SadByteBufferV* %3, %T4test13SadByteBufferV** %6, align 8
        %7 = bitcast %swift.refcounted* %4 to %swift.opaque*
        %8 = tail call swiftcc i64 @"function signature specialization <Arg[0] = Exploded> of function signature specialization <Arg[0] = [Closure Propagated : reabstraction thunk helper from @callee_guaranteed (@unowned Swift.UnsafeBufferPointer<Swift.UInt8>) -> (@unowned Swift.Int) to @escaping @callee_guaranteed (@unowned Swift.UnsafeBufferPointer<Swift.UInt8>) -> (@out Swift.Int), Argument Types : [@callee_guaranteed (@unowned Swift.UnsafeBufferPointer<Swift.UInt8>) -> (@unowned Swift.Int)]> of generic specialization <Swift.Int> of Swift.StaticString.withUTF8Buffer<A>((Swift.UnsafeBufferPointer<Swift.UInt8>) -> A) -> A"(i64 %0, i64 %1, i8 %2, i8* bitcast (i64 (i64, i64, %swift.refcounted*)* @"partial apply forwarder for closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> Swift.Int in test.SadByteBuffer.set(staticString: Swift.StaticString, at: Swift.Int) -> Swift.Int" to i8*), %swift.opaque* %7)
        tail call void @swift_release(%swift.refcounted* %4) #5
        %.x._value = getelementptr inbounds %T4test13SadByteBufferV, %T4test13SadByteBufferV* %3, i64 0, i32 0, i32 0
        store i64 %8, i64* %.x._value, align 8
        ret i64 %8
      }
      

      note the

        %4 = tail call noalias %swift.refcounted* @swift_allocObject(%swift.type* getelementptr inbounds (%swift.full_boxmetadata, %swift.full_boxmetadata* @metadata, i64 0, i32 2), i64 24, i64 7) #5
      

      this is the test I run to show the number of allocations

      swiftc -O test.swift && sudo dtrace -n 'pid$target::malloc:entry { @malloc_calls[ustack()] = count(); } ::END { printa(@malloc_calls); }' -c ./test
      

      which outputs

                    libsystem_malloc.dylib`malloc
                    libswiftCore.dylib`swift_slowAlloc+0x19
                    libswiftCore.dylib`_swift_allocObject_(swift::TargetHeapMetadata<swift::InProcess> const*, unsigned long, unsigned long)+0x14
                    test`specialized SadByteBuffersetat(_:_:)+0x2a
                    test`specialized testx(_:)+0x3e
                    test`main+0x21
                    libdyld.dylib`start+0x1
                    test`0x1
                1000000
      

      at the end so one allocation there per loop iteration .

      and btw

      $ jw-swift-latest swiftc -version
      Apple Swift version 4.2-dev (LLVM 44fac26087, Clang 1ecf6042e0, Swift 659e7ceb2d)
      Target: x86_64-apple-darwin17.6.0
      

      but behaves the same in 4.1

        Attachments

          Activity

            People

            • Assignee:
              aschwaighofer@apple.com Arnold Schwaighofer
              Reporter:
              jw Johannes Weiss
            • Votes:
              0 Vote for this issue
              Watchers:
              5 Start watching this issue

              Dates

              • Created:
                Updated: