关于性能:即使使用-Ofast,Swift的字典也很慢

Swift's Dictionary is slow even with -Ofast

我在SWIFT中使用一个EDCOX1的0个实现基本上是一个缓存。演出远没有我预料的那么好。我已经阅读了一些其他的问题,例如这个关于数组排序的问题,这似乎表明EDCOX1的1个答案是答案(如果你准备接受它带来的变化)。然而,即使在编译EDCOX1(1)时,性能与其他语言相比也很差。我使用Swift版本1(SWIFT-600 .0.34.4.8)。

下面是一个简单的例子,说明了这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Foundation

class Holder {
    var dictionary = Dictionary<Int, Int>()

    func store(#key: Int, value: Int) {
        dictionary[key] = value
    }
}

let holder = Holder()

let items = 5000

for (var i: Int = 0; i < 5000; i++) {
    holder.store(key: i, value: i)
}

-O3编译,运行时间超过2秒:

1
2
3
4
5
xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) -O3 Test.swift && time ./Test

real    0m2.295s
user    0m2.176s
sys     0m0.117s

使用-Ofast编译可获得3-4x改进:

1
2
3
4
5
xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) -Ofast Test.swift && time ./Test

real    0m0.602s
user    0m0.484s
sys     0m0.117s

通过比较,这种Java实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.Map;
import java.util.HashMap;

public class Test {
    public static void main(String[] args) {
        Holder holder = new Holder();
        int items = 5000;
        for (int i = 0; i < items; i++) {
            holder.store(i, i);
        }
    }
}

class Holder {
    private final Map<Integer, Integer> map = new HashMap<Integer, Integer>();

    public void store(Integer key, Integer value) {
        map.put(key, value);
    }
}

再快约6倍:

1
2
3
4
5
javac Test.java && time java Test

real    0m0.096s
user    0m0.088s
sys     0m0.021s

这仅仅是复制Dictionary的成本吗?因为它变异并存储在Holder实例中,这导致了swift的糟糕表现。移除Holder并直接访问Dictionary,这表明确实如此。

此代码:

1
2
3
4
5
6
7
8
9
import Foundation

var dictionary = Dictionary<Int, Int>()

let items = 5000

for (var i: Int = 0; i < 5000; i++) {
    dictionary[i] = i
}

明显更快:

1
2
3
4
5
6
7
8
9
10
11
$ xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) -O3 NoHolder.swift && time ./NoHolder

real    0m0.011s
user    0m0.009s
sys     0m0.002s

$ xcrun swift -sdk $(xcrun --show-sdk-path --sdk macosx) -Ofast NoHolder.swift && time ./NoHolder

real    0m0.011s
user    0m0.007s
sys     0m0.003s

虽然它提供了一个(希望)有趣的数据点,但是在我的情况下,直接访问字典是不可能的。我还能做些什么来接近这一水平的表现与斯威夫特在其目前的形式?


我是贝塔医生。

我认为现在的答案就是Swift是beta版的,工具是beta版的,还有很多优化工作要做。在obj-c中复制您的"holder"类示例表明,即使在相同的-Ofast级别,它也要快得多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@import Foundation;

@interface Holder : NSObject

@property NSMutableDictionary *dictionary;
- (void)storeValue:(NSInteger)value forKey:(NSString *)key;

@end

@implementation Holder

- (instancetype)init {
   self = [self initWithDict];
    return self;
}


- (instancetype)initWithDict {
    if (!self) {
        self = [super init];
        _dictionary = [NSMutableDictionary dictionary];
    }

    return self;
}

- (void)storeValue:(NSInteger)value forKey:(NSString *)key {
    [self.dictionary setValue:@(value) forKey:key];
}

@end

int main(int argc, const char * argv[]) {

    Holder *holder = [Holder new];

    for (NSInteger i = 0; i < 5000; i++) {
        [holder storeValue:i forKey:[NSString stringWithFormat:@"%ld", i]];
    }

}

OBJ-C很快就出了大门。

1
2
3
4
5
time ./loop

    real    0m0.013s
    user    0m0.006s
    sys     0m0.003s

与您给出的noholder示例的时间相似性很好地说明了obj-c编译器正在做多少优化。

查看Swift中的-O3-Ofast级别的组件,发现所做的安全检查量有很大差异。查看obj-c组件可以发现,要执行的操作要少得多。因为使程序快速运行的关键是使它不需要做太多的工作…

