ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SE-0524] Add withTemporaryAllocation using Output(Raw)Span
    Swift 2026. 5. 9. 13:50

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 SE-0524 — OutputSpan을 활용한 withTemporaryAllocation 추가에 대해 정리해보겠습니다 🙋🏻


    Intro

    • Proposal: SE-0524
    • Author: Max Desiatov
    • Review Manager: Doug Gregor
    • Status: Implemented (Swift 6.4)

    Motivation

    SE-0322와 SE-0437에서 도입된 withUnsafeTemporaryAllocation은 스택에 임시 메모리를 할당할 수 있는 유용한 API입니다.

    하지만 이 함수는 UnsafeMutableBufferPointer 또는 UnsafeMutableRawBufferPointer를 yield하기 때문에, 초기화와 해제를 개발자가 직접 관리해야 했어요.

    // 기존 방식 — 수동 관리가 필요해서 실수하기 쉬워요 🐛
    withUnsafeTemporaryAllocation(of: Float.self, capacity: 42) { buffer in
      // 초기화, 사용, 해제를 모두 직접 해야 함
      // 에러 발생 시 deinit을 빠뜨리면 버그로 이어짐
    }

    에러가 발생하는 경우에도 초기화된 요소를 올바르게 해제해야 한다는 점이 특히 까다로웠습니다.

    한편, SE-0485에서 도입된 OutputSpanOutputRawSpan연속적인 메모리 영역의 초기화 상태를 직접 추적하고 관리해주는 타입입니다.

    이 두 가지를 결합하면? 안전하고 편리한 임시 메모리 할당 API를 만들 수 있습니다!


    Proposed Solution

    withUnsafeTemporaryAllocation을 내부적으로 감싸는 새로운 전역 함수 두 가지를 추가합니다.

    raw 포인터 대신 inout OutputSpan 또는 inout OutputRawSpan을 yield하는 방식이에요.

    타입 지정 할당 (Typed Allocation)

    let capacity = 42
    let result = try withTemporaryAllocation(
      of: Float.self,
      capacity: capacity
    ) { output -> Int in
      for i in 0..<capacity {
        output.append(i)
      }
    
      var mutableSpan = output.mutableSpan
      updateInPlace(&mutableSpan)
    
      return aggregate(output.span)
    
      // 클로저가 끝나면 OutputSpan이 자동으로 해제됩니다
    }

    Raw 바이트 할당 (Raw Bytes Allocation)

    let byteCount = 16
    
    let result = try withTemporaryAllocation(
      byteCount: byteCount,
      alignment: 4
    ) { rawSpan -> Int in
      rawSpan.append(repeating: 0, count: byteCount, as: UInt8.self)
    
      var mutableBytes = rawSpan.mutableBytes
      updateInPlace(&mutableBytes)
    
      return aggregate(rawSpan.bytes)
    
      // OutputRawSpan은 클로저 종료 시 자동으로 해제됩니다
    }

    Detailed Design

    두 함수의 시그니처와 내부 동작을 살펴볼게요~

    ① withTemporaryAllocation(of:capacity:) — OutputSpan

    @available(SwiftCompatibilitySpan 5.0, *)
    @export(implementation)
    public func withTemporaryAllocation<T: ~Copyable, R: ~Copyable, E: Error>(
      of type: T.Type,
      capacity: Int,
      _ body: (inout OutputSpan<T>) throws(E) -> R
    ) throws(E) -> R where T: ~Copyable, R: ~Copyable {
      try withUnsafeTemporaryAllocation(of: type, capacity: capacity) { (buffer) throws(E) in
        var span = OutputSpan(buffer: buffer, initializedCount: 0)
        defer {
          let initializedCount = span.finalize(for: buffer)
          span = OutputSpan()
          buffer.extracting(.<initializedCount).deinitialize()
        }
        return try body(&span)
      }
    }

    내부 동작 흐름은 다음과 같습니다.

    1. AllocationwithUnsafeTemporaryAllocation(of:capacity:)로 초기화되지 않은 타입 버퍼를 확보합니다.
    2. Span CreationinitializedCount: 0으로 OutputSpan을 생성합니다.
    3. ExecutionOutputSpaninout 파라미터로 클로저에 전달합니다.
    4. Cleanupdefer 블록에서 finalize(for:)로 초기화된 요소 수를 파악하고, deinitialize()로 정리합니다.

    ② withTemporaryAllocation(byteCount:alignment:) — OutputRawSpan

    @available(SwiftCompatibilitySpan 5.0, *)
    @export(implementation)
    public func withTemporaryAllocation<R: ~Copyable, E: Error>(
      byteCount: Int,
      alignment: Int,
      _ body: (inout OutputRawSpan) throws(E) -> R
    ) throws(E) -> R where R: ~Copyable {
      try withUnsafeTemporaryAllocation(byteCount: byteCount, alignment: alignment) { (buffer) throws(E) in
        var span = OutputRawSpan(buffer: buffer, initializedCount: 0)
        defer {
          _ = span.finalize(for: buffer)
          span = OutputRawSpan()
        }
        return try body(&span)
      }
    }

    OutputRawSpan은 raw 바이트를 다루며 BitwiseCopyable로 간주되기 때문에, OutputSpan과 달리 deinitialize() 호출이 필요 없습니다.
    임시 메모리는 자동으로 해제됩니다.


    Source Compatibility / ABI

    • 이번 변경은 순수 additive 변경으로, 기존 코드에 영향을 주지 않습니다.
    • 두 함수 모두 @export(implementation)으로 표시되어 클라이언트 바이너리에 직접 인라인됩니다.
    • 표준 라이브러리의 새로운 ABI 엔트리 포인트가 아닌, 기존 ABI를 활용하는 방식이라 ABI 호환성에도 영향이 없습니다.

    Alternatives Considered

    아무것도 하지 않기

    기존 withUnsafe... 변형을 계속 사용하고 개발자가 직접 OutputSpan/OutputRawSpan으로 감싸는 방법이 있어요.
    하지만 동일한 보일러플레이트 코드를 반복해야 하기 때문에, 이번 제안처럼 표준 라이브러리에서 제공하는 것이 적절하다고 판단했습니다.

    OutputSpan / OutputRawSpan의 static 메서드로 추가하기

    각 span 타입의 static 메서드로 만드는 방법도 고려되었지만, withUnsafeTemporaryAllocation이나 withExtendedLifetime 같은 기존 전역 함수 패턴과 일관성을 유지하기 위해 전역 함수로 채택했습니다.


    Future Directions

    현재 제안은 주요 안전 래퍼를 다루고 있으며, 향후 async 오버로드도 고려할 수 있어요.

    다만 async 오버로드는 이 함수만을 위한 게 아니라 표준 라이브러리의 with 스타일 함수 전체에 걸쳐 일관되게 추가되어야 한다고 보고 있습니다.
    또한 suspension point에서의 할당은 힙 메모리를 사용하게 되어, 스택 할당의 이점이 사라진다는 점도 고려 중이에요.


    Conclusion

    withUnsafeTemporaryAllocationOutputSpan/OutputRawSpan의 강점을 결합한 실용적인 변경입니다 🙌

    기존에는 임시 메모리 할당 시 raw 포인터를 직접 다루며 초기화/해제 관리까지 책임져야 했는데, 이번 제안으로 안전하고 간결하게 임시 메모리를 사용할 수 있게 됩니다.

    특히 unsafe API에 익숙하지 않은 개발자도 스택 할당 임시 메모리를 부담 없이 활용할 수 있게 된다는 점에서 진입 장벽을 낮추는 변경이라고 생각합니다 😄


    References

     

    swift-evolution/proposals/0524-span-temporary-allocation.md at main · swiftlang/swift-evolution

    This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution

    github.com

Designed by Tistory.