Проблемы с производительностью в списке миров

У меня возникают проблемы с производительностью памяти при выполнении операций со списком областей. У меня есть два объекта, похожих на этот:

final class Contact: Object {
    let phones = List<Phone>()
    let emails = List<Email>()
}

Теперь я пытаюсь найти возможные сходства между двумя объектами одного типа (например, по крайней мере, одним общим элементом), которые потенциально могут иметь повторяющиеся электронные письма или телефоны. Для этого я использовал Set операций.

func possibleDuplicateOf(contact: Contact) {
    return !Set(emails).isDisjoint(with: Set(contact.emails)) || !Set(phones).isDisjoint(with: Set(contact.phones))
}

Это функция внутри объекта Contact. Я знаю, что при преобразовании Realm List в Set или Array производительность падает, и я сильно это чувствую, когда у меня большое количество контактов (10k или больше), потребление памяти возрастает до более чем 1GB.

Поэтому я попытался заменить указанную выше функцию на эту:

func possibleDuplicateOf(contact: Contact) {
    let emailsInCommon = emails.contains(where: contact.emails.contains)
    let phonesInCommon = phones.contains(where: contact.phones.contains)

    return emailsInCommon || phonesInCommon
}

Это имеет ту же производительность, что и при использовании наборов.

Метод isEqual в электронных письмах и телефонах - это простое сравнение строк:

extension Email {
    static func ==(lhs: Email, rhs: Email) -> Bool {
        return (lhs.email == rhs.email)
    }

    override func isEqual(_ object: Any?) -> Bool {
        guard let object = object as? Email else { return false }

        return object == self
    }

    override var hash: Int {
        return email.hashValue
    }
}

Email.swift

final class Email: Object {

enum Attribute: String { case primary, secondary }

@objc dynamic var email: String = ""
@objc dynamic var label: String = ""

/* Cloud Properties */
@objc dynamic var attribute_raw: String = ""
var attribute: Attribute {
    get {
        guard let attributeEnum = Attribute(rawValue: attribute_raw) else { return .primary }
        return attributeEnum
    }
    set { attribute_raw = newValue.rawValue }
}

override static func ignoredProperties() -> [String] {
    return ["attribute"]
}

convenience init(email: String, label: String = "email", attribute: Attribute) {
        self.init()

        self.email = email
        self.label = label
        self.attribute = attribute
    }
}

У меня здесь немного вариантов, я потратил весь день, пытаясь придумать другой подход к этой проблеме, но безуспешно. Если у кого-то есть идея получше, я хотел бы ее услышать :)

Спасибо


person Diogo Antunes    schedule 28.11.2018    source источник
comment
В вашем объекте Contact у вас есть список Phones и Emails. Они тоже сущности царства? Если они являются объектами, они могут иметь обратное отношение к объекту Contact. Имея это отношение, вы можете запрашивать необходимую информацию из области, не обрабатывая ее в памяти.   -  person ilya    schedule 04.12.2018
comment
@ilya да, это списки из области   -  person Diogo Antunes    schedule 04.12.2018
comment
Не могли бы вы рассказать, как эти объекты определены?   -  person ilya    schedule 04.12.2018
comment
@ilya, конечно, это прямо там, в первом блоке кода :) final class Контакт: Object {let phone = List ‹Phone› () let emails = List ‹Email› ()}   -  person Diogo Antunes    schedule 04.12.2018
comment
Я имел в виду конкретно определение классов Phone и Email.   -  person ilya    schedule 04.12.2018
comment
@ilya Ой, извини, вот так :)   -  person Diogo Antunes    schedule 04.12.2018
comment
Вы сравниваете каждый из 10 тысяч контактов с любым другим контактом (то есть 100 миллионов сравнений)?   -  person Andreas Ley    schedule 09.12.2018
comment
@AndreasLey, Нет :) это сценарий, в котором у нас есть контакты на сервере и контакты в адресной книге, и мы сравниваем их с сервера с теми, что есть в адресной книге, на предмет возможного сходства, чтобы объединить их вместе. Если бы было 10 КБ в адресной книге и 10 КБ на сервере, да, было бы очень большое сравнение. Но в этом конкретном случае, который я пытаюсь решить, если я удалю Set операции, он будет работать очень быстро и почти не тратит дополнительную память.   -  person Diogo Antunes    schedule 10.12.2018


Ответы (2)


Всякий раз, когда происходит что-то подобное, неплохо начать с использования инструментов, чтобы узнать, где потребляются циклы ЦП и память. . Вот хорошее руководство: Использование Time Profiler в инструментах

Вы пропустили код, производящий фактическое сравнение, но я подозреваю, что это могут быть вложенные for циклы или что-то в этом роде. Realm не знает вашего варианта использования и не кэширует должным образом для чего-то в этом роде.

Используя инструменты, довольно легко найти узкие места. В вашем случае это должно сработать:

final class Contact: Object
{
    let emails = List<Email>()

    lazy var emailsForDuplicateCheck:Set<Email> = Set(emails)

    func possibleDuplicateOf(other: Contact) -> Bool {
        return !emailsForDuplicateCheck.isDisjoint(with: other.emailsForDuplicateCheck)
    }

    override static func ignoredProperties() -> [String] {
        return ["emailsForDuplicateCheck"]
    }
}

И для сравнения:

// create an array of the contacts to be compared to cache them 
let contacts = Array(realm.objects(Contact.self))
for contact in contacts {
    for other in contacts {
        if contact.possibleDuplicateOf(other: other) {
            print("Possible duplicate found!")
        }
    }
}

Эта реализация гарантирует, что объекты Contact выбираются только один раз, а Set из Email создается только один раз для каждого Contact.

person Andreas Ley    schedule 10.12.2018

Проблема, которая у вас возникла, может быть решена более оптимально, если немного перестроить структуры данных. Получить все в памяти и попытаться преобразовать в набор (сборка - дорогостоящая операция) далеко не оптимально :(. Я предлагаю это решение.

Считайте, что этим объектом является контакт (я добавил свойство id). Объекты телефонов я не добавлял для краткости, но точно такой же подход можно использовать и для телефонов.

class Contact: Object {
    @objc dynamic var id = UUID().uuidString
    var emails = List<Email>()

    override public static func primaryKey() -> String? {
        return "id"
    }
}

И это класс электронной почты. Добавлена ​​связь с контактом.

class Email: Object {
    @objc dynamic var email: String = ""
    @objc dynamic var contact: Contact?
}

Имея эти "связанные" таблицы в области, вы можете создать запрос для поиска повторяющихся объектов:

func hasDups(contact: Contact) -> Bool {
  let realm = try! Realm()
  let emails: [String] = contact.emails.map { $0.email }
  let sameObjects = realm.objects(Email.self)
                         .filter("email in %@ AND contact.id != %@", emails, contact.id)
  // sameObject will contain emails which has duplicates with current contact
  return !sameObjects.isEmpty
}

Это работает очень быстро, я тестировал более 100000 объектов и сразу же выполнил.

Надеюсь это поможет!

person ilya    schedule 13.12.2018