Why compile an array size 3773 statically init each element very slow?

let allEmojis: [Emoji] = [
    .init(character: "πŸ˜€", name: "Grinning face", category: .init(group: "Smileys & Emotion", subgroup: "face-smiling"), isNew: false, zwjGroup: nil),
// this repeat 3772 more time
// ...
]

this is making my compile taking a long time (several minutes) on a small SwiftUI project. I'm tempted to switch to json and parse it at runtime.

It slow every compile even when I only change other files.

Can you plot a rough compilation time / count graph? Will it be linear, quadratic or what?

No idea why it takes long, but using JSON is a good workaround. Either store that JSON in an external file or in the source file:

let json = #"[
    {
        "character": "πŸ˜€",
        "name": "Grinning face",
        "category": {
            "group": "Smileys & Emotion",
            "subgroup": "face-smiling" 
        },
        "isNew": false,
        "zwjGroup": nil
    },
    ...
]"#

Does writing Emoji instead of .init make a difference?

1 Like

Large static arrays seem to be a known slow point for the compiler β€” see e.g. these issues

4 Likes

Since I have [Emoji] type annotation, I didn't think it would make any difference.

But I give it a try and very surprising result:

First, I change the first .init to Emoji of every element and the compile is about the same slow. Then I changed the second .init to Category and clean and. compile, first a window pop up saying:

"..run out of application memory. Quit some app ..."

after a little while, my MacBook Air M2 with 24GB went black and shutdown.

I reboot back, clean and compile again an it's much much slower than before.

So whatever difference this make Xcode to consume all the memory.

1 Like

What happens if you change all .inits to Emoji?

I did change all but there is another init to Catagory in the argument and I also changed that and Xcode code consume all memory

1 Like

Got you. That slowdown you saw before is probably due to VM trashing, just in that case the memory required didn't overflow available VM size (typically about machine RAM size x 2).

Another workaround that might work for you:

var allEmojis: [Emoji] = []

func initEmojis() {
    allEmojis.removeAll()
    allEmojis.append(.init(character: "πŸ˜€", name: "Grinning face", category: .init(group: "Smileys & Emotion", subgroup: "face-smiling"), isNew: false, zwjGroup: nil))
    // repeat this 3772 more time
}
1 Like

I did this:

var allEmojis: [Emoji] = {
    var result = [Emoji]()
    result.reserveCapacity(3773)
    result.append(.init(character: "πŸ˜€", name: "Grinning face", category: .init(group: "Smileys & Emotion", subgroup: "face-smiling"), isNew: false, zwjGroup: nil))
    // ... repeat etc...

    return result
}()

Xcode compile took over 10 minutes now. And change some other file and recompile is much slower.

So my original way is the only way Xcode can finish compile, it's faster to compile than doing it as you suggest.

BTW, Xcode cannot edit my AllEmoji file, any change cause it to beach ball. I had to edit it with vim outside of Xcode.

Looks like parsing each

.init(...., category: .init(...))

expression is slow?

1 Like

I confirm your findings. Here's a nice quadratic timing graph I got:

x - number of appends, y - compilation time in seconds.

Raw timing data
100 2.5
200 4.8
300 8.5
400 13.8
500 20.2
600 26.7
700 38.3
800 44.3
900 55.6
1000 71.0
5 Likes

Wonder what the memory curve is?

It’s probably the same quadratic and that’s why it’s slow?

I didn't notice bad memory behaviour during that test.

I tried that approach, it took just 0.03 seconds to parse that JSON string during runtime, not a big deal.

I did this:

import Foundation

var allEmojis: [Emoji] = {
  // just let it crash if something is wrong but shouldn't because
  // we created the input and the output, everything should parse...
  try! JSONDecoder().decode([Emoji].self, from: allEmojisJson.data(using: .utf8)!)
}()
private let allEmojisJson = """
.
.
.

Now the command line tool that generates this is very slow to compile. But my app now compile super fast and app startup is instant on device even with debug.

It's best to continue here to not mix the threads.

I confirm there's something odd going on here. Xcode consuming all those 10s or even 100s of GB and then crashing my computer (it did) is not good. I tried 4K of your lines with different permutations of explicit types vs ".init":

let emojis: [Emoji] = [
    .init(character: "πŸ˜€", name: "Grinning face", category: .init(group: "Smileys & Emotion", subgroup: "face-smiling"), isNew: false, zwjGroup: nil),
// 4K of those
]

vs

let emojis: [Emoji] = [
    Emoji(character: "πŸ˜€", name: "Grinning face", category: .init(group: "Smileys & Emotion", subgroup: "face-smiling"), isNew: false, zwjGroup: nil),
// 4K of those
]

vs

let emojis: [Emoji] = [
    .init(character: "πŸ˜€", name: "Grinning face", category: Category(group: "Smileys & Emotion", subgroup: "face-smiling"), isNew: false, zwjGroup: nil),
// 4K of those
]

vs

let emojis: [Emoji] = [
    Emoji(character: "πŸ˜€", name: "Grinning face", category: Category(group: "Smileys & Emotion", subgroup: "face-smiling"), isNew: false, zwjGroup: nil),
// 4K of those
]

the types:

struct Category {
    var group: String
    var subgroup: String
}

struct Emoji {
    var character: String
    var name: String
    var category: Category
    var isNew: Bool
    var zwjGroup: String?
}

It would be extremely rare anyone would want to write 4K of such lines, but still, quadratic CPU time complexity aside, quite questionable memory complexity (like 10 MB per expression?!) is ... unexpectable to put it mildly. Highly likely there's a memory leak in there. It's a bug worth filing.

1 Like

Swift bug? It’s the same on command line.

It would be extremely rare anyone would want to write 4K of such lines

Not by hands but code gen tools can often produce large boilerplate source. My case is pure data. Storing in json and decode at runtime is better to avoid slow Swift compile.

However, it’s better if Swift can just compile such code and avoid the extra decode and large json string.

1 Like

There could also be the DoS angle here. For the record godbolt's compiler is killed with one error or another (when using explicit types vs .inits) - I guess in one case it's killed due to OOM situation and in another due to compiler spending too much time during the compilation.