Swift Dictionary即使在优化的情况下也会变慢:执行uncessary retain / release?

时间:2015-07-11 04:09:03

标签: performance swift swift2

下面的代码将简单的值持有者映射到布尔值,在Java中运行速度比Swift 2快20倍 - XCode 7 beta3,“最快,最具侵略性的优化[-Ofast]”和“快速,整体模块优化”开启。我可以在Java中获得超过280M查询/秒,但在Swift中只能获得大约10M.

当我在Instruments中查看它时,我发现大部分时间都会进入与地图查找相关联的一对保留/释放调用。任何关于为什么会发生这种情况或解决方法的建议都将受到赞赏。

代码的结构是我的真实代码的简化版本,它具有更复杂的密钥类,并且还存储其他类型(尽管布尔值是我的实际情况)。另外,请注意我使用单个可变键实例进行检索以避免在循环内分配对象,根据我的测试,这在Swift中比在不可变键中更快。

编辑:我也试过切换到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 个答案:

答案 0 :(得分:6)

经过多次实验,我得出了一些结论并找到了解决方法(虽然有些极端)。

首先让我说,我认识到在紧密循环中这种非常精细的数据结构访问并不代表一般性能,但它确实会影响我的应用程序,我想象其他人喜欢游戏和大数字应用程序。另外,我要说,我知道Swift是一个移动目标,我相信它会有所改进 - 也许在您阅读本文时,我的解决方法(黑客)可能没有必要。但是,如果您今天尝试做类似的事情并且您正在查看仪器并看到您在保留/发布中花费的大部分应用时间,并且您不想在objc中重写整个应用程序,请继续阅读。

我发现几乎任何在Swift中触及对象引用的内容都会导致ARC保留/释放惩罚。另外,可选值 - 甚至是可选的基元 - 也会产生这种成本。这几乎排除了使用Dictionary或NSDictionary。

以下是一些可以包含在变通方法中的快速内容:

a)原始类型的数组。

b)只要数组在堆栈上而不在堆上,最终对象的数组就像一样长。例如在方法体中声明一个数组(当然在你的循环之外)并迭代地将值复制到它。不要将数组(数组)复制它。

将这些放在一起你可以构建一个基于数组的数据结构,这些数组存储例如Ints然后将数组索引存储到该数据结构中的对象。在循环中,您可以通过快速本地数组中的索引查找对象。在你问“数据结构不能为我存储阵列”之前 - 不,因为那将导致我上面提到的两个处罚:(

考虑到这一点的所有方法都不错 - 如果你可以枚举你想要存储在Dictionary / data结构中的实体,你应该能够按照描述将它们托管在一个数组中。使用上面的技术,在我的案例中,我能够在Swift中将Java性能提高2倍。

如果有人还在阅读并对此感兴趣,我会考虑更新我的示例代码并发布。

编辑:我添加了一个选项:c)也可以使用UnsafeMutablePointer&lt;&gt;或非管理&lt;&gt;在Swift中创建一个在传递时不会被保留的引用。当我开始时我并没有意识到这一点,我会毫不犹豫地推荐它,因为它是一个黑客攻击,但我在一些情况下使用它来包装一个使用频繁的数组,每次都会产生一个保留/释放引用。