I've noticed a rather strange performance difference between URLComponents(url:resolvingAgainstBaseUrl:)
and URLComponents(string:)
- namely that the former seems a fair bit faster than the latter. This is on a macOS 11.6.
Components From URL
Looking at the implementation for NSURLComponents(url:resolvingAgainstBaseURL:)
, I find that it forwards to the CoreFoundation function _CFURLComponentsCreateWithURL
. If we are resolving against the base URL, this function:
- Calls
CFURLCopyAbsoluteURL
to do the resolving, then -
CFURLGetString
on the result, which it then forwards to... -
_CFURLComponentsCreateWithString
, which creates theCFURLComponents
URL.absoluteString
A quick note here: if we look at the implementation for NSURL.absoluteString
, we can see it uses exactly the same pattern as above: CFURLCopyAbsoluteURL
, followed by CFURLGetString
on the result.
Components From String
Now, let's look at NSURLComponents(string:)
- as you might expect, it just calls _CFURLComponentsCreateWithString
directly. Same function as before, but without resolving the URL and copying its string.
Now, you can probably see where I'm going with this - theoretically, if I have a URL
instance, I should be able to call .absoluteString
and feed that result in to URLComponents(string:)
myself, and expect exactly the same performance as calling URLComponents(url:, resolvingAgainstBaseURL: true)
. The benefit to doing it manually is that I'd also have the .absoluteString
which I can use for other purposes.
Performance
But that isn't what I'm seeing. Benchmarking the two on a set of URL strings (specifically, these strings), I'm seeing a 27% regression. Just calling these functions, and nothing else. And it's repeatable - I can change back and forth and consistently get the same results.
URLComponents(url:)
for url in average_urls {
let cmps = URLComponents(url: url, resolvingAgainstBaseURL: true)
blackHole(cmps)
}
name time std iterations
--------------------------------------------------------------
FoundationToWeb.AverageURLs 30076.000 ns ± 23.54 % 43943
FoundationToWeb.IPv4 40066.000 ns ± 24.83 % 31307
FoundationToWeb.IPv6 44944.000 ns ± 24.34 % 28852
URLComponents(string:)
for url in average_urls {
let str = url.absoluteString
let cmps = URLComponents(string: str)
blackHole(cmps)
}
name time std iterations
--------------------------------------------------------------
FoundationToWeb.AverageURLs 41393.000 ns ± 22.51 % 32415
FoundationToWeb.IPv4 60179.500 ns ± 22.54 % 20860
FoundationToWeb.IPv6 65387.000 ns ± 21.91 % 19123
The only thing that I can think of is that it's the whole ._swiftObject
stuff that's causing the overhead (bridging the string to Swift, rather than keeping it in C). Does that sound plausible, or could there be some other explanation for this? As shown above, they ultimately call the exact same CF functions, in the same order.
I can't find where ._swiftObject
is implemented for these (presumably CFString
s, so I haven't been able to examine further.