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 для создания ссылки, которая не будет сохранена при передаче. Я не знал об этом, когда начинал, и я бы сговорился рекомендовать его вообще, потому что это взломать, но я использовал его в нескольких случаях для обертывания сильно используемого массива, который выполнял удержание/выпуск каждый раз, когда он был ссылка.