У меня есть 2 массива строк beforeStringList и afterStringList. На самом деле это представления таблиц sqlite, и моя конечная цель — получить список элементов, которые присутствуют только в одном из массивов, чтобы я мог определить, было ли что-то добавлено или удалено. Например, если элемент существует в beforeStringList, а не в afterStringList, то он был удален и так далее. Моя логика заключалась в том, чтобы просто сравнить массивы и использовать операцию .contains, чтобы увидеть, содержит ли один массив элемент или нет. Однако это занимает очень много времени. Порядка многих минут, что не сработает. beforeStringList и afterStringList содержат около 100 тыс. элементов каждый.
Вот как я пытаюсь это запустить:
DispatchQueue.global().async {
var output = beforeStringList.filter{afterStringList.contains($0) == false}
}
Я также попробовал использовать set, который ничем не отличался по производительности:
var set = Set(beforeStringList.map {$0})
DispatchQueue.global().async {
var rowsAdded = set.filter { row in
if afterStringList.contains(row) == false{
print("row: \(row)")
}
return true
}
}
Открыт для всех идей, как это улучшить!
Кроме того, вы проводите измерения в оптимизированной сборке выпуска? Отладочные сборки выполняются намного медленнее.
Вы неправильно используете Set, поэтому получаете квадратичную временную сложность. Set выполняется медленнее, чем Array. Его преимущество — быстрее выдавать contains чеки. Вам следует перебирать beforeStringList: Array, сверяя contains с afterStringList: Set. А еще лучше использовать методы Set, как сказал Ларм, которые делают это правильно.
Не лучше ли сделать это на уровне базы данных?



Я тестировал «командное приложение macOS» с помощью простого запуска.
Вот код, который я тестировал:
var beforeStringList: [String] = []
var afterStringList: [String] = []
for _ in 0...10000 {
beforeStringList.append("\(Int.random(in: 0...10000))")
afterStringList.append( "\(Int.random(in: 0...10000))")
}
//Testing Initial code: ie Array.filter() and Array.contains()
var startDate = Date()
var output1 = beforeStringList.filter {
afterStringList.contains($0) == false
}
print("Array.filter() and Array.contains(): \(Date().timeIntervalSince(startDate))")
//Testing with using a Set for the beforeList: ie Set.filter() and Array.contains()
startDate = Date()
var beforeSet = Set(beforeStringList)
var output2 = beforeSet.filter { afterStringList.contains($0) == false }
print("Set.filter() and Array.contains(): \(Date().timeIntervalSince(startDate))")
//Testing with using a Set for the afterList: Array.filter() and Set.contains()
startDate = Date()
var afterSet = Set(afterStringList)
var output3 = beforeStringList.filter { afterSet.contains($0) == false }
print("Array.filter() and Set.contains(): \(Date().timeIntervalSince(startDate))")
//Testing with using a Set for the beforeList and afterList: Set.subtract()
startDate = Date()
beforeSet = Set(beforeStringList)
afterSet = Set(afterStringList)
var output4 = beforeSet.subtracting(afterSet)
print("Set.subtract(): \(Date().timeIntervalSince(startDate))")
//Testing with using a Set for the beforeList: Set.subtract()
startDate = Date()
beforeSet = Set(beforeStringList)
var output5 = beforeSet.subtracting(afterStringList)
print("Set.subtract() on array: \(Date().timeIntervalSince(startDate))")
print("--- Verifications ---")
print("Verifying output2")
print(Set(output1) == Set(output2))
print("Verifying output 3")
print(Set(output1) == Set(output3))
print("Verifying output 4")
print(Set(output1) == Set(output4))
print("Verifying output 5")
print(Set(output1) == Set(output5))
print("--- end ---")
У меня были разные выводы, но вот один из них:
$>Array.filter() and Array.contains(): 3.4921441078186035
$>Set.filter() and Array.contains(): 2.178665041923523
$>Array.filter() and Set.contains(): 0.0021200180053710938
$>Set.subtract(): 0.002646923065185547
$>Set.subtract() on array: 0.0021669864654541016
$>--- Verifications ---
$>Verifying output2
$>true
$>Verifying output 3
$>true
$>Verifying output 4
$>true
$>Verifying output 5
$>true
Итак, давайте проанализируем исходный код:
array1.filter { array2.contains($0) } //simplified
Итак, Array.filter() выполняет итерацию для каждого элемента array1, а array2 повторяется (от 1 до array2.count или меньше, если элемент, если элемент найден ранее, считается 1 полной итерацией). Итак, Array.contains() был частью замедления вашего кода.
Затем вы продолжили использовать Set вместо array1, но операция по-прежнему требует времени, поскольку Set «оптимизирован» для Set.contains(). Итак, «обратный путь» (Array.filter() + Set.contains() быстрее).
Отсюда вы можете использовать Set, но затем полностью Set и использовать методы Set. В вашем случае вы хотите использовать Set.subtract()
Теперь, согласно этой информации, мы могли бы сказать: давайте использовать Set.subtractOnArray() (где subtractOnArray() — это Set.subtract() с параметром массива) или комбинацию Array.filter() + Set.contains(), но истина может лежать посередине. Нет? Почему Set.subtract() работает медленнее, чем если бы мы говорили ранее, что он «оптимизирован» для Set.contains() и тому подобное?
Я всегда ставлю startDate = Date() ПЕРЕД преобразованием Array в Set.
Если я поставлю это позже, сосредоточив внимание только на Array.filter(), Set.filter(),Array.contains(), Set.contains(),Set.subtract(), Set.subtractOnArray(), другой пример вывода будет таким:
$>Array.filter() and Array.contains(): 3.4415539503097534
$>Set.filter() and Array.contains(): 2.1599819660186768
$>Array.filter() and Set.contains(): 0.0010540485382080078
$>Set.subtract(): 0.00034797191619873047
$>Set.subtract() on array: 0.0010139942169189453
И здесь мы видим полную победу subtract().
Действительно, в зависимости от ваших данных преобразование Array в Set может занять некоторое время. Все зависит от того, какую часть вы хотите оптимизировать, от ваших вариантов использования и особенно от того, имеет ли это значение в данном случае. Если что-то занимает «0,0003» секунды или «0,002» секунды, действительно ли я это увижу?
Теперь я предположил, что порядок не имеет значения, и в этом случае вывод будет «неупорядоченным», и в этом случае вы можете использовать NSMutableOrderedSet или версию Swift в Swift-Collections OrderedSet. Здесь нет «вычитания», но вы можете найти свой путь туда.
Отличный ответ, как обычно. Небольшая проблема: у вас опечатка в коде регистрации. Вероятно, следует читать print("Set.subtract()..., а не print("Set.substract().... (Сначала я немного растерялся.)
Действительно. После быстрого поиска substract действительно существовал на английском языке до 18 века из-за французского влияния, а поскольку я француз, мой родной язык все еще влияет на мой английский в 21 веке ^^
Хорошая мысль о включении преобразования в набор в сравнение времени. В любом случае я склонен заключать каждую часть обоих подходов, которые я сравниваю, в свои временные блоки, но я специально не задумывался о стоимости преобразования массивов в множества. Во многих случаях я ожидаю, что экономия будет перегружена, поскольку contains() — это операция O(n) над массивами, а циклическое перебор массивов с содержанием становится O(n²) (то есть производительность ОТСТАЛЬНАЯ).
Спасибо за очень подробное объяснение, я ценю это. Я собираюсь попробовать этот подход.
DispatchQueue.global().async {
let afterSet = Set(afterStringList)
var output = beforeStringList.filter { !afterSet.contains($0) }
// Now 'output' contains items that are in 'beforeStringList' but not in 'afterStringList'
}
.contains(row), это то, что должно занять время. Поскольку порядок вас, похоже, не беспокоит, как насчет использования двухSet? На нем есть полезные методы сintersect,substractи т. д.