Swift Dictionary медленный даже с оптимизацией: делать незавершенное сохранение/выпуск?
Следующий код, который сопоставляет простые носители значений с логическими значениями, быстрее работает на Java быстрее, чем Swift 2 - XCode 7 beta3, "Быстрая, агрессивная оптимизация [-Ofast]" и "Быстрая оптимизация всего модуля", Я могу получить более 280 м поисков/сек в Java, но только около 10 М в Swift.
Когда я смотрю на него в "Инструментах", я вижу, что большую часть времени идет в пару вызовов сохранения/освобождения, связанных с поиском карты. Любые предложения о том, почему это происходит или обходной путь, будут оценены.
Структура кода представляет собой упрощенную версию моего реального кода, который имеет более сложный класс ключей, а также хранит другие типы (хотя для меня является фактическим для Boolean). Также обратите внимание, что я использую один экземпляр изменяемого ключа для извлечения, чтобы избежать выделения объектов внутри цикла, и, согласно моим тестам, это быстрее в Swift, чем неизменяемый ключ.
EDIT: Я также попытался переключиться на NSMutableDictionary, но при использовании с объектами Swift в качестве ключей это кажется очень медленным.
EDIT2: я попытался выполнить тест в objc (который не имел бы дополнительных служебных служебных данных), и он быстрее, но все же на порядок медленнее, чем Java... Я собираюсь представить этот пример как еще один вопрос, чтобы увидеть, есть ли у кого-нибудь идеи.
EDIT3 - Ответ. Я опубликовал свои выводы и свое обходное решение в ответе ниже.
public final class MyKey : Hashable {
var xi : Int = 0
init( _ xi : Int ) { set( xi ) }
final func set( xi : Int) { self.xi = xi }
public final var hashValue: Int { return xi }
}
public func == (lhs: MyKey, rhs: MyKey) -> Bool {
if ( lhs === rhs ) { return true }
return lhs.xi==rhs.xi
}
...
var map = Dictionary<MyKey,Bool>()
let range = 2500
for x in 0...range { map[ MyKey(x) ] = true }
let runs = 10
for _ in 0...runs
{
let time = Time()
let reps = 10000
let key = MyKey(0)
for _ in 0...reps {
for x in 0...range {
key.set(x)
if ( map[ key ] == nil ) { XCTAssertTrue(false) }
}
}
print("rate=\(time.rate( reps*range )) lookups/s")
}
и вот соответствующий код Java:
public class MyKey {
public int xi;
public MyKey( int xi ) { set( xi ); }
public void set( int xi) { this.xi = xi; }
@Override public int hashCode() { return xi; }
@Override
public boolean equals( Object o ) {
if ( o == this ) { return true; }
MyKey mk = (MyKey)o;
return mk.xi == this.xi;
}
}
...
Map<MyKey,Boolean> map = new HashMap<>();
int range = 2500;
for(int x=0; x<range; x++) { map.put( new MyKey(x), true ); }
int runs = 10;
for(int run=0; run<runs; run++)
{
Time time = new Time();
int reps = 10000;
MyKey buffer = new MyKey( 0 );
for (int it = 0; it < reps; it++) {
for (int x = 0; x < range; x++) {
buffer.set( x );
if ( map.get( buffer ) == null ) { Assert.assertTrue( false ); }
}
}
float rate = reps*range/time.s();
System.out.println( "rate = " + rate );
}
Ответы
Ответ 1
После долгих экспериментов я пришел к некоторым выводам и нашел обходное решение (хотя и несколько экстремальное).
Прежде всего позвольте мне сказать, что я понимаю, что такой вид очень тонкой структуры данных в узком цикле не является репрезентативным для общей производительности, но это влияет на мое приложение, и я представляю других, таких как игры и сильно числовые приложения. Также позвольте мне сказать, что я знаю, что Swift - движущаяся цель, и я уверен, что она улучшится - возможно, к тому моменту, когда вы ее прочтете, мое обходное решение (хаки) не понадобится. Но если вы пытаетесь сделать что-то подобное сегодня, и вы смотрите на инструменты и видите большую часть времени вашего приложения, потраченного на сохранение/выпуск, и вы не хотите переписывать все свое приложение в objc, пожалуйста, прочитайте.
Я обнаружил, что почти все, что делает в Swift, касающееся ссылки на объект, несет штраф за сохранение/освобождение ARC. Дополнительно Необязательные значения - даже необязательные примитивы - также несут эту стоимость. Это в значительной степени исключает использование словаря или NSDictionary.
Вот некоторые быстрые вещи, которые вы можете включить в обходной путь:
a) Массивы примитивных типов.
b) Массивы конечных объектов до тех пор, пока массив находится в стеке, а не в куче. например Объявите массив внутри тела метода (но за пределами вашего цикла, конечно) и итеративно скопируйте значения в него. Не массируйте массив (массив).
Объединяя это вместе, вы можете построить структуру данных на основе массивов, которые хранятся, например. Ints, а затем хранить индексы массива для ваших объектов в этой структуре данных. Внутри цикла вы можете искать объекты по их индексу в быстром локальном массиве. Прежде чем спросить: "Не может ли структура данных хранить массив для меня" - нет, потому что это повлечет за собой два штрафа, о которых я упоминал выше: (
Все рассмотренные проблемы не так уж плохи. Если вы можете перечислить объекты, которые хотите сохранить в структуре Dictionary/data, вы сможете разместить их в массиве, как описано. Используя вышеприведенную технику, я смог превысить производительность Java в 2 раза в Swift в моем случае.
Если кто-то все еще читает и заинтересован в этом вопросе, я подумаю о том, чтобы обновить свой примерный код и опубликовать.
EDIT: я бы добавил параметр: c) Также можно использовать UnsafeMutablePointer < > или Unmanaged < > в Swift для создания ссылки, которая не будет сохранена при передаче. Я не знал об этом, когда начинал, и я бы сговорился рекомендовать его вообще, потому что это взломать, но я использовал его в нескольких случаях для обертывания сильно используемого массива, который выполнял удержание/выпуск каждый раз, когда он был ссылка.