【问题标题】:Swift Dictionary slow even with optimizations: doing uncessary retain/release?Swift Dictionary 即使优化也很慢:做不必要的保留/释放?
【发布时间】:2015-07-11 04:09:03
【问题描述】:

以下代码将简单的值持有者映射到布尔值,在 Java 中的运行速度比 Swift 2 快 20 倍以上 - XCode 7 beta3、“Fastest, Aggressive Optimizations [-Ofast]”和“Fast, Whole Module Optimizations”开启.我可以在 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 );
    }

【问题讨论】:

  • 您是否尝试将 MyKey 从类更改为结构或使用indexForKey(key: Key) 查找数据?结构具有不同的内存管理,indexForKey 可能不同,因为它不返回对象,只是索引。
  • 我正在尝试运行它,它显示错误未解析标识符Time
  • 为了保持代码的重点,我没有包含我琐碎的计时器代码。我把它放在这里(Java 和 Swift):gist.github.com/patniemeyer/bf73e0e6f06a8b6de97e
  • 使用 indexForKey() 的速度差不多。
  • “使用 indexForKey() 的速度差不多” 出于同样的原因 - 您每次都会生成一个额外的 Optional。

标签: performance swift swift2


【解决方案1】:

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

首先让我说,我认识到这种紧密循环内的非常细粒度的数据结构访问并不代表一般性能,但它确实会影响我的应用程序,我正在想象其他应用程序,例如游戏和大量数字应用程序。还要让我说,我知道 Swift 是一个移动的目标,我相信它会改进——也许当你阅读本文时,我不需要下面的解决方法(hacks)。但是,如果您今天尝试做这样的事情,并且您正在查看 Instruments 并看到您的应用程序的大部分时间都花在了保留/发布上,并且您不想用 objc 重写整个应用程序,请继续阅读。

我发现几乎在 Swift 中所做的任何涉及对象引用的操作都会导致 ARC 保留/释放惩罚。此外,可选值 - 甚至可选原语 - 也会产生此成本。这几乎排除了使用 Dictionary 或 NSDictionary 的可能性。

您可以在解决方法中包含以下一些快速的内容:

a) 原始类型数组。

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

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

考虑到这种解决方法的所有事情都不算太糟糕 - 如果您可以枚举要存储在 Dictionary / 数据结构中的实体,您应该能够将它们托管在所描述的数组中。就我而言,使用上述技术,我能够在 Swift 中将 Java 性能提高 2 倍。

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

编辑:我要添加一个选项:c) 也可以在 Swift 中使用 UnsafeMutablePointer 或 Unmanaged 来创建一个在传递时不会保留的引用。当我开始时我并没有意识到这一点,我一般会犹豫推荐它,因为它是一个黑客,但我在少数情况下使用它来包装一个频繁使用的数组,每次它都会导致保留/释放它是参考。

【讨论】:

  • “如果此时有人仍在阅读并感兴趣,我将考虑更新我的示例代码并发布”我对此投赞成票。
  • 顺便说一下,您可能想观看 WWDC 2015 视频 409,因为您的结论听起来很像他们的结论。
  • 感谢您指点我观看该视频 - 我刚刚观看了它,这很有趣,但他们的大部分建议归结为尽可能确保事情是最终的,并打开整个模块优化。
  • 好的,但是 IIRC 他们确实说了一些关于引用类型如何涉及引用计数以及一个简单的 for 循环如何即使对于一个看起来很无辜的 Swift 结构来说也很昂贵的事情。
  • 我不会说使用 Unsafe[Mutable]Pointer 或 Unmanaged 是一种 hack;恕我直言,如果您想绕过 ARC,这是完全合法的(无论您是想使用遗留代码还是只是加快速度并不重要)。但是,您应该有充分的理由绕过 ARC,并且应该谨慎行事,以避免内存泄漏或访问已释放的内存。
猜你喜欢
  • 2011-09-23
  • 2014-08-20
  • 1970-01-01
  • 2011-06-25
  • 2015-05-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多