-2

Under some conditions, string interpolation produces a string that is way slower to use as a dictionary key than a string produced with String(format:). Take the following code:

var map = [String: String]()
let start = Date()
for _ in 0..<100000 {
    var s = "asdf"
    s = (s as NSString).appendingPathComponent("")
    s = transform(s)
    s = (s as NSString).substring(from: 1)
    map[s] = s
}
print(-start.timeIntervalSinceNow)

func transform(_ s: String) -> String {
    return "\(s)/\(s)"
//    return String(format: "%@/%@", s, s)
}

On my Mac I get the time interval 0.69 seconds printed out in the console (when using the string interpolation), but when commenting out line 13 and uncommenting line 14 (so that we use String(format:)) I get a 0.33 seconds time interval, less than half the time. Curiously, whenever uncommenting line 5 or line 7, string interpolation is faster. Also, when commenting out line 8 (map[s] = s), the version with the string interpolation is faster than the one with String(format:).

This took me quite a lot of time to figure out, since I would expect both methods to produce the same kind of string, but string interpolation to be always faster. Does anybody know why?

Nickkk
  • 2,261
  • 1
  • 25
  • 34
  • They do not create the same kind of String. An NSString-backed String is not the same thing as a native Swift String; they just have the same interface. At various points, they have to convert. @matt's answer is correct. Don't force the system to make NSString-backed Strings, and then force it to convert those back to Character-backed Strings. Those are not free operations. – Rob Napier Aug 16 '20 at 17:24
  • If you want to explore this further, study the source, and particularly _StringGuts. Start here: https://github.com/apple/swift/blob/master/stdlib/public/core/String.swift and then here: https://github.com/apple/swift/blob/master/stdlib/public/core/StringGuts.swift Swift heavily optimizes over different storage backends to avoid conversions. When you force a conversion, you can add a lot of cost. You avoid that by just using String, and not using `as NSString`. – Rob Napier Aug 16 '20 at 17:28
  • Just replacing the `substring(from: 1)` line with `s = String(s.dropFirst())` will dramatically speed things up. – Rob Napier Aug 16 '20 at 17:30
  • Thanks, I get that the context switch is a problem. What still doesn't make sense is that the two variants inside the `transform()` function both use a Swift String, still one is faster to insert into the dictionary than the other. – Nickkk Aug 17 '20 at 21:59
  • 1
    "Both use a Swift String" is misunderstanding what it means to have an NSString-backed String. A Swift String can be backed several ways. It isn't necessarily converted at the point of `as`; it just has different Guts. For hashes to be consistent, however, you're going to have to make sure that the string is evaluated in a consistent form (which, depending on several factors, could involve converting an UTF-16 NSString into UTF-8). The point is "String" is an interface, not an implementation. And forcing conflicting implementations to work together is expensive. – Rob Napier Aug 17 '20 at 22:10

1 Answers1

1

It's not the string interpolation vs the format string per se that takes time, and it has basically nothing to do with the dictionary insertion.

The significant thing is the amount of context switching between String and NSString that the rest of the code requires.

To see that, comment out the two lines that contain (s as NSString). Now you'll find that the format string version takes considerably longer than the string interpolation. That's because the format string introduces a context switch.

With your original code, sticking a String operation in the middle of two NSString operations means there are three pairs of context switches. That's the source of the slowness. In other words, if you're going to switch to NSString at all, do all NSString stuff until you're done, so there is just one context switch in one direction and one context switch back in the other direction.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Thanks for your help, what you say definitely makes sense. What I wanted to point out is that when commenting out line 8 (`map[s] = s`), the version with the string interpolation is faster than the one with `String(format:)`, so the dictionary insertion definitely plays a role here. I'll add this info to the question. – Nickkk Aug 16 '20 at 17:17
  • Cool, but you are not measuring at that level of granularity. Deleting the `map` line made very little difference in my tests, whereas it is clear that every context switch is a quantum. – matt Aug 16 '20 at 17:24
  • Thanks, I get that the context switch is a problem. What still doesn't make sense is that the two variants inside the `transform()` function both use a Swift String, still one is faster to insert into the dictionary than the other. – Nickkk Aug 17 '20 at 21:59
  • 1
    Because, as I explained, `String(format:)` is a Cocoa Objective-C function, but simple string interpolation is a Swift function. Therefore if we are already an NSString at the time `transform` is called, `String(format:)` is faster because there is no context switch, but if we are a Swift String, string interpolation is faster because there is no context switch. – matt Aug 17 '20 at 22:03
  • I suspect that some of the confusion is that the OP may be reading "context switch" as a one-time conversion that happens at the `as` rather than something that happens repeatedly if the StringGuts mismatch. – Rob Napier Aug 17 '20 at 22:11
  • Sorry, it wasn't clear to me that `String(format:)` is a Cocoa Objective-C function, in this case your explanation makes sense. It looks like it should be a native Swift `String` function since it doesn't have the `NS` prefix. Is this documented somewhere or how can I differentiate between Swift and Objective-C functions? – Nickkk Aug 17 '20 at 22:31
  • The easiest way is to try to compile your code in the absence of Foundation. Whip out a playground and do _not_ import UIKit or Foundation. If your code compiles, it is pure Swift. If not, it is Cocoa Foundation code, rooted in Objective-C. — Another way is to examine NSString. If a String method also exists in NSString, it is an NSString method, acquired by String only thanks to bridging. – matt Aug 17 '20 at 22:46