Flag to make ContiguousArray the default?

Hi :slight_smile:

Continuing with the theme of added boilerplate in performance critical code;
The default Array gets simple syntax like [CChar] rather than ContiguousArray<CChar> which makes the code a lot simpler, especially in extensions. Not to mention Array is inferred when no explicit type annotation is present, so it's often an even more significant difference.

This is a big issue in function arguments, where having even one of these means other arguments are probably not going to fit on the same line.

For many use cases the added overhead of Array is completely unnecessary. Outside of Apple platforms, Obj-C is not exactly a popular language, and even without any obj-c in my code or dependencies, reading the assembly shows a lot of reference counting and NSArray bridging stuff.
The documentation also mentions Array can have unpredictable performance compared to ContiguousArray when Element is a class.
But I don't use ContiguousArray because it's hard to read :slight_smile:


It feels very weird to compromise arrays of a language for a single platform. Why not have a swiftc flag to make [Element] mean ContiguousArray and infer it for arrays without an explicit type?

This doesn't solve the problem of arrays in general though, in something I made recently a single array that doesn't escape is reference counted so much that 30% of the time is just wasted on retain and release.
I assume it's the copy on write "optimisation"? :upside_down_face:
I can't even make it unmanaged because Array itself is a struct :pensive:

The cost of bridging is that essentially all operations have to dispatch to two internal implementations - one for native arrays, and another for bridged NSArrays. NSArray only supports class-type elements, so when the element type is known to not be a class (e.g. [CChar]), that dispatching should be constant-folded, and performance should be the same as ContiguousArray.

If you are finding something different, can you give more information about what exactly you're seeing and where?

If you might be working with a class element type, you can perform your work on ContiguousArray, then convert to Array at the interface to present a more convenient API for your users. The conversion does not allocate new storage.

Reference counting is part of ARC, which ensures memory lifetimes. Copy-on-write implementations can make use of reference counts to detect when memory is uniquely referenced and may be modified in-place without violating value semantics, but that's as far as they are related.

I know I can use a ContiguousArray, but it requires being explicit about the type everywhere that array is used, which is annoying and defeats some of the advantages of using Swift in the first place, that is, everything becomes more complicated than languages with manual memory management.

The problem is that I'm passing an array exclusively using inout and performance suffers 30% from release calls according to the profiling tools. Everything is structured in a way where if you were to implement it in Rust it would not need to use lifetimes or heap allocation, memory management is completely unnecessary. It's just an array passed inout and mutated in a loop.

Everything is inside a @Sendable function, the wasted time thing is called: completeTaskAndRelease(swift::AsyncContext*, swift::SwiftError*)
and inside it, a swift_release. It's more or less 6 seconds out of 18. there's also maybe 3 seconds of swift_bridgeObjectRetain.

Does Swift fall back on reference counting no matter what when in an async context, even when nothing is escaping? I tried using @_effects(notEscaping <name>) but it made no difference.

It would be worthwhile to break on the retain call and figure out what the object is that’s being retained.

It seems somewhat difficult in Xcode, especially with 8 threads
I can disable the threads, but how can I make lldb actually show me the retain and release calls?

ok I figured it out there's a disassemble command :)

In all likelihood this is just a missed optimization in the ARC optimizer, not something that should require code changes on your end. Probably worth a bug report. Using ContiguousArray shouldn't help at all, as it also uses reference counting in exactly the same was as Array.

4 Likes

Seems like this is related to Swift Concurrency and not necessarily Array.

That is true, it does not help in any way sadly; the reference counting was just something I mentioned as a side note because I've created enough threads complaining about arrays already :slight_smile:
(I guess, technically, this is complaining about array too now)

Good point, though I tried it now without concurrency and sadly there's still reference counting.

I guess I'll just wait a while until a static array is in the language, I should stop creating threads complaining about arrays :laughing:

And a small update:
The code went from 30 to 8 seconds at writing 1 billion random characters to the "array" after making my code look like C, using withUnsafeTemporaryAllocation and buffer[index] = char; index++ rather than .append()

Since like I said, the code is structured in a way where all lifetimes are compile time known and so is array size. It would be cool to have proper syntax for lifetimes.
Sadly my code is completely unreadable now.