1
2
3
4
OS-X-Dos-Equis:~ joshwisenbaker$ wc -l objc.txt
     159 objc.txt
OS-X-Dos-Equis:~ joshwisenbaker$ wc -l oFast.txt
    3749 oFast.txt

(编辑:更新最终确定holder类的结果。)

另一个有趣的问题是在类定义上使用@final装饰。如果你知道你的类永远不会被子类化,那么试着添加这样的关键字:@final class Holder

如您所见,它还规范化了以相同方式编译时的性能。

1
2
3
4
5
OS-X-Dos-Equis:~ joshwisenbaker$ swift -sdk $(xcrun --show-sdk-path --sdk macosx) -Ofast bench.swift && time ./bench

real    0m0.013s
user    0m0.007s
sys     0m0.003s

即使只使用-O3@final也会产生魔力。

1
2
3
4
5
OS-X-Dos-Equis:~ joshwisenbaker$ swift -sdk $(xcrun --show-sdk-path --sdk macosx) -O3  bench.swift && time ./bench

real    0m0.015s
user    0m0.009s
sys 0m0.003s

同样,我认为您在性能上看到的差异可能在编译时降到了当前的优化级别。


Swift 4.2的更新:我从@霍华德LoVATT使用HORD测试类获得130到180Ms的速度。

速度尖端α1:如果您正在使用游乐场进行测试,请将所有代码(包括循环测试方法)放置在单独的源文件中。

速度尖端α2:在这种情况下,通过将字典预分配到适当的大小,可以实现大约1.5到3x的速度增加:var dictionary = Dictionary(minimumCapacity: 1_000_000)


不幸的是,我得到了更糟糕的结果。

我修改了Java代码以避免启动时间,增加循环次数以获得更多可重复的定时,并检查结果以防止JVM优化循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.Map;
import java.util.HashMap;

public class HolderTest {
  private static final int items = 1_000_000;

  public static void main(String[] args) {
    final long start = System.nanoTime();
    final Holder holder = new Holder();
    for (int i = 0; i < items; i++) {
      holder.store(i, i);
    }
    final long finish = System.nanoTime();
    System.out.println("time =" + (finish - start) / 1_000_000.0 +" ms, holder 0 =" + holder.map.get(0) +", holder" + (items - 1) +" =" + holder.map.get(items - 1));
  }
}

class Holder {
  final Map<Integer, Integer> map = new HashMap<>();

  public void store(Integer key, Integer value) {
    map.put(key, value);
  }
}

同样,银行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Foundation

class Holder {
    var dictionary = Dictionary<Int, Int>()

    func store(#key: Int, value: Int) {
        dictionary[key] = value
    }
}

let start = CFAbsoluteTimeGetCurrent()
let holder = Holder()
let items = 1_000_000
for i in 0 ..< items {
    holder.store(key: i, value: i)
}
let finish = CFAbsoluteTimeGetCurrent()
println("time = \((finish - start) * 1000.0) ms, holder 0 = \(holder.dictionary[0]), holder \(items - 1) = \(holder.dictionary[items - 1])")

我得到了300毫秒的Java和20秒(!)对于Swift:(

这是6点1分。

更新1:

改为nsmutabledictionary性能更好。

sunzero ln:holdertest lov080$swift-sdk$(xrun--show sdk path--sdk macosx)-ouncheck main.swift时间=647.060036659241 ms,支架0=可选(0),支架999999=可选(999999)

比Java慢2倍,但好多了!

更新2:

虽然我在xcode中要求取消选中-)但似乎我没有得到它(可能我还需要其他设置:()。从命令行,使用swift字典的swift版本给出:

sunzero ln:holdertest lov080$swift-sdk$(xrun--show sdk path--sdk macosx)-ouncheck main.swift时间=303.406000137329 ms,支架0=可选(0),支架999999=可选(999999)

IE与Java- Huray:


在Xcode6接近发布,Apple禁用调试代码并完成优化器之前,为什么不简单地将var声明为nsmutabledictionary呢?

这是经过现场验证的,而且速度相当快。

1
2
3
4
5
6
class Holder {
    var dictionary = NSMutableDictionary()
    func store(#key: Int, value: Int) {
        dictionary[key] = value
    }
}

如果字典的性能相似或更好,您可以稍后再更改它。

更新:

我在TestPerformanceHolder()的单元测试中尝试了上述代码。

用-O3进行优化,平均完成013秒,比Java示例快7倍。