Swift中的第一步,在BST中分配小对象时的性能问题

Iri*_*mFX 7 memory-management swift

在尝试学习Swift 2.2时,我在尝试分配许多小对象时遇到了严重的性能下降(从根本上说,是一个262144元素的BST).我目前的基准是一个古老的喀嚓,我几年前写的,一个Java 1.8.0_74编译它,在59秒(59036178微秒)我2012的Retina MacBook Pro的执行.我可以通过Instruments观察到的问题是每次迭代我会得到几十个swift_retain_和swift_release.不知道如何避免它们:

import Foundation
import Darwin;

import Foundation

public class BinarySearchTree<T : Comparable> {
    private var _value : T?;

    private var _leftTree : BinarySearchTree<T>?;
    private var _rightTree : BinarySearchTree<T>?;

    public init(value : T) {
        _value = value;
    }

    var value : T? {
        get {
            return self._value;
        }
        set {
            self._value = newValue;
        }
    }

    var leftTree : BinarySearchTree<T>? {
        get {
            return self._leftTree;
        }
        set {
            self._leftTree = newValue;
        }
    }

    var rightTree : BinarySearchTree<T>? {
        get {
            return self._rightTree;
        }
        set {
            self._rightTree = newValue;
        }
    }

    public func add(newValue : T) -> BinarySearchTree<T> {
        var navigator : BinarySearchTree<T>?;
        var subtree : BinarySearchTree<T>?;

        var done : Bool?;

        done = false;
        navigator = self;

        while (!done!) {
            if (newValue < navigator?.value) {
                subtree = navigator?.leftTree;
                if (subtree != nil) {
                    navigator = subtree;
                } else {
                    let newNode = BinarySearchTree<T>(value: newValue);
                    navigator!.leftTree = newNode;
                    done = true;
                }
            } else if (newValue > navigator?.value) {
                subtree = navigator?.rightTree;
                if (subtree != nil) {
                    navigator = subtree;
                } else {
                    let newNode = BinarySearchTree<T>(value: newValue);
                    navigator?.rightTree = newNode;
                    done = true;
                }
            } else {
                done = true;
            }
        }
        return self;
    }
} /* cut remove/search methods */
Run Code Online (Sandbox Code Playgroud)

这是我为测试运行编写的测试代码

let count : Int32 = 262144;
let base : Int32 = 65536;
let target : Int32 = count + 1;

var info = mach_timebase_info(numer:0, denom:0);
var timebase = mach_timebase_info(&info);
let numer = UInt64(info.numer);
let denom = UInt64(info.denom);
let norm = UInt64(numer/denom);

let check1 = (mach_absolute_time() * norm);

var root = BinarySearchTree<Int32>(value:base);

for var loop in 0 ... count-1 {
    if (loop % 1000 == 0) {
        print(loop);
    }
    root = root.add(loop);
}

let check2 = (mach_absolute_time() * norm);
print("Creation phase microseconds: [" + String((check2 - check1) / 1000) + "]");
Run Code Online (Sandbox Code Playgroud)

我试着寻找特定的快速释放/保留问题而没有运气,我不知道如何继续.感谢大家

Rob*_*ier 7

你注意到的问题是保留/释放(虽然它不是真的,保留/释放在嗯的力量旁边是微不足道的......我们最终会到达那里).这与分配无关.你没有分配额外的对象,你只是简单地保留它们然后释放它们.我将从Kenneth的代码开始,该代码优化了原始版本中的许多性能问题,但仍然存在这个问题.(我没有考虑递归代码,因为它在你当前的用例中崩溃了.但它确实躲避了一些冗余的保留.)

值得一提的是,Kenneth的代码很好,通常是你应该做的事情(因为随着你的进展,你会看到更多).

首先要注意:当你提到的时候-Ofast,那就是ObjC,而不是Swift.Swift的旗帜就是-O.你也想要-whole-module-optimization,但这对这里没有任何帮助.

还有一件小事,那我们就明白了.final随时标记课程.这确保没有动态调度.这与保留/释放相比并不重要,但是,嘿,采取简单的东西.

30%的音质听起来不错吗?

好的,现在是一个很大的,这是一个技巧.我发现我可以通过重写这个来减少大约30%的时间(从完全导入的大约6分钟到大约4分钟):

guard let subtree = navigator.leftTree else {
    navigator.leftTree = BinarySearchTree<T>(value: newValue)
    break
}
navigator = subtree
continue
Run Code Online (Sandbox Code Playgroud)

这样:

let subtree = navigator.leftTree
if subtree == nil {
    navigator.leftTree = BinarySearchTree(value: newValue)
    break
}
navigator = subtree!
continue
Run Code Online (Sandbox Code Playgroud)

这是一件非常谨慎的事情.在这种情况下,结果会更快,但在其他输入中可能不会那么快.对于优化器的更改可能没有那么快(SIL生成有点奇怪,我怀疑实际上可能是一个错误,因为它似乎navigator在第二种情况下加倍保留,但只有在if成功之后).但它似乎目前似乎更快.(编辑:Swift团队对这一发现感到惊讶,现在有一个错误对它开放了.不要指望这会在将来发挥作用.)

怎么样85%听起来怎么样?

但就像你说的那样,我们难道不能用结构避免这一切吗?但是每次触摸它时复制整棵树都是非常昂贵的.当然,我们可以通过像Array一样使用copy-on-write来显着改善它.但COW非常复杂.如果只有一种方法可以重用现有的东西.如果我们使用Array怎么办?

