yerpy yerpy - 3 months ago 20
Swift Question

More efficient way for converting structure properties into dictionary

I was looking for most efficient way for creating dictionary from structures. After small research I found easy way for converting it via initialising all properties like that :

func toDictionary() -> [String : AnyObject] {
let dictionary: [String: AnyObject] = ["firstProperty" : sth, "secondProperty" : "sth2"]
return dictionary
}


But wait.. Do I have to initialise it every time in my structures ? They might have a loot of properties.. It gave me to think. What if I could get properties though the looping of the class somehow. Yeah, it is possible using
Mirror
reflection. After a while of trying I've got it - instead of function above I've written protocol which implements one function.

protocol Mirrorable {
func toDictionary() -> [String : AnyObject]
}


Then in my structure I can simply use :

extension MyStructure/MyClass : Mirrorable {

func toDictionary() -> [String : AnyObject] {
let reflection = Mirror(reflecting: self).children
var dictionary = [String : AnyObject]()
for (label, value) in reflection {
dictionary[label!] = value as AnyObject
}
return dictionary
}
}


Curiosity does not let me to stop thinking about it. Which way would be more efficient ?

Answer Source

TLDR

Direct conversion is faster but since most cases you won't have to convert 1.000.000+ structs to dictionary I'd use a protocol extension.


Long answer

I created a Mac cmd line app to test the time and as expected the direct conversion is faster. This is kind of obvious since the compiler could optimize it and since the code itself is simple. When using reflection you create a bunch of extra structures and variables, also add some overhead.

Even though the direct conversion is faster, I think in most real cases would be fine to use the protocol extension approach since it's easy to adopt in your code base. One thing you must have in mind is that your code using reflection is not considering nested objects, this would make the method even slower and also if you used recursion would generate a bigger overhead.

Here are the results for 3 executions encoding 1.000.000 structs to a dictionary:

Build settings

  • Apple LLVM 8.1 - Code Generation

    • Optimization Level: -Ofast
  • Swift Compiler - Code Generation:

    • Optimization Level: -O -whole-module-optimization

Time for 1000000 executions with S1(direct conversion): 0.569061994552612

Time for 1000000 executions with S2(struct extension + protocol): 7.68360501527786

Time for 1000000 executions with S3(protocol extension): 7.71803396940231

Time for 1000000 executions with S1(direct conversion): 0.500779032707214

Time for 1000000 executions with S2(struct extension + protocol): 7.58478999137878

Time for 1000000 executions with S3(protocol extension): 7.73368299007416

Time for 1000000 executions with S1(direct conversion): 0.492152035236359

Time for 1000000 executions with S2(struct extension + protocol): 7.81585901975632

Time for 1000000 executions with S3(protocol extension): 7.41855001449585

Build settings

  • Apple LLVM 8.1 - Code Generation

    • Optimization Level: -Ofast
  • Swift Compiler - Code Generation:

    • Optimization Level: -Onone

Time for 1000000 executions with S1(direct conversion): 2.92627400159836

Time for 1000000 executions with S2(struct extension + protocol): 9.25952398777008

Time for 1000000 executions with S3(protocol extension): 9.19355899095535

Time for 1000000 executions with S1(direct conversion): 2.9830749630928

Time for 1000000 executions with S2(struct extension + protocol): 9.06750500202179

Time for 1000000 executions with S3(protocol extension): 8.77240401506424

Time for 1000000 executions with S1(direct conversion): 2.81389397382736

Time for 1000000 executions with S2(struct extension + protocol): 8.84287703037262

Time for 1000000 executions with S3(protocol extension): 9.08754301071167

Build settings

  • Apple LLVM 8.1 - Code Generation

    • Optimization Level: -O0
  • Swift Compiler - Code Generation:

    • Optimization Level: -O -whole-module-optimization

Time for 1000000 executions with S1(direct conversion): 0.533200979232788

Time for 1000000 executions with S2(struct extension + protocol): 8.15365797281265

Time for 1000000 executions with S3(protocol extension): 7.80043601989746

Time for 1000000 executions with S1(direct conversion): 0.509769976139069

Time for 1000000 executions with S2(struct extension + protocol): 7.76911997795105

Time for 1000000 executions with S3(protocol extension): 8.00845402479172

Time for 1000000 executions with S1(direct conversion): 0.532546997070312

Time for 1000000 executions with S2(struct extension + protocol): 7.99552202224731

Time for 1000000 executions with S3(protocol extension): 7.86273497343063

Build settings

  • Apple LLVM 8.1 - Code Generation

    • Optimization Level: -O0
  • Swift Compiler - Code Generation:

    • Optimization Level: -Onone

Time for 1000000 executions with S1(direct conversion): 2.90398299694061

Time for 1000000 executions with S2(struct extension + protocol): 9.62662398815155

Time for 1000000 executions with S3(protocol extension): 9.55038601160049

Time for 1000000 executions with S1(direct conversion): 2.98312002420425

Time for 1000000 executions with S2(struct extension + protocol): 9.62088203430176

Time for 1000000 executions with S3(protocol extension): 8.82720899581909

Time for 1000000 executions with S1(direct conversion): 2.77569997310638

Time for 1000000 executions with S2(struct extension + protocol): 8.83749902248383

Time for 1000000 executions with S3(protocol extension): 8.76373296976089

Code:

import Foundation

// Direct conversion to dictionary
struct S1 {
    var a: Int
    var b: Int
    var c: Int
    func toDictionary() -> [String : AnyObject] {
        let dictionary: [String: AnyObject] = [
            "a":a as AnyObject,
            "b":b as AnyObject,
            "c":c as AnyObject
        ]
        return dictionary
    }
}

// Conversion using struct extension + protocol
protocol Mirrorable1 {
    func toDictionary() -> [String : AnyObject]
}

struct S2 {
    var a: Int
    var b: Int
    var c: Int
}

extension S2: Mirrorable1 {
    func toDictionary() -> [String : AnyObject] {
        let reflection = Mirror(reflecting: self).children
        var dictionary = [String : AnyObject]()
        for (label, value) in reflection {
            dictionary[label!] = value as AnyObject
        }
        return dictionary
    }
}

// Conversion using protocol extension
protocol Mirrorable2 {
    func toDictionary() -> [String : AnyObject]
}

extension Mirrorable2 {
    func toDictionary() -> [String : AnyObject] {
        let reflection = Mirror(reflecting: self).children
        var dictionary = [String : AnyObject]()
        for (label, value) in reflection {
            dictionary[label!] = value as AnyObject
        }
        return dictionary
    }
}

struct S3: Mirrorable2 {
    var a: Int
    var b: Int
    var c: Int
}

let listOfExecutions = [1_000_000, 1_000_000, 1_000_000]
for executions in listOfExecutions {
    // S1
    var start = CFAbsoluteTimeGetCurrent()
    for i in 0..<executions {
        S1(a: 0, b: 1, c: 2).toDictionary()
    }
    print("Time for \(executions) executions with S1(direct conversion):")
    print(CFAbsoluteTimeGetCurrent() - start)

    // S2
    start = CFAbsoluteTimeGetCurrent()
    for i in 0..<executions {
        S2(a: 0, b: 1, c: 2).toDictionary()
    }
    print("Time for \(executions) executions with S2(struct extension + protocol):")
    print(CFAbsoluteTimeGetCurrent() - start)

    // S3
    start = CFAbsoluteTimeGetCurrent()
    for i in 0..<executions {
        S3(a: 0, b: 1, c: 2).toDictionary()
    }
    print("Time for \(executions) executions with S3(protocol extension):")
    print(CFAbsoluteTimeGetCurrent() - start)
}