Swift 字典即使经过优化也很慢:做不必要的事情 retain/release?
Swift Dictionary slow even with optimizations: doing uncessary retain/release?
以下代码将简单的值持有者映射到布尔值,在 Java 中的运行速度比 Swift 2 - XCode 7 beta3、"Fastest, Aggressive Optimizations [-Ofast]" 和 Java 快 20 倍以上"Fast, Whole Module Optimizations" 开启。我可以在 Java 中获得超过 280M lookups/sec 但在 Swift 中只能获得大约 10M。
当我在 Instruments 中查看它时,我发现大部分时间都进入了一对与地图查找相关的 retain/release 调用。任何关于为什么会发生这种情况或解决方法的建议将不胜感激。
代码的结构是我真实代码的简化版本,它有一个更复杂的键class并且还存储了其他类型(尽管布尔是我的实际情况)。另外,请注意,我使用单个可变键实例进行检索以避免在循环内分配对象,根据我的测试,这在 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 );
}
经过大量实验后,我得出了一些结论并找到了解决方法(尽管有些极端)。
首先我要说的是,我认识到这种在紧密循环中进行的非常细粒度的数据结构访问并不能代表一般性能,但它确实会影响我的应用程序,我正在想象其他应用程序,例如游戏和大量数字应用程序。另外我要说的是,我知道 Swift 是一个移动的目标,我相信它会有所改善 - 也许在您阅读本文时,我下面的解决方法(技巧)将不再需要。但是,如果您今天正在尝试做这样的事情并且您正在查看 Instruments 并看到您的大部分应用程序时间花费在 retain/release 并且您不想在 objc 中重写整个应用程序,请继续阅读。
我发现 几乎 在 Swift 中触及对象引用的任何事情都会导致 ARC retain/release 惩罚。另外,可选值——甚至是可选原语——也会产生这种成本。这几乎排除了使用 Dictionary 或 NSDictionary 的可能性。
以下是您可以在解决方法中加入的一些快速方法:
a) 基本类型数组。
b) 只要数组在堆栈上而不是堆上,最终对象的数组只要。例如在方法主体内声明一个数组(但当然在循环之外)并迭代地将值复制到它。不要 Array(array) 复制它。
将这些放在一起,您可以构建一个基于数组的数据结构,该数组存储例如整数,然后将数组索引存储到该数据结构中的对象。在循环中,您可以通过快速本地数组中的索引查找对象。在你问 "couldn't the data structure store the array for me" 之前 - 不,因为那会招致我上面提到的两项处罚:(
考虑到所有因素,此解决方法并不算太糟糕 - 如果您可以枚举要存储在字典/数据结构中的实体,您应该能够按照所述将它们托管在数组中。在我的例子中,使用上面的技术,我能够将 Java 的性能提高 2 倍 Swift 。
如果此时有人仍在阅读并对这一点感兴趣,我会考虑更新我的示例代码并发布。
编辑:我会添加一个选项:c) 也可以在 Swift 中使用 UnsafeMutablePointer<> 或 Unmanaged<> 来创建一个在传递时不会保留的引用。当我开始时我并没有意识到这一点,我一般会犹豫推荐它,因为它是一个 hack,但我在一些情况下使用它来包装一个经常使用的数组,该数组每次都会产生 retain/release它被引用了。
以下代码将简单的值持有者映射到布尔值,在 Java 中的运行速度比 Swift 2 - XCode 7 beta3、"Fastest, Aggressive Optimizations [-Ofast]" 和 Java 快 20 倍以上"Fast, Whole Module Optimizations" 开启。我可以在 Java 中获得超过 280M lookups/sec 但在 Swift 中只能获得大约 10M。
当我在 Instruments 中查看它时,我发现大部分时间都进入了一对与地图查找相关的 retain/release 调用。任何关于为什么会发生这种情况或解决方法的建议将不胜感激。
代码的结构是我真实代码的简化版本,它有一个更复杂的键class并且还存储了其他类型(尽管布尔是我的实际情况)。另外,请注意,我使用单个可变键实例进行检索以避免在循环内分配对象,根据我的测试,这在 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 );
}
经过大量实验后,我得出了一些结论并找到了解决方法(尽管有些极端)。
首先我要说的是,我认识到这种在紧密循环中进行的非常细粒度的数据结构访问并不能代表一般性能,但它确实会影响我的应用程序,我正在想象其他应用程序,例如游戏和大量数字应用程序。另外我要说的是,我知道 Swift 是一个移动的目标,我相信它会有所改善 - 也许在您阅读本文时,我下面的解决方法(技巧)将不再需要。但是,如果您今天正在尝试做这样的事情并且您正在查看 Instruments 并看到您的大部分应用程序时间花费在 retain/release 并且您不想在 objc 中重写整个应用程序,请继续阅读。
我发现 几乎 在 Swift 中触及对象引用的任何事情都会导致 ARC retain/release 惩罚。另外,可选值——甚至是可选原语——也会产生这种成本。这几乎排除了使用 Dictionary 或 NSDictionary 的可能性。
以下是您可以在解决方法中加入的一些快速方法:
a) 基本类型数组。
b) 只要数组在堆栈上而不是堆上,最终对象的数组只要。例如在方法主体内声明一个数组(但当然在循环之外)并迭代地将值复制到它。不要 Array(array) 复制它。
将这些放在一起,您可以构建一个基于数组的数据结构,该数组存储例如整数,然后将数组索引存储到该数据结构中的对象。在循环中,您可以通过快速本地数组中的索引查找对象。在你问 "couldn't the data structure store the array for me" 之前 - 不,因为那会招致我上面提到的两项处罚:(
考虑到所有因素,此解决方法并不算太糟糕 - 如果您可以枚举要存储在字典/数据结构中的实体,您应该能够按照所述将它们托管在数组中。在我的例子中,使用上面的技术,我能够将 Java 的性能提高 2 倍 Swift 。
如果此时有人仍在阅读并对这一点感兴趣,我会考虑更新我的示例代码并发布。
编辑:我会添加一个选项:c) 也可以在 Swift 中使用 UnsafeMutablePointer<> 或 Unmanaged<> 来创建一个在传递时不会保留的引用。当我开始时我并没有意识到这一点,我一般会犹豫推荐它,因为它是一个 hack,但我在一些情况下使用它来包装一个经常使用的数组,该数组每次都会产生 retain/release它被引用了。