private struct Node<Element: Comparable> {
    let value: Element
    var leftIndex = -1 // Ugly, but ~25% faster than using Int? in my tests
    var rightIndex = -1
    init(_ value: Element) { self.value = value }
}

// This works exactly the same if you make it a `final class`. Your choice.
public struct BinarySearchTree<Element: Comparable> {
    private var storage: [Node<Element>] = []

    init(value: Element) { storage.append(Node(value)) }

    public mutating func add(newValue: Element) {
        if storage.isEmpty {
            storage.append(Node(newValue))
        }

        var index = 0

        while (true) {
            let node = storage[index]
            if (newValue < node.value) {
                if node.leftIndex < 0 {
                    storage.append(Node(newValue))
                    storage[index].leftIndex = storage.count - 1 // Don't use node here; remember value types!
                    break
                }
                index = node.leftIndex
                continue
            } else if (newValue > node.value) {
                if node.rightIndex < 0 {
                    storage.append(Node(newValue))
                    storage[index].rightIndex = storage.count - 1
                    break
                }
                index = node.rightIndex
                continue
            } else {
                break
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这需要大约45秒才能在我的系统上运行.当然这delete有点复杂.您要么必须接受"泄露"的内存(可能需要定期重新打包),要么您需要维护空闲列表.但是,自由主义者不会太难添加.

让我们尝试99.97%的改进而不做任何改变add().

当然,重要的是要记住,这是BST的近乎病态的情况.即使你经常按顺序交付数据,你最好在插入数据之前应用shuffle,甚至包括shuffle的成本.例如,使用shuffleInPlace(并计算其时间),插入完全相同的值:

var values = Array(0 ... count - 1)
values.shuffleInPlace()
for (loop, value) in values.enumerate() {
    if (loop % 1000 == 0) {
        print(loop)
    }
    root.add(value)
}
Run Code Online (Sandbox Code Playgroud)

这需要我们从45s到大约0.1s.(Kenneth的版本和我的"!"版本在这个指标下约为0.2秒;我可能会使用Kenneth的解决方案,final添加.甚至你的原始代码,肯尼斯修复了很多低效率,只需0.5秒记住,在我的系统上,带有按顺序添加的Kenneth优化版本是6分钟.)

在插入之前进行随机播放是值得的.如果你随着时间的推移得到了东西,那么在插入之前将它们分批并随机播放是值得的.如果树随着时间的推移而变化,那么值得检查它是否变得太深并且定期重建它.保持树深度合理压倒所有其他优化.解决Swift内存管理的聪明方法无法触及这一变化.

修复算法.相比之下,其他一切都是花生.


小智 1

我简化了您的代码,删除了一些代码Optionals和您的 getter/setter,因为它们是不必要的并且可能会导致代码缓慢。

我分析了你的代码和我的代码,并在相同的随机元素数据集上得到了这个结果:

1000 个元素:

您的:创建阶段微秒:[28680771]

我的:创建阶段微秒:[8564279]

10000 个元素:

您的:创建阶段微秒:[426233689]

我的:创建阶段微秒:[126725800]

这是我的代码:

public class BinarySearchTree2<T : Comparable> {
  public init(value : T) {
    self.value = value
  }

  var value : T
  var leftTree : BinarySearchTree2<T>?
  var rightTree : BinarySearchTree2<T>?

  public func add(newValue : T) -> BinarySearchTree2<T> {
    var navigator = self

    while (true) {
      if (newValue < navigator.value) {
        guard let subtree = navigator.leftTree else {
          navigator.leftTree = BinarySearchTree2<T>(value: newValue)
          break
        }
        navigator = subtree
        continue
      }
      if (newValue > navigator.value) {
        guard let subtree = navigator.rightTree else {
          navigator.rightTree = BinarySearchTree2<T>(value: newValue)
          break
        }
        navigator = subtree
        continue
      }
      break
    }
    return self
  }
} /* cut remove/search methods */
Run Code Online (Sandbox Code Playgroud)

编辑:

我还做了一个更优化的平衡树测试,其中创建了一个包含 1001 个连续元素的数据集,删除了中间元素,使用 Fisher-Yates 洗牌来随机化顺序,使用中间元素初始化根,然后运行两个集合。这是我的结果:

您的:创建阶段微秒:[27648219]

我的:创建阶段微秒:[8332361]

编辑2:

我将add()方法切换为使用递归,从而显着提高了速度:

之前(我的原始代码):创建阶段微秒:[8088804]

之后:创建阶段微秒:[1179398]

这是新代码:

public class BinarySearchTree3<T : Comparable> {
  public init(value : T) {
    self.value = value
  }

  let value : T
  var leftTree : BinarySearchTree3<T>?
  var rightTree : BinarySearchTree3<T>?

  public func add(newValue : T) {
    if (newValue < self.value) {
      if self.leftTree?.add(newValue) == nil {
        self.leftTree = BinarySearchTree3<T>(value: newValue)
      }
      return
    }
    if (newValue > self.value) {
      if self.rightTree?.add(newValue) == nil {
        self.rightTree = BinarySearchTree3<T>(value: newValue)
      }
      return
    }
  }
} /* cut remove/search methods */
Run Code Online (Sandbox Code Playgroud)