Upgrade to Pro — share decks privately, control downloads, hide ads and more …

No Strings Attached

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for ellsk1 ellsk1
September 20, 2019

No Strings Attached

Swift String is one of the most commonly used fundamental types in the Swift standard library, but it has pitfalls that are easily overlooked. At Uber, we work with multiple apps that are deployed in multiple markets and localized in over 50 languages, which rely heavily on correctness and safety of String. We’ve also built multiple code generators and other developer tools that involve a large amount of String processing, and thus String performance can have a huge impact. This talk will discuss various scenarios where we encountered pitfalls of Swift Strings, and the methods we used to resolve challenges from both the correctness and performance perspectives. It will also cover what you can do for your apps or tools to safeguard and optimize if you use Strings a lot.

Avatar for ellsk1

ellsk1

September 20, 2019

More Decks by ellsk1

Other Decks in Technology

Transcript

  1. Commonly used type in standard library Unicode encoding Non-Latin languages

    / Emojis let hi = "Hi ‍‍! It’s ☀ on " Swift String
  2. "‍‍".count == 1 "‍‍".utf8.count == 25 ‍‍ Code Point U+1F468

    200D U+1F469 200D U+1F467 200D U+1F466 UTF8 F0 9F 91 A8 E2 80 8D F0 9F 91 A9 E2 80 8D F0 9F 91 A7 E2 80 8D F0 9F 91 A6 Extended Grapheme Cluster
  3. @Uber Multiple apps - Rider, Driver, Eats, Freight, Bike/Scooter, etc.

    Deployed in multiple markets Localized in 50+ languages / locales String Correctness / Safety
  4. @Uber Developer Tools (e.g. codegen) ◦ Mockolo: Mock generation ◦

    Needle: Dependency injection automation ◦ Piranha: Code cleanup ◦ Cyborg: Vector image parsing Scan/Parse ~2M LoC in Swift Run on every build / at runtime String Performance
  5. Swift 5.0 compiler / ABI stability UTF16 → UTF8 Unified

    storage / Memory management Size and performance optimizations Changes in Swift String
  6. Correctness / Performance Code relying on UTF 16 encoding Bridging

    / Localized strings Potential crashes / Incorrect behavior Cost $$$ Implications
  7. Pick-up Location: Parco本店 let parco = "Parco" let mainBranch =

    "本店" // [Hon-ten] Main-Branch let idx = mainBranch.index(mainBranch.startIndex, offsetBy: 1) // 2nd index 店 Scenario let combo = parco + mainBranch // Parco本店 let calculatedIdx = combo.index(idx, offsetBy: parco.count) // 7th index 店?
  8. Index _rawBits Breakdown Position (Encoded offset: A 48-bit offset into

    the string's code units) Trans- coded Offset Grapheme Cache Reserved Scalar Aligned [b63]--------- 48 bits --------[b16] 2 bits 6 bits 7 bits 1 bit let idx = mainBranch.index(mainBranch.startIndex, offsetBy: 1) print(idx) // Index(_rawBits: 197376)
  9. let start = mainBranch.startIndex let idx = mainBranch.index(start, offsetBy: 1)

    let distance = mainBranch.distance(from: start, to: idx) // 1 本 店 UTF8 E6 9C AC E5 BA 97 UTF16 672C 5E97 Index rawBits 0 197376 let utf16Distance = mainBranch.utf16.distance(from: start, to: idx) // 1 let utf8Distance = mainBranch.utf8.distance(from: start, to: idx) // 3
  10. P a r c o 本 店 UTF8 50 61

    72 63 6F E6 9C AC E5 BA 97 UTF16 50 61 72 63 6F 672C 5E97 Index rawBits 0 65792 131328 196864 262400 328448 525056 let start = mainBranch.startIndex let idx = mainBranch.index(start, offsetBy: 1) let distance = mainBranch.distance(from: start, to: idx) // 1 let utf8Distance = mainBranch.utf8.distance(from: start, to: idx) // 3 let distanceInCombo = combo.distance(from: start, to: idx) // 3!
  11. P a r c o 本 店 Index rawBits 0

    65792 131328 196864 262400 328448 525056 // Fatal error: String index is out of bounds! let calculatedIdx = combo.index(idx, offsetBy: parco.count) let i = combo.index(idx, offsetBy: 0) print(combo[i]) // co怀 (Incorrect)
  12. // Incorrect let calculatedIdx = combo.index(idx, offsetBy: parco.count) // Correct

    let distance = 1 // Character (Grapheme cluster) distance let correctIdx = combo.index(combo.startIndex, offsetBy: parco.count + distance) Crashes
  13. Update code relying on UTF16 encoding Test index handling /

    string manipulation code Test non-ASCII characters / localized strings / mixed languages Lessons
  14. let arg = "Hi ‍‍! It’s ☀ on " let

    sunIndex = arg.index(of: str.startIndex, offsetBy: 12) Index Lookup O(n) 48 69 20 F0 9F 91 A8 E2 80 8D F0 9F 91 A9 E2 80 8D F0 9F 91. . . ..73 20 E2 98 80 EF B8 8F 20 79 . . . .
  15. // O(n) let beginIdx = arg.index(arg.startIndex, offsetBy: position) let endIdx

    = arg.index(arg.startIndex, offsetBy: position + length) let part = arg[beginIdx ..< endIdx] Extract substring 48 69 20 F0 9F 91 A8 E2 80 8D F0 9F 91 A9 E2 80 8D F0 9F 91. . . ..73 20 E2 98 80 EF B8 8F 20 79 . . . . position position + length
  16. First Attempt let utf8 = arg.utf8 let beginIdx = utf8.index(utf8.startIndex,

    offsetBy: position) let endIdx = utf8.index(beginIdx, offsetBy: position + length) let part = utf8[beginIdx ..< endIdx] → O(n)
  17. Alternatives CString Cumbersome APIs / Pointer calculations NSString UTF 8

    <-> UTF16 / Bridging overhead Data APIs: split(separator:..), subdata(in:), starts(with:), range(of:..), etc. Fast lookup: Random access, Integer index, O(1) Easy to convert to/from String
  18. // O(1) let subdata = arg[position ..< position + length]

    let part = String(data: subdata, encoding: .utf8) Extract substring
  19. Operation: Extract substring Input: 3K LOC, offset (various), length (ave.

    150 bytes) #Runs: 10K Swift compiler version: 5.1 Results w/ String w/ Data 24s 0.004s < Measured on MBP 15-inch 2018 w/ 6 cores >
  20. Constraints • Offsets and lengths in bytes • Non-ASCII characters

    Control input space for performance benefits Constraints
  21. Standard library APIs Equivalent operations • Tokenize ◦ split(separator:) VS.

    components(separatedBy:) • Find / Match ◦ range(of:options: .regularExpression) VS. NSRegularExpression etc. Different content types (non-ASCII Strings) Profiling
  22. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do

    eiusmod tempor incididunt ut labore et dolore magna aliqua. Adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus urna. Vitae justo eget magna fermentum iaculis eu non. Enim nulla aliquet porttitor lacus. Sem viverra aliquet eget sit amet tellus. Mauris a diam maecenas sed enim ut. In metus vulputate eu scelerisque felis. Morbi non arcu risus quis varius quam. At consectetur lorem donec massa sapien faucibus et. Vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras. ...
  23. Separator == " " Input: String w/ count = 1000

    #Runs: 200K Output: 150 tokens (in String) Swift compiler version: 5.1 components(separatedBy: " ") 15.64s split(separator: " ") 10.94s < Measured on MBP 15-inch 2018 w/ 6 cores > (~1.4x slower)
  24. Input: String w/ count = 1000 #Runs: 200K Output: 10

    tokens (in String) Swift compiler version: 5.1 < Measured on MBP 15-inch 2018 w/ 6 cores > components(separatedBy: "\n") 3.4s split(separator: "\n") 10.67s Separator == "\n" (~3x slower)
  25. components(separatedBy:) Swift Compiler 5.1 / #Runs = 200K / Input

    String count = 1000 Separator "\n" " " Output (#Tokens) 10 150 Total Time (s) 3.4s 15.64s ~5x slower
  26. components(separatedBy:) Call Tree "\n" (10 tokens) Total 3.4s " "

    (150 tokens) Total 15.64s NSString rangeOfString:options:range: - CFStringFindWithOptionsAndLocale - CFStringInlineBuffer - NSTaggedPointerString 2.5s 5s NSString _newSubstringWithRange:zone: 0.4s 2s _ObjectiveCBridgeable - String.init(_cocoaString:) 0.3s 7s
  27. split(separator:) Input: String w/ count = 1000 #Runs: 200K Swift

    compiler version: 5.1 "\n" " " components(separatedBy:) 3.4s 15.64s split(separator:) 10.67s 10.94s
  28. Swift Compiler 5.1 / #Runs = 200K / Input String

    count = 1000 / Output = 10 tokens Operation components(separatedBy: "\n") split(separator: "\n") Total Time (s) 3.4s 10.67s ~3x slower Separator == "\n"
  29. public func index(after i: Index) -> Index { _precondition(i <

    endIndex, "String index is out of bounds") // TODO: known-ASCII fast path, single-scalar-grapheme fast path, etc. let i = _guts.scalarAlign(i) let stride = _characterStride(startingAt: i) let nextOffset = i._encodedOffset &+ stride let nextStride = _characterStride(startingAt: Index(_encodedOffset: nextOffset)._scalarAligned) return Index(encodedOffset: nextOffset, characterStride: nextStride)._scalarAligned } https://github.com/apple/swift/blob/master/stdlib/public/core/StringCharacterView.swift
  30. Lorem sum dolor sit amet, consectetur adipiscing elit, sed do

    eiusmod tempor incididunt ut labore et dolore magna aliqua. Adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus urna. Vitae justo eget magna fermentum iaculis eu non. Enim nulla aliquet porttitor lacus. Sem viverra aliquet eget sit amet tellus. Mauris a diam maecenas sed enim ut. In metus vulputate eu scelerisque felis. Morbi non arcu risus quis varius quam. At consectetur lorem donec massa sapien faucibus et. Vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras. ...
  31. Input: String w/ count = 1000 #Runs: 200K Output: 10

    Tokens Swift compiler version: 5.1 w/ ASCII components(separatedBy: "\n") 3.4s 6.44s split(separator: "\n") 10.67s 10.88s
  32. ASCII 2x Slower CFStringFindWithOptionsAndLocale - CFStringInlineBuffer - NSTaggedPointerString getCharacters:range: -

    NSTaggedPointerString _fastCStringContents: CFStringFindWithOptionsAndLocale - __SharedStringStorage.getCharacters(_:range:) - String._nativeCopyUTF16CodeUnits(into:range:) - String.UTF16View._nativeGetIndex(for:) - _StringObject.sharedUTF8.getter components(separatedBy: "\n") Swift Compiler 5.1 / #Runs: 200K / Input: String w/ count = 1000 / Output: 10 Tokens
  33. b63 b62 b61 b60 b60:b0 isImmortal (isValue) isASCII / isBridged

    isSmall isForeign (NSString) objectAddr b63 b62 b61 b60 b59:48 b47:0 isASCII isNFC (Normal Form C) isNativelyStored isTailAllocated (isFastUTF8) TBD count Performance Flags CountAndFlags Discriminator
  34. Profile standard library APIs Check different types of input -

    Lengths, non-ASCII, parameters Check out internal implementations - Github, instruments, SIL, etc. Don’t assume stdlib APIs are fully optimized Can rewrite functions Check out alternatives Control input space Focus on performance hot-paths Do’s & Dont’s
  35. Correctness - Test edge cases (non-ASCII strings, index handling, etc.)

    Performance - Profile (various input contents, variable lengths or params, etc.) - Optimize Putting Everything Together Cumbersome Interface Incorrect behavior Potential Crashes Slow
  36. Proprietary and confidential © 2019 Uber Technologies, Inc. All rights

    reserved. No part of this document may be reproduced or utilized in any form or by any means, electronic or mechanical, including photocopying, recording, or by any information storage or retrieval systems, without permission in writing from Uber. This document is intended only for the use of the individual or entity to whom it is addressed and contains information that is privileged, confidential or otherwise exempt from disclosure under applicable law. All recipients of this document are notified that the information contained herein includes proprietary and confidential information of Uber, and recipient may not make use of, disseminate, or in any way disclose this document or any of the enclosed information to any person other than employees of addressee to the extent necessary for consultations with authorized personnel of Uber.