Why is CaseIterable faster then a Set

// V1
enum VaccineCodeSet: String {
    case covidModerna = "207"
    case covidBioNTech = "208"
    case covidAstraZeneca = "210"
    case covidNovax = "211"
    case covidJohnson = "212"
    case covidValneva = "213"
    case hepatitesB = "45"
    case whoopingCough = "11"
    case measles = "05"
    case mumps = "07"
    case rubella = "06"
    case chickenpox = "21"
    case tbe = "77"
    case hepatitesA = "85"
    case yellowFever = "184"

    static var allCases: Set<String> = [
        Self.covidModerna.rawValue,
        Self.covidBioNTech.rawValue,
        Self.covidAstraZeneca.rawValue,
        Self.covidNovax.rawValue,
        Self.covidJohnson.rawValue,
        Self.covidValneva.rawValue,
        Self.hepatitesB.rawValue,
        Self.whoopingCough.rawValue,
        Self.measles.rawValue,
        Self.mumps.rawValue,
        Self.rubella.rawValue,
        Self.chickenpox.rawValue,
        Self.tbe.rawValue,
        Self.hepatitesA.rawValue,
        Self.yellowFever.rawValue
    ]
}

printTimeElapsedWhenRunningCode(title: "VaccineCodeSet") {
    if VaccineCodeSet.allCases.contains("184") {
        true
    }
}

// Time elapsed to check: 0.006421875 s.


// V2
enum VaccineCodeCaseIterable: String, CaseIterable {
    case covidModerna = "207"
    case covidBioNTech = "208"
    case covidAstraZeneca = "210"
    case covidNovax = "211"
    case covidJohnson = "212"
    case covidValneva = "213"
    case hepatitesB = "45"
    case whoopingCough = "11"
    case measles = "05"
    case mumps = "07"
    case rubella = "06"
    case chickenpox = "21"
    case tbe = "77"
    case hepatitesA = "85"
    case yellowFever = "184"
}



printTimeElapsedWhenRunningCode(title: "Array") {
    if let code = VaccineCodeCaseIterable(rawValue: "184"),
       IllnessCodeCaseIterable.allCases.contains(code) {
       true
    }
}

// Time elapsed to check:: 0.005916875 s.

Why is V1 slower as V2?

I guess this is a typo and should be VaccineCodeSet.allCases.contains.

Let's see. In the first case you have a Set of strings, in the second case you have an Array of enumeration values (note that it is not an array of strings, it's more like array of integers). Worth noting that the number of elements is small. And that the measured time difference is also small. And the strings are tiny – will be stored inline string structs without being allocated on heap. Set lookup is one hash calculation plus O(1) element comparisons, Array lookup is O(n) element comparisons. The combination of these factors make Array version the winner in this test: it's faster to compare 15 ints then to do a hash calculation of a short inline stored string and then do one (or few) short string comparisons. Under different conditions (e.g. a larger number of elements) set would be a winner. OTOH, if you had a longer heap allocated strings the number of elements for the set version to be a winner would be higher.

2 Likes

Actually.... could this line:

SomeEnumeration.allCases.contains(enumerationValue)

could ever return anything but true? In which case couldn't its implementation be just:

func contains(_ v: Element) -> Bool {
    true
}

I wonder if we could have this optimisation in Swift.

1 Like

Users are allowed to customize CaseIterable conformance, even if it’s not recommended. BUT the compiler could still do the optimization if it knows the conformance was synthesized. BUT it would be silly to add an optimization (and thus add compile time) for something that people are very unlikely to write in practice.

2 Likes

V2 does have one more bit of work to do: going from a String to an enum case. But that can be optimized (in the init(rawValue:) implementation) because the compiler knows the full set of possible valid strings, so it ends up faster than the hash lookup. (In fact, since all the strings are short, it can even be done as an integer switch, treating the string contents as a multi-byte integer after checking the length. I didn’t look to see if this is what actually gets generated, though.)

1 Like