Swift/개념 & 응용

[Swift] 이제는 Int(String(Substring))과 Int(Substring) 비교가 의미 없는 이유

유정주 2022. 5. 29. 13:59
반응형

안녕하세요. 개발하는 정주입니다.

 

오늘은 "이제는 Int(String(Substring))과 Int(Substring) 비교가 의미 없는 이유"에 대해 알아보겠습니다.

작성하면서 느낀 점은 지금의 제 수준으로는 소화하기가 어려웠다는 점입니다...

혹시 틀린 점이 있다면 알려주시면 감사하겠습니다.

 


서론

알고리즘 문제를 풀면서 String을 Int로 바꿔야할 때가 많습니다.

split( )으로 문자열을 분리하면 Substring으로 return이 되는데요.

이때, Int(String(Substring))과 Int(Substring)의 속도 차이가 있는 것을 알고 계셨나요??

 

오늘은 그 이유에 대해 알아보도록 합시다.

그리고 이 비교가 이제는 의미가 없는 이유에 대해서도 다뤘습니다.


속도 테스트

일단 String으로 변환하여 Int로 캐스팅하는 것이 정말 빠른지, 얼마나 빠른지 알아보도록 합시다.

 

테스트 String은 1을 1천 만 회 반복한 문자열입니다. 이 문자열을 split으로 나눠 [Substring]을 준비했습니다.

let time = 10000000
let testArr: [Substring] = String(repeating: "1 ", count: time).split { $0 == " " }

 

이 배열을 map을 이용해 Int(String(Substring))과 Int(Substring)로 변환하는 시간을 비교해보았습니다.

각 10회씩 반복하였습니다.

var stringTime: TimeInterval = 0, subStringTime: TimeInterval = 0

for i in 1...10 {
    let result = measureTime {
        let _ = testArr.map { Int(String($0))! }
    }
    stringTime += result
}


for i in 1...10 {
    let result = measureTime {
        let _ = testArr.map { Int($0)! }
    }
    subStringTime += result
}

 

결과는 어떨까요?

Int(String(Substring))은 1천만 당 2초의 시간이 소요되었고

Int(Substring)은 1천만 당 2.2초의 시간이 소요되었습니다.

 

10회를 반복한 총 시간은 전자가 20.6, 후자가 22.5로 Int(String(Substring))이 10% 더 빨랐습니다.

절대적인 수치로 보면 차이가 큰가 싶지만 일단 속도 차이가 있긴 있었습니다.

 


Substring이란?

먼저 Substring이 무엇인지 알면 좋습니다.

 

 

[Swift] 공식 문서 - 문자열과 문자 (Strings and Characters)

안녕하세요. 개발하는 정주입니다. 오늘은 문자열과 문자 (Strings and Characters)를 정리해보겠습니다. * 공식 문서 내용을 한 줄 한 줄 읽는 것에 의의를 두었습니다. * 파파고의 힘을 빌려 번역했

jeong9216.tistory.com

 

 

Apple Developer Documentation

 

developer.apple.com

제가 정리했던 Swift 공식 문서의 Strings and Characters에서 Substring을 소개하고 있습니다.

애플 공식문서에도 Substring을 따로 다루고 있습니다.

 

Substring은 String과 마찬가지로 문자열을 구성하는 characters가 저장되는 메모리 영역을 가지고 있습니다.

Substring은 성능 최적화를 위해 원본 String을 저장하는 메모리 영역을 재사용할 수 있습니다.

 

아래 그림을 보면 Substring은 원본 String의 메모리 영역을 가리키는 것을 볼 수 있습니다.

 

 

이 성능 최적화로 인해 String 혹은 Substring을 수정할 때까지 메모리 복사 성능 비용을 절약할 수 있지만,

원본 문자열의 메모리를 참조하는 것이기 때문에 메모리 회수가 늦어질 수 있습니다.

따라서 공식 문서에서도 "Don’t store substrings longer than you need them to perform a specific operation." 라고 경고하고 있네요.

 


속도 차이가 발생"했던" 이유

Int(String(Substring))이 Int(Substring)보다 빠른 이유를 알아봅시다.

 

Swift는 코드가 공개되어 있기 때문에 IntegerParsing 코드를 살펴볼 수 있습니다.

주의할 점은 최신 버전이 아닌 Swift 5.4를 살펴봐야 한다는 점입니다.

 

GitHub - apple/swift: The Swift Programming Language

The Swift Programming Language. Contribute to apple/swift development by creating an account on GitHub.

github.com

 

 

  public init?<S: StringProtocol>(_ text: S, radix: Int = 10) {
    _precondition(2...36 ~= radix, "Radix not in range 2...36")

    if let str = text as? String, str._guts.isFastUTF8 {
      guard let ret = str._guts.withFastUTF8 ({ utf8 -> Self? in
        var iter = utf8.makeIterator()
        return _parseASCII(codeUnits: &iter, radix: Self(radix))
      }) else {
        return nil
      }
      self = ret
      return
    }

    // TODO(String performance): We can provide fast paths for common radices,
    // native UTF-8 storage, etc.
    var iter = text.utf8.makeIterator()
    guard let ret = Self._parseASCIISlowPath(
      codeUnits: &iter, radix: Self(radix)
    ) else { return nil }

    self = ret
  }

 

String은 FastUTF8을 확인하고, 이 부분이 true라면 바로 파싱된 값을 반환합니다.

 

이 str._guts.isFastUTF8은 아래처럼 구현되어 있습니다.

internal var isFastUTF8: Bool { return _fastPath(_object.providesFastUTF8) }

 

자세한건 모르겠으나, fastPath를 지원한다면 빠르게 파싱이 된다는 점은 알겠습니다.

 

만약 fastPath를 지원하지 않는다면, parseASCIISlowPath를 수행합니다.

이름만 봐도 fast보다는 느릴 것이라는 것이 느껴집니다.

 

결론은 Substring은 slowPath로 분기가 되고 String(Substring)은 fastPath로 분기가 되어서

Int(String(Substring))이 빨랐던 것입니다.

 

이때는 실제로 10배나 차이가 났다고 하네요.

 


Swift 5.5부터는 비교가 의미 없다.

Swift 5.4에서는 저랬고... 지금의 IntegerParsing은 어떤지 볼까요?

@inlinable
  @inline(__always)
  public init?<S: StringProtocol>(_ text: S, radix: Int = 10) {
    _precondition(2...36 ~= radix, "Radix not in range 2...36")
    guard _fastPath(!text.isEmpty) else { return nil }
    let result: Self? =
      text.utf8.withContiguousStorageIfAvailable {
        _parseInteger(ascii: $0, radix: radix)
      } ?? _parseInteger(ascii: text, radix: radix)
    guard let result_ = result else { return nil }
    self = result_
  }

fastPath, slowPath가 사라지고 utf8.withContiguousStorageIfAvailable로 통합되었습니다.

withContiguousStorageIfAvailable는 메모리에서 문자에 직접 접근할 수 있어 인덱스의 번거로움을 줄일 수 있다고 합니다.

 

즉, 이제는 String이던 Substring이던 충분히 빠른 속도로 Integer Parsing이 가능하다는 점입니다.

 


결론

해당 주제에 대해 검색해보며 Substring의 속돠 개선된 후에는 모두가 하나의 주장을 펼치고 있었습니다.

속도만큼 코드의 양과 가독성도 중요하다는 점입니다.

이제는 속도는 충분히 빨라졌으니 개발자가 협업하기 좋은 코드를 작성하라는 의미라고 생각합니다.

 

여러 가지로 배운 것이 많은 포스팅이었습니다.

 

감사합니다!

 


참조 

https://icksw.tistory.com/218

https://github.com/apple/swift/blob/main/stdlib/public/core/StringProtocol.swift

https://intrepidgeeks.com/tutorial/method-of-quickly-reading-numbers-using-swift

https://forums.swift.org/t/parse-int-from-substring-slower-than-string/50516


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

반응형