В моем приложении мне нужно хранить на диске миллионы пользовательских структур, содержащих 2 атрибута UUID. Мой проект требует, чтобы они были записаны и прочитаны на диск очень быстро. Я попробовал использовать JSONDecoder, но обнаружил, что производительность очень низкая при сохранении от 100 тысяч до примерно 10 миллионов этих структур, что заняло от 3 до 30 секунд, как показало тщательное тестирование.
Есть ли гораздо более эффективный способ сохранить структуру всего с двумя UUID, например, используя двоичное представление? Мне не удалось найти ресурсы по сохранению UUID на диск. Мне не нужен формат JSON для сохранения и чтения с диска.
struct MetadataItem: Identifiable, Codable {
var id = UUID()
let itemId: UUID
}
//Just to simulate adding the custom structs
for i in 1...1000000 {
let newItem = MetadataItem(itemId: UUID())
metadataItems.append(newItem)
}
do {
let data = try JSONEncoder().encode(metadataItems)
if let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first {
try data.write(to: url.appending(path: "Metadata"))
}
print("Done")
} catch {
}
Я пробовал основные данные, но сохранение большого количества этих данных было слишком медленным, в среднем около 5 секунд или намного больше даже для 100 тысяч этих записей. Мое приложение требует, чтобы данные были сохранены и прочитаны как можно быстрее как минимум для миллиона этих структур UUID. Мне нужно просмотреть массив uuid, чтобы получить правильный, но кроме этого нет необходимости в быстром поиске. Эти UUID на самом деле предназначены только для извлечения связанных объектов данных Core. Данные обновляются не слишком часто.
Кроме того, мне нужно упорядочить эти структуры, но в основных данных это потребует переиндексации ненужных атрибутов индекса при обновлении или другого объекта, с которым нужно иметь связь, чтобы многие отношения были упорядочены, что означает, что о NSBatchInsertRequest не может быть и речи, который нельзя использовать. устанавливать отношения. Так что у меня нет другого выбора
Тогда один из способов, который вы можете использовать, — это просто записать каждый itemId
в файл, названный по его id
. При таком большом количестве файлов вам, вероятно, потребуется реализовать структуру каталогов. Вы можете взять первые четыре символа id
и использовать их для ряда каталогов, так что в качестве идентификатора 1234-abc123-def789-0987
у вас будет путь к файлу 1/2/3/4/1234-abc123-def789-0987
. Это даст вам около 150 файлов в конечном каталоге для 10 миллионов идентификаторов.
Также важно понять, записаны ли эти 10 миллионов записей один раз или неоднократно. Одной 30-секундной настройки при первом запуске может быть достаточно, поскольку вы можете показать представление активности и объяснить, что происходит. Индивидуальное чтение и запись будут быстрыми.
Вы также можете использовать SQLite напрямую, но я не думаю, что вы найдете способ создать 10 миллионов записей за нулевое время.
Я понимаю. Мне было интересно, а как насчет PropertyListEncoders с двоичным выходным форматом, это быстрее и эффективнее, чем кодировщик json?
Просматривая некоторые другие ваши вопросы, я думаю, вам нужно переосмыслить свой фундаментальный подход. Действительно ли необходимо поддерживать строгий порядок в 10 миллионов (или даже 10 тысяч) элементов в плейлисте? Пользователь не может с этим справиться. Все, что им нужно видеть, это следующие 10 или 20 элементов и иметь возможность добавить новый элемент в начало очереди (или, возможно, в какой-то другой момент очереди). Вы даже можете получить больше предметов для игры, прокручивая список. Затем вы просто управляете элементами, которые будут воспроизводиться, и элементами, которые недавно воспроизводились.
Давайте продолжим обсуждение в чате.
Одна из главных причин заключается в том, что Apple Music и Spotify мгновенно загружают следующую очередь в правильном порядке плейлистов даже для плейлистов из 100 тысяч песен после тестирования, когда они загружаются для прослушивания в автономном режиме. Итак, я пытался реализовать подобное
Вы можете попробовать сохранить два UUID в файл или файл zip
в виде строк, разделенных запятыми (или, что еще лучше, в его двоичном представлении). Например, id.uuidString, itemId.uuidString
, затем при необходимости воссоздайте структуры/UUID из строк.
Как сохранить двоичное представление структуры?
Для uuid используйте: id.uuidString.data(using: .utf8)
для всей структуры, которую вы можете использовать NSKeyedArchiver.archivedData...
Вероятно, вы получите максимальную производительность, если напишете собственный код файлового менеджера, который выполняет запись файлов с произвольным доступом с записями фиксированной длины и запись двоичных данных.
Чтобы преобразовать MetadataItem
в двоичный код, а затем прочитать его обратно,
вы можете попробовать этот подход, как показано в примере кода.
Обратите внимание: я не проверял это на эффективность.
struct MetadataItem: Identifiable, Codable {
var id: UUID // <--- here
let itemId: UUID
}
struct ContentView: View {
var body: some View {
Text("testing")
.onAppear {
// test struct
let metadataItem = MetadataItem(id: UUID(), itemId: UUID())
print("---> metadataItem: \(metadataItem)\n")
// convert to binary
let binaryData = metadataItemToBinary(item: metadataItem)
print("------> binaryData: \(binaryData)")
// write to file
// read from file
// then recreate the struct
if let restoredItem = binaryToMetadataItem(data: binaryData) {
print("\n---> restored item: \(restoredItem)")
}
}
}
func metadataItemToBinary(item: MetadataItem) -> Data {
var data = Data()
data.append(item.id.uuidString.data(using: .utf8)!)
data.append(item.itemId.uuidString.data(using: .utf8)!)
print("------> data: \(data)")
return data
}
func binaryToMetadataItem(data: Data) -> MetadataItem? {
guard data.count == 72 else {
return nil
}
let idData = data.subdata(in: 0..<36)
let itemIdData = data.subdata(in: 36..<72)
if let id = UUID(uuidString: String(data: idData, encoding: .utf8)!),
let itemId = UUID(uuidString: String(data: itemIdData, encoding: .utf8)!) {
print("------> id: \(id) itemId: \(itemId)")
return MetadataItem(id: id, itemId: itemId)
} else {
// deal with errors
return nil
}
}
}
РЕДАКТИРОВАТЬ-1:
для теста массива:
.onAppear {
// test array of MetadataItem
let arr: [MetadataItem] = [MetadataItem(id: UUID(), itemId: UUID()), MetadataItem(id: UUID(), itemId: UUID())]
print("---> arr: \(arr)\n")
// convert to binary
var binarr: [Data] = arr.map{metadataItemToBinary(item: $0)}
// recover the structs
print("---> restored")
binarr.forEach{ data in
if let restoredItem = binaryToMetadataItem(data: data) {
print("---> item: \(restoredItem)")
}
}
}
Как вместо этого добавить массив MetadataItem к экземпляру Data в вашей функции и также получить обратно массив MetadataItem? Могу ли я просто добавить прямо с помощью функции карты?
Здесь я описал инструменты, позволяющие преобразовать MetadataItem
в двоичный формат, а затем преобразовать его обратно в структуру. Для такого массива просто добавьте в массив и сделайте то же самое.
вау, ваш метод был намного быстрее для миллиона элементов метаданных по сравнению с JSON Encoder или PropertyLisEncoder.. Интересно, будет ли двоичное представление намного быстрее. Действительно, это намного быстрее, чем любой из них.
обновил мой ответ с использованием массивов. Спасибо, рад, что это работает для вас.
Если кому-то интересно, после тестирования большего количества версий метода, предложенных «workdog», преобразование UUID в данные с использованием свойства .uuid и последующая запись на диск происходит намного быстрее, чем вместо использования строковой версии.
После тестирования вот что я получил за сохранение на диске 1 миллиона таких структур от самой медленной до самой быстрой...
PropertyListEncoder: 4,46 с.
JSONEncoder: 3,37 с.
Преобразование UUID в строку: 1,13 с.
Преобразование UUID в байты данных uuid: 345 мс.
func metadataItemsToBinary(items: [MetadataItem]) -> Data {
var data = Data()
for item in items {
data.append(withUnsafeBytes(of: item.id.uuid) { Data($0) })
data.append(withUnsafeBytes(of: item.itemId.uuid) { Data($0) })
}
return data
}
CoreData? Проблема с массивом JSON заключается в том, что вам нужно читать и записывать все значения одновременно, поэтому для изменения или добавления одного значения вам нужно прочитать весь массив, а затем переписать его. Каждый раз, когда вы имеете дело с IO и 10 миллионами, это может занять некоторое время. Вам необходимо предоставить более подробную информацию. Это разовая операция? Нужна ли вам возможность поиска по этим данным? Данные обновляются, удаляются или добавляются? Как часто это происходит?