一尘不染

Swift字典即使进行了优化也很慢:执行不必要的保留/释放?

swift

下面的代码将简单的值持有者映射为布尔值,在Java中的运行速度比Swift 2快20倍-XCode 7
beta3,“最快,积极的优化[-Ofast]”和“最快,完整的模块优化”处于打开状态。在Java中,每秒可以进行2.8亿次以上的查询,但是在Swift中,只能达到1000万次。

当我在Instruments中查看它时,我看到大多数时间都在进行与映射查找相关的一对保留/释放调用。关于为什么发生这种情况或解决方法的任何建议将不胜感激。

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

编辑:我也尝试过切换到NSMutableDictionary,但是当与Swift对象作为键一起使用时,它似乎非常慢。

EDIT2:我已经尝试在objc中实现测试(它不会具有可选的展开开销),它比Java更快,但仍然比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 );
    }

阅读 285

收藏
2020-07-07

共1个答案

一尘不染

经过大量的实验,我得出了一些结论并找到了一种解决方法(尽管有些极端)。

首先让我说,我认识到这种紧密循环内的非常细粒度的数据结构访问不能代表一般性能,但确实会影响我的应用程序,并且我在想象其他游戏,例如数字应用程序。另外,我想说我知道Swift是一个不断发展的目标,并且我相信它会有所改进-
也许到您读这篇文章时,下面的解决方法(hack)将不再必要。但是,如果您今天尝试做这样的事情,并且正在查看Instruments,并且看到大部分应用程序时间都在保留/释放上,并且您不想在objc中重写整个应用程序,请继续阅读。

我发现,在Swift中所做的 几乎 所有与对象引用相关的操作都会导致ARC保留/释放。此外,可选值-甚至可选基元-
也会产生此费用。这几乎排除了使用Dictionary或NSDictionary的可能性。

您可以在解决方法中包括以下一些快速操作:

a)基本类型数组。

b)最终对象 的数组,只要该数组在堆栈上而不在堆上即可
。例如,在方法主体中声明一个数组(但当然要在循环之外),然后将值迭代复制到该数组中。不要Array(array)复制它。

放在一起,您可以基于存储例如Ints的数组构建数据结构,然后将数组索引存储到该数据结构中的对象。在循环中,您可以通过快速本地数组中的对象索引来查找对象。在您问“数据结构无法为我存储数组”之前,否,因为这将招致我在上面提到的两项惩罚:(

所有被视为该解决方法的方法都还不错-如果您可以枚举要存储在Dictionary
/数据结构中的实体,则应该能够按照所述将它们托管在一个数组中。使用上面的技术,在我的案例中,我能够将Java性能超出Swift的2倍。

如果此时仍然有人在阅读并且有兴趣,我将考虑更新示例代码并发布。

编辑:我要添加一个选项:c)也可以在Swift中使用UnsafeMutablePointer <>或Unmanaged
<>来创建一个引用,该引用在传递时将不会保留。我刚开始时并没有意识到这一点,因此我通常会犹豫地推荐它,因为它是一个hack,但是在某些情况下,我已经使用它来包装一个频繁使用的数组,该数组每次发生保留/释放参考。

2020-07-07