From 0fc5d6ad31871ea1a2b4122e9ae5f21f646defbc Mon Sep 17 00:00:00 2001 From: Scott Perry Date: Wed, 26 Jun 2019 14:35:48 -0700 Subject: [PATCH 1/3] Performance improvements and availability updates for Collection.difference(from:using:) --- benchmark/CMakeLists.txt | 2 + benchmark/single-source/Diffing.swift | 125 +++ benchmark/single-source/Myers.swift | 196 +++++ benchmark/utils/main.swift | 4 + .../NSOrderedCollectionDifference.swift | 6 +- stdlib/public/core/CollectionDifference.swift | 24 +- stdlib/public/core/Diffing.swift | 790 ++++-------------- test/stdlib/Diffing.swift | 3 +- 8 files changed, 510 insertions(+), 640 deletions(-) create mode 100644 benchmark/single-source/Diffing.swift create mode 100644 benchmark/single-source/Myers.swift diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index 3b14a70793559..708b3109068f6 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -78,6 +78,7 @@ set(SWIFT_BENCH_MODULES single-source/DictionaryRemove single-source/DictionarySubscriptDefault single-source/DictionarySwap + single-source/Diffing single-source/DropFirst single-source/DropLast single-source/DropWhile @@ -104,6 +105,7 @@ set(SWIFT_BENCH_MODULES single-source/Memset single-source/MonteCarloE single-source/MonteCarloPi + single-source/Myers single-source/NSDictionaryCastToSwift single-source/NSError single-source/NSStringConversion diff --git a/benchmark/single-source/Diffing.swift b/benchmark/single-source/Diffing.swift new file mode 100644 index 0000000000000..36c9f5ea3641a --- /dev/null +++ b/benchmark/single-source/Diffing.swift @@ -0,0 +1,125 @@ +//===--- Diffing.swift ----------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import TestsUtils + +let t: [BenchmarkCategory] = [.api] +public let Diffing = [ + BenchmarkInfo( + name: "DiffSame", + runFunction: run_DiffSame, + tags: t, + legacyFactor: 10), + BenchmarkInfo( + name: "DiffPangramToAlphabet", + runFunction: run_DiffPangramToAlphabet, + tags: t, + legacyFactor: 10), + BenchmarkInfo( + name: "DiffPangrams", + runFunction: run_DiffPangrams, + tags: t, + legacyFactor: 10), + BenchmarkInfo( + name: "DiffReversedAlphabets", + runFunction: run_DiffReversedAlphabets, + tags: t, + legacyFactor: 10), + BenchmarkInfo( + name: "DiffReversedLorem", + runFunction: run_DiffReversedLorem, + tags: t, + legacyFactor: 10), + BenchmarkInfo( + name: "DiffDisparate", + runFunction: run_DiffDisparate, + tags: t, + legacyFactor: 10), + BenchmarkInfo( + name: "DiffSimilar", + runFunction: run_DiffSimilar, + tags: t, + legacyFactor: 10), +] + +let numbersAndSymbols = Array("0123456789`~!@#$%^&*()+=_-\"'?/<,>.\\{}'") +let alphabets = Array("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +let alphabetsReversed = Array("ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba") +let longPangram = Array("This pangram contains four As, one B, two Cs, one D, thirty Es, six Fs, five Gs, seven Hs, eleven Is, one J, one K, two Ls, two Ms, eighteen Ns, fifteen Os, two Ps, one Q, five Rs, twenty-seven Ss, eighteen Ts, two Us, seven Vs, eight Ws, two Xs, three Ys, & one Z") +let typingPangram = Array("The quick brown fox jumps over the lazy dog") +let loremIpsum = Array("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.") +let unabridgedLorem = Array("Lorem ipsum, quia dolor sit amet consectetur adipisci[ng] velit, sed quia non-numquam [do] eius modi tempora inci[di]dunt, ut labore et dolore magnam aliqua.") +let loremReverse = Array(".auqila angam erolod te erobal tu tnudidicni ropmet domsuie od des ,tile gnicsipida rutetcesnoc ,tema tis rolod muspi meroL") + + +@inline(never) +public func run_DiffSame(_ N: Int) { + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + for _ in 1...N { + let _ = longPangram.difference(from: longPangram) + } + } +} + +@inline(never) +public func run_DiffPangramToAlphabet(_ N: Int) { + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + for _ in 1...N { + let _ = longPangram.difference(from: alphabets) + } + } +} + +@inline(never) +public func run_DiffPangrams(_ N: Int) { + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + for _ in 1...N { + let _ = longPangram.difference(from: typingPangram) + } + } +} + +@inline(never) +public func run_DiffReversedAlphabets(_ N: Int) { + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + for _ in 1...N { + let _ = alphabets.difference(from: alphabetsReversed) + } + } +} + +@inline(never) +public func run_DiffReversedLorem(_ N: Int) { + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + for _ in 1...N { + let _ = loremIpsum.difference(from: loremReverse) + } + } +} + +@inline(never) +public func run_DiffDisparate(_ N: Int) { + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + for _ in 1...N { + let _ = alphabets.difference(from: numbersAndSymbols) + } + } +} + +@inline(never) +public func run_DiffSimilar(_ N: Int) { + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + for _ in 1...N { + let _ = loremIpsum.difference(from: unabridgedLorem) + } + } +} diff --git a/benchmark/single-source/Myers.swift b/benchmark/single-source/Myers.swift new file mode 100644 index 0000000000000..b96d47a4d32f9 --- /dev/null +++ b/benchmark/single-source/Myers.swift @@ -0,0 +1,196 @@ +//===--- Myers.swift -------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import TestsUtils + +public let Myers = [ + BenchmarkInfo(name: "Myers", runFunction: run_Myers, tags: [.algorithm]), +] + +let loremShort = Array("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.") +let loremLong = Array("Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam eaque ipsa, quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt, explicabo. Nemo enim ipsam voluptatem, quia voluptas sit, aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos, qui ratione voluptatem sequi nesciunt, neque porro quisquam est, qui dolorem ipsum, quia dolor sit amet consectetur adipisci[ng] velit, sed quia non-numquam [do] eius modi tempora inci[di]dunt, ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum[d] exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit, qui in ea voluptate velit esse, quam nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla pariatur?") + +@inline(never) +public func run_Myers(N: Int) { + if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { + for _ in 1...N { + let _ = myers(from: loremShort, to: loremLong, using: ==) + } + } +} + +// _V is a rudimentary type made to represent the rows of the triangular matrix type used by the Myer's algorithm +// +// This type is basically an array that only supports indexes in the set `stride(from: -d, through: d, by: 2)` where `d` is the depth of this row in the matrix +// `d` is always known at allocation-time, and is used to preallocate the structure. +fileprivate struct _V { + + private var a: [Int] + + // The way negative indexes are implemented is by interleaving them in the empty slots between the valid positive indexes + @inline(__always) private static func transform(_ index: Int) -> Int { + // -3, -1, 1, 3 -> 3, 1, 0, 2 -> 0...3 + // -2, 0, 2 -> 2, 0, 1 -> 0...2 + return (index <= 0 ? -index : index &- 1) + } + + init(maxIndex largest: Int) { + a = [Int](repeating: 0, count: largest + 1) + } + + subscript(index: Int) -> Int { + get { + return a[_V.transform(index)] + } + set(newValue) { + a[_V.transform(index)] = newValue + } + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +fileprivate func myers( + from old: C, to new: D, + using cmp: (C.Element, D.Element) -> Bool +) -> CollectionDifference + where + C : BidirectionalCollection, + D : BidirectionalCollection, + C.Element == D.Element +{ + + // Core implementation of the algorithm described at http://www.xmailserver.org/diff2.pdf + // Variable names match those used in the paper as closely as possible + func _descent(from a: UnsafeBufferPointer, to b: UnsafeBufferPointer) -> [_V] { + let n = a.count + let m = b.count + let max = n + m + + var result = [_V]() + var v = _V(maxIndex: 1) + v[1] = 0 + + var x = 0 + var y = 0 + iterator: for d in 0...max { + let prev_v = v + result.append(v) + v = _V(maxIndex: d) + + // The code in this loop is _very_ hot—the loop bounds increases in terms + // of the iterator of the outer loop! + for k in stride(from: -d, through: d, by: 2) { + if k == -d { + x = prev_v[k &+ 1] + } else { + let km = prev_v[k &- 1] + + if k != d { + let kp = prev_v[k &+ 1] + if km < kp { + x = kp + } else { + x = km &+ 1 + } + } else { + x = km &+ 1 + } + } + y = x &- k + + while x < n && y < m { + if !cmp(a[x], b[y]) { + break; + } + x &+= 1 + y &+= 1 + } + + v[k] = x + + if x >= n && y >= m { + break iterator + } + } + if x >= n && y >= m { + break + } + } + + return result + } + + // Backtrack through the trace generated by the Myers descent to produce the changes that make up the diff + func _formChanges( + from a: UnsafeBufferPointer, + to b: UnsafeBufferPointer, + using trace: [_V] + ) -> [CollectionDifference.Change] { + var changes = [CollectionDifference.Change]() + + var x = a.count + var y = b.count + for d in stride(from: trace.count &- 1, to: 0, by: -1) { + let v = trace[d] + let k = x &- y + let prev_k = (k == -d || (k != d && v[k &- 1] < v[k &+ 1])) ? k &+ 1 : k &- 1 + let prev_x = v[prev_k] + let prev_y = prev_x &- prev_k + + while x > prev_x && y > prev_y { + // No change at this position. + x &-= 1 + y &-= 1 + } + + assert((x == prev_x && y > prev_y) || (y == prev_y && x > prev_x)) + if y != prev_y { + changes.append(.insert(offset: prev_y, element: b[prev_y], associatedWith: nil)) + } else { + changes.append(.remove(offset: prev_x, element: a[prev_x], associatedWith: nil)) + } + + x = prev_x + y = prev_y + } + + return changes + } + + /* Splatting the collections into contiguous storage has two advantages: + * + * 1) Subscript access is much faster + * 2) Subscript index becomes Int, matching the iterator types in the algorithm + * + * Combined, these effects dramatically improves performance when + * collections differ significantly, without unduly degrading runtime when + * the parameters are very similar. + * + * In terms of memory use, the linear cost of creating a ContiguousArray (when + * necessary) is significantly less than the worst-case n² memory use of the + * descent algorithm. + */ + func _withContiguousStorage( + for values: C, + _ body: (UnsafeBufferPointer) throws -> R + ) rethrows -> R { + if let result = try values.withContiguousStorageIfAvailable(body) { return result } + let array = ContiguousArray(values) + return try array.withUnsafeBufferPointer(body) + } + + return _withContiguousStorage(for: old) { a in + return _withContiguousStorage(for: new) { b in + return CollectionDifference(_formChanges(from: a, to: b, using:_descent(from: a, to: b)))! + } + } +} \ No newline at end of file diff --git a/benchmark/utils/main.swift b/benchmark/utils/main.swift index 9c1bf3eb3b13b..92dfbc1cdb70f 100644 --- a/benchmark/utils/main.swift +++ b/benchmark/utils/main.swift @@ -66,6 +66,7 @@ import DictionaryOfAnyHashableStrings import DictionaryRemove import DictionarySubscriptDefault import DictionarySwap +import Diffing import DropFirst import DropLast import DropWhile @@ -92,6 +93,7 @@ import MapReduce import Memset import MonteCarloE import MonteCarloPi +import Myers import NibbleSort import NIOChannelPipeline import NSDictionaryCastToSwift @@ -240,6 +242,7 @@ registerBenchmark(DictionaryOfAnyHashableStrings) registerBenchmark(DictionaryRemove) registerBenchmark(DictionarySubscriptDefault) registerBenchmark(DictionarySwap) +registerBenchmark(Diffing) registerBenchmark(DropFirst) registerBenchmark(DropLast) registerBenchmark(DropWhile) @@ -267,6 +270,7 @@ registerBenchmark(MapReduce) registerBenchmark(Memset) registerBenchmark(MonteCarloE) registerBenchmark(MonteCarloPi) +registerBenchmark(Myers) registerBenchmark(NSDictionaryCastToSwift) registerBenchmark(NSErrorTest) #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) diff --git a/stdlib/public/Darwin/Foundation/NSOrderedCollectionDifference.swift b/stdlib/public/Darwin/Foundation/NSOrderedCollectionDifference.swift index 8254a2207d6c8..83134e63d5714 100644 --- a/stdlib/public/Darwin/Foundation/NSOrderedCollectionDifference.swift +++ b/stdlib/public/Darwin/Foundation/NSOrderedCollectionDifference.swift @@ -13,7 +13,7 @@ @_exported import Foundation // Clang module // CollectionDifference.Change is conditionally bridged to NSOrderedCollectionChange -@available(iOS 9999, macOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference.Change : _ObjectiveCBridgeable { @_semantics("convertToObjectiveC") public func _bridgeToObjectiveC() -> NSOrderedCollectionChange { @@ -66,7 +66,7 @@ extension CollectionDifference.Change : _ObjectiveCBridgeable { } // CollectionDifference is conditionally bridged to NSOrderedCollectionDifference -@available(iOS 9999, macOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference : _ObjectiveCBridgeable { @_semantics("convertToObjectiveC") public func _bridgeToObjectiveC() -> NSOrderedCollectionDifference { @@ -101,6 +101,6 @@ extension CollectionDifference : _ObjectiveCBridgeable { @_effects(readonly) public static func _unconditionallyBridgeFromObjectiveC(_ s: NSOrderedCollectionDifference?) -> CollectionDifference { - return _formDifference(from: s!) { $0 as! Change }! + return _formDifference(from: s!) { ($0 as! Change) }! } } diff --git a/stdlib/public/core/CollectionDifference.swift b/stdlib/public/core/CollectionDifference.swift index 6be1e24022a33..f209fa02be903 100644 --- a/stdlib/public/core/CollectionDifference.swift +++ b/stdlib/public/core/CollectionDifference.swift @@ -12,7 +12,7 @@ /// A collection of insertions and removals that describe the difference /// between two ordered collection states. -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public struct CollectionDifference { /// A single change to a collection. @frozen @@ -222,7 +222,7 @@ public struct CollectionDifference { /// } /// } /// ``` -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference: Collection { public typealias Element = Change @@ -270,7 +270,7 @@ extension CollectionDifference: Collection { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference.Index: Equatable { @inlinable public static func == ( @@ -281,7 +281,7 @@ extension CollectionDifference.Index: Equatable { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference.Index: Comparable { @inlinable public static func < ( @@ -292,7 +292,7 @@ extension CollectionDifference.Index: Comparable { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference.Index: Hashable { @inlinable public func hash(into hasher: inout Hasher) { @@ -300,19 +300,19 @@ extension CollectionDifference.Index: Hashable { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference.Change: Equatable where ChangeElement: Equatable {} -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference: Equatable where ChangeElement: Equatable {} -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference.Change: Hashable where ChangeElement: Hashable {} -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference: Hashable where ChangeElement: Hashable {} -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference where ChangeElement: Hashable { /// Returns a new collection difference with associations between individual /// elements that have been removed and inserted only once. @@ -369,7 +369,7 @@ extension CollectionDifference where ChangeElement: Hashable { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference.Change: Codable where ChangeElement: Codable { private enum _CodingKeys: String, CodingKey { case offset @@ -406,5 +406,5 @@ extension CollectionDifference.Change: Codable where ChangeElement: Codable { } } -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference: Codable where ChangeElement: Codable {} diff --git a/stdlib/public/core/Diffing.swift b/stdlib/public/core/Diffing.swift index 0ef9c34f2f777..7c865e6bff74b 100644 --- a/stdlib/public/core/Diffing.swift +++ b/stdlib/public/core/Diffing.swift @@ -12,7 +12,7 @@ // MARK: Diff application to RangeReplaceableCollection -@available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) extension CollectionDifference { fileprivate func _fastEnumeratedApply( _ consume: (Change) -> Void @@ -64,7 +64,7 @@ extension RangeReplaceableCollection { /// /// - Complexity: O(*n* + *c*), where *n* is `self.count` and *c* is the /// number of changes contained by the parameter. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public func applying(_ difference: CollectionDifference) -> Self? { var result = Self() var enumeratedRemoves = 0 @@ -112,8 +112,8 @@ extension RangeReplaceableCollection { // MARK: Definition of API extension BidirectionalCollection { - /// Returns the difference needed to produce this collection's ordered - /// elements from the given collection, using the given predicate as an + /// Returns the difference needed to produce this collection's ordered + /// elements from the given collection, using the given predicate as an /// equivalence test. /// /// This function does not infer element moves. If you need to infer moves, @@ -121,54 +121,27 @@ extension BidirectionalCollection { /// /// - Parameters: /// - other: The base state. - /// - areEquivalent: A closure that returns a Boolean value indicating + /// - areEquivalent: A closure that returns a Boolean value indicating /// whether two elements are equivalent. /// /// - Returns: The difference needed to produce the reciever's state from /// the parameter's state. /// - /// - Complexity: Worst case performance is O(*n* * *m*), where *n* is the - /// count of this collection and *m* is `other.count`. You can expect + /// - Complexity: Worst case performance is O(*n* * *m*), where *n* is the + /// count of this collection and *m* is `other.count`. You can expect /// faster execution when the collections share many common elements. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public func difference( from other: C, - by areEquivalent: (Element, C.Element) -> Bool + by areEquivalent: (C.Element, Element) -> Bool ) -> CollectionDifference where C.Element == Self.Element { - var rawChanges: [CollectionDifference.Change] = [] - - let source = _CountingIndexCollection(other) - let target = _CountingIndexCollection(self) - let result = _CollectionChanges(from: source, to: target, by: areEquivalent) - for change in result { - switch change { - case let .removed(r): - for i in source.indices[r] { - rawChanges.append( - .remove( - offset: i.offset!, - element: source[i], - associatedWith: nil)) - } - case let .inserted(r): - for i in target.indices[r] { - rawChanges.append( - .insert( - offset: i.offset!, - element: target[i], - associatedWith: nil)) - } - case .matched: break - } - } - - return CollectionDifference(_validatedChanges: rawChanges) + return _myers(from: other, to: self, using: areEquivalent) } } extension BidirectionalCollection where Element : Equatable { - /// Returns the difference needed to produce this collection's ordered + /// Returns the difference needed to produce this collection's ordered /// elements from the given collection. /// /// This function does not infer element moves. If you need to infer moves, @@ -177,14 +150,14 @@ extension BidirectionalCollection where Element : Equatable { /// - Parameters: /// - other: The base state. /// - /// - Returns: The difference needed to produce this collection's ordered + /// - Returns: The difference needed to produce this collection's ordered /// elements from the given collection. /// - /// - Complexity: Worst case performance is O(*n* * *m*), where *n* is the - /// count of this collection and *m* is `other.count`. You can expect - /// faster execution when the collections share many common elements, or + /// - Complexity: Worst case performance is O(*n* * *m*), where *n* is the + /// count of this collection and *m* is `other.count`. You can expect + /// faster execution when the collections share many common elements, or /// if `Element` conforms to `Hashable`. - @available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, *) // FIXME(availability-5.1) + @available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) public func difference( from other: C ) -> CollectionDifference where C.Element == Self.Element { @@ -192,615 +165,186 @@ extension BidirectionalCollection where Element : Equatable { } } -extension BidirectionalCollection { - /// Returns a pair of subsequences containing the initial elements that - /// `self` and `other` have in common. - fileprivate func _commonPrefix( - with other: Other, - by areEquivalent: (Element, Other.Element) -> Bool - ) -> (SubSequence, Other.SubSequence) - where Element == Other.Element { - let (s1, s2) = (startIndex, other.startIndex) - let (e1, e2) = (endIndex, other.endIndex) - var (i1, i2) = (s1, s2) - while i1 != e1 && i2 != e2 { - if !areEquivalent(self[i1], other[i2]) { break } - formIndex(after: &i1) - other.formIndex(after: &i2) - } - return (self[s1.. -where SourceIndex: Comparable, TargetIndex: Comparable { - fileprivate typealias Endpoint = (x: SourceIndex, y: TargetIndex) - - /// An encoding of change elements as an array of index pairs stored in - /// `pathStorage[pathStartIndex...]`. - /// - /// This encoding allows the same storage to be used to run the difference - /// algorithm, report the result, and repeat in place using - /// `formChanges`. - /// - /// The collection of changes between XABCD and XYCD is: - /// - /// [.match(0..<1, 0..<1), .remove(1..<3), .insert(1..<2), - /// .match(3..<5, 2..<4)] - /// - /// Which gets encoded as: - /// - /// [(0, 0), (1, 1), (3, 1), (3, 2), (5, 4)] - /// - /// You can visualize it as a two-dimensional path composed of remove - /// (horizontal), insert (vertical), and match (diagonal) segments: - /// - /// X A B C D - /// X \ _ _ - /// Y | - /// C \ - /// D \ - /// - private var pathStorage: [Endpoint] - - /// The index in `pathStorage` of the first segment in the difference path. - private var pathStartIndex: Int - - /// Creates a collection of changes from a difference path. - fileprivate init( - pathStorage: [Endpoint], pathStartIndex: Int - ) { - self.pathStorage = pathStorage - self.pathStartIndex = pathStartIndex - } - - /// Creates an empty collection of changes, i.e. the changes between two - /// empty collections. - private init() { - self.pathStorage = [] - self.pathStartIndex = 0 - } -} - -extension _CollectionChanges { - /// A range of elements removed from the source, inserted in the target, or - /// that the source and target have in common. - fileprivate enum Element { - case removed(Range) - case inserted(Range) - case matched(Range, Range) - } -} +// MARK: Internal implementation -extension _CollectionChanges: RandomAccessCollection { - fileprivate typealias Index = Int +// _V is a rudimentary type made to represent the rows of the triangular matrix type used by the Myer's algorithm +// +// This type is basically an array that only supports indexes in the set `stride(from: -d, through: d, by: 2)` where `d` is the depth of this row in the matrix +// `d` is always known at allocation-time, and is used to preallocate the structure. +fileprivate struct _V { - fileprivate var startIndex: Index { - return 0 - } + private var a: [Int] +#if INTERNAL_CHECKS_ENABLED + private let isOdd: Bool +#endif - fileprivate var endIndex: Index { - return Swift.max(0, pathStorage.endIndex - pathStartIndex - 1) + // The way negative indexes are implemented is by interleaving them in the empty slots between the valid positive indexes + @inline(__always) private static func transform(_ index: Int) -> Int { + // -3, -1, 1, 3 -> 3, 1, 0, 2 -> 0...3 + // -2, 0, 2 -> 2, 0, 1 -> 0...2 + return (index <= 0 ? -index : index &- 1) } - fileprivate func index(after i: Index) -> Index { - return i + 1 + init(maxIndex largest: Int) { +#if INTERNAL_CHECKS_ENABLED + _internalInvariant(largest >= 0) + isOdd = largest % 2 == 1 +#endif + a = [Int](repeating: 0, count: largest + 1) } - fileprivate subscript(position: Index) -> Element { - _internalInvariant((startIndex.. Int { + get { +#if INTERNAL_CHECKS_ENABLED + _internalInvariant(isOdd == (index % 2 != 0)) +#endif + return a[_V.transform(index)] } - } -} - -extension _CollectionChanges: CustomStringConvertible { - fileprivate var description: String { - return _makeCollectionDescription() - } -} - -extension _CollectionChanges { - /// Creates the collection of changes between `source` and `target`. - /// - /// - Runtime: O(*n* * *d*), where *n* is `source.count + target.count` and - /// *d* is the minimal number of inserted and removed elements. - /// - Space: O(*d* * *d*), where *d* is the minimal number of inserted and - /// removed elements. - fileprivate - init( - from source: Source, - to target: Target, - by areEquivalent: (Source.Element, Target.Element) -> Bool - ) where - Source.Element == Target.Element, - Source.Index == SourceIndex, - Target.Index == TargetIndex - { - self.init() - formChanges(from: source, to: target, by: areEquivalent) - } - - /// Replaces `self` with the collection of changes between `source` - /// and `target`. - /// - /// - Runtime: O(*n* * *d*), where *n* is `source.count + target.count` and - /// *d* is the minimal number of inserted and removed elements. - /// - Space: O(*d*²), where *d* is the minimal number of inserted and - /// removed elements. - private mutating func formChanges< - Source: BidirectionalCollection, - Target: BidirectionalCollection - >( - from source: Source, - to target: Target, - by areEquivalent: (Source.Element, Target.Element) -> Bool - ) where - Source.Element == Target.Element, - Source.Index == SourceIndex, - Target.Index == TargetIndex - { - let pathStart = (x: source.startIndex, y: target.startIndex) - let pathEnd = (x: source.endIndex, y: target.endIndex) - let matches = source._commonPrefix(with: target, by: areEquivalent) - let (x, y) = (matches.0.endIndex, matches.1.endIndex) - - if pathStart == pathEnd { - pathStorage.removeAll(keepingCapacity: true) - pathStartIndex = 0 - } else if x == pathEnd.x || y == pathEnd.y { - pathStorage.removeAll(keepingCapacity: true) - pathStorage.append(pathStart) - if pathStart != (x, y) && pathEnd != (x, y) { - pathStorage.append((x, y)) - } - pathStorage.append(pathEnd) - pathStartIndex = 0 - } else { - formChangesCore(from: source, to: target, x: x, y: y, by: areEquivalent) + set(newValue) { +#if INTERNAL_CHECKS_ENABLED + _internalInvariant(isOdd == (index % 2 != 0)) +#endif + a[_V.transform(index)] = newValue } } +} - /// The core difference algorithm. - /// - /// - Precondition: There is at least one difference between `a` and `b` - /// - Runtime: O(*n* * *d*), where *n* is `a.count + b.count` and - /// *d* is the number of inserts and removes. - /// - Space: O(*d* * *d*), where *d* is the number of inserts and removes. - private mutating func formChangesCore< - Source: BidirectionalCollection, - Target: BidirectionalCollection - >( - from a: Source, - to b: Target, - x: Source.Index, - y: Target.Index, - by areEquivalent: (Source.Element, Target.Element) -> Bool - ) where - Source.Element == Target.Element, - Source.Index == SourceIndex, - Target.Index == TargetIndex - { - // Written to correspond, as closely as possible, to the psuedocode in - // Myers, E. "An O(ND) Difference Algorithm and Its Variations". - // - // See "FIGURE 2: The Greedy LCS/SES Algorithm" on p. 6 of the [paper]. - // - // Note the following differences from the psuedocode in FIGURE 2: - // - // 1. FIGURE 2 relies on both *A* and *B* being Arrays. In a generic - // context, it isn't true that *y = x - k*, as *x*, *y*, *k* could - // all be different types, so we store both *x* and *y* in *V*. - // 2. FIGURE 2 only reports the length of the LCS/SES. Reporting a - // solution path requires storing a copy of *V* (the search frontier) - // after each iteration of the outer loop. - // 3. FIGURE 2 stops the search after *MAX* iterations. We run the loop - // until a solution is found. We also guard against incrementing past - // the end of *A* and *B*, both to satisfy the termination condition - // and because that would violate preconditions on collection. - // - // [paper]: http://www.xmailserver.org/diff2.pdf - var (x, y) = (x, y) - let (n, m) = (a.endIndex, b.endIndex) - - var v = _SearchState(consuming: &pathStorage) - - v.appendFrontier(repeating: (x, y)) - var d = 1 - var delta = 0 - outer: while true { - v.appendFrontier(repeating: (n, m)) +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +fileprivate func _myers( + from old: C, to new: D, + using cmp: (C.Element, D.Element) -> Bool +) -> CollectionDifference + where + C : BidirectionalCollection, + D : BidirectionalCollection, + C.Element == D.Element +{ + + // Core implementation of the algorithm described at http://www.xmailserver.org/diff2.pdf + // Variable names match those used in the paper as closely as possible + func _descent(from a: UnsafeBufferPointer, to b: UnsafeBufferPointer) -> [_V] { + let n = a.count + let m = b.count + let max = n + m + + var result = [_V]() + var v = _V(maxIndex: 1) + v[1] = 0 + + var x = 0 + var y = 0 + iterator: for d in 0...max { + let prev_v = v + result.append(v) + v = _V(maxIndex: d) + + // The code in this loop is _very_ hot—the loop bounds increases in terms + // of the iterator of the outer loop! for k in stride(from: -d, through: d, by: 2) { - if k == -d || (k != d && v[d - 1, k - 1].x < v[d - 1, k + 1].x) { - (x, y) = v[d - 1, k + 1] - if y != m { b.formIndex(after: &y) } + if k == -d { + x = prev_v[k &+ 1] } else { - (x, y) = v[d - 1, k - 1] - if x != n { a.formIndex(after: &x) } + let km = prev_v[k &- 1] + + if k != d { + let kp = prev_v[k &+ 1] + if km < kp { + x = kp + } else { + x = km &+ 1 + } + } else { + x = km &+ 1 + } + } + y = x &- k + + while x < n && y < m { + if !cmp(a[x], b[y]) { + break; + } + x &+= 1 + y &+= 1 } - let matches = a[x..= n && y >= m { + break iterator } } - d += 1 + if x >= n && y >= m { + break + } } - self = v.removeCollectionChanges(a: a, b: b, d: d, delta: delta) - } -} - -/// The search paths being explored. -fileprivate struct _SearchState< - SourceIndex: Comparable, - TargetIndex: Comparable -> { - fileprivate typealias Endpoint = (x: SourceIndex, y: TargetIndex) - - /// The search frontier for each iteration. - /// - /// The nth iteration of the core algorithm requires storing n + 1 search - /// path endpoints. Thus, the shape of the storage required is a triangle. - private var endpoints = _LowerTriangularMatrix() - - /// Creates an instance, taking the capacity of `storage` for itself. - /// - /// - Postcondition: `storage` is empty. - fileprivate init(consuming storage: inout [Endpoint]) { - storage.removeAll(keepingCapacity: true) - swap(&storage, &endpoints.storage) - } - - /// Returns the endpoint of the search frontier for iteration `d` on - /// diagonal `k`. - fileprivate subscript(d: Int, k: Int) -> Endpoint { - get { - _internalInvariant((-d...d).contains(k)) - _internalInvariant((d + k) % 2 == 0) - return endpoints[d, (d + k) / 2] - } - set { - _internalInvariant((-d...d).contains(k)) - _internalInvariant((d + k) % 2 == 0) - endpoints[d, (d + k) / 2] = newValue - } - } + _internalInvariant(x >= n && y >= m) - /// Adds endpoints initialized to `repeatedValue` for the search frontier of - /// the next iteration. - fileprivate mutating func appendFrontier(repeating repeatedValue: Endpoint) { - endpoints.appendRow(repeating: repeatedValue) + return result } -} -extension _SearchState { - /// Removes and returns `_CollectionChanges`, leaving `_SearchState` empty. - /// - /// - Precondition: There is at least one difference between `a` and `b` - fileprivate mutating func removeCollectionChanges< - Source: BidirectionalCollection, - Target: BidirectionalCollection - >( - a: Source, - b: Target, - d: Int, - delta: Int - ) -> _CollectionChanges - where Source.Index == SourceIndex, Target.Index == TargetIndex - { - // Calculating the difference path is very similar to running the core - // algorithm in reverse: - // - // var k = delta - // for d in (1...d).reversed() { - // if k == -d || (k != d && self[d - 1, k - 1].x < self[d - 1, k + 1].x) { - // // insert of self[d - 1, k + 1].y - // k += 1 - // } else { - // // remove of self[d - 1, k - 1].x - // k -= 1 - // } - // } - // - // It is more complicated below because: - // - // 1. We want to include segments for matches - // 2. We want to coallesce consecutive like segments - // 3. We don't want to allocate, so we're overwriting the elements of - // endpoints.storage we've finished reading. - - let pathStart = (a.startIndex, b.startIndex) - let pathEnd = (a.endIndex, b.endIndex) - - // `endpoints.storage` may need space for an additional element in order - // to store the difference path when `d == 1`. - // - // `endpoints.storage` has `(d + 1) * (d + 2) / 2` elements stored, - // but a difference path requires up to `2 + d * 2` elements[^1]. - // - // If `d == 1`: - // - // (1 + 1) * (1 + 2) / 2 < 2 + 1 * 2 - // 3 < 4 - // - // `d == 1` is the only special case because: - // - // - It's a precondition that `d > 0`. - // - Once `d >= 2` `endpoints.storage` will have sufficient space: - // - // (d + 1) * (d + 2) / 2 = 2 + d * 2 - // d * d - d - 2 = 0 - // (d - 2) * (d + 1) = 0 - // d = 2; d = -1 - // - // [1]: An endpoint for every remove, insert, and match segment. (Recall - // *d* is the minimal number of inserted and removed elements). If there - // are no consecutive removes or inserts and every remove or insert is - // sandwiched between matches, the path will need `2 + d * 2` elements. - _internalInvariant(d > 0, "Must be at least one difference between `a` and `b`") - if d == 1 { - endpoints.storage.append(pathEnd) - } - - var i = endpoints.storage.endIndex - 1 - // `isInsertion` tracks whether the element at `endpoints.storage[i]` - // is an insertion (`true`), a removal (`false`), or a match (`nil`). - var isInsertion: Bool? = nil - var k = delta - endpoints.storage[i] = pathEnd - for d in (1...d).reversed() { - if k == -d || (k != d && self[d - 1, k - 1].x < self[d - 1, k + 1].x) { - let (x, y) = self[d - 1, k + 1] - - // There was match before this insert, so add a segment. - if x != endpoints.storage[i].x { - i -= 1; endpoints.storage[i] = (x, b.index(after: y)) - isInsertion = nil - } - - // If the previous segment is also an insert, overwrite it. - if isInsertion != .some(true) { i -= 1 } - endpoints.storage[i] = (x, y) + // Backtrack through the trace generated by the Myers descent to produce the changes that make up the diff + func _formChanges( + from a: UnsafeBufferPointer, + to b: UnsafeBufferPointer, + using trace: [_V] + ) -> [CollectionDifference.Change] { + var changes = [CollectionDifference.Change]() + + var x = a.count + var y = b.count + for d in stride(from: trace.count &- 1, to: 0, by: -1) { + let v = trace[d] + let k = x &- y + let prev_k = (k == -d || (k != d && v[k &- 1] < v[k &+ 1])) ? k &+ 1 : k &- 1 + let prev_x = v[prev_k] + let prev_y = prev_x &- prev_k + + while x > prev_x && y > prev_y { + // No change at this position. + x &-= 1 + y &-= 1 + } - isInsertion = true - k += 1 + assert((x == prev_x && y > prev_y) || (y == prev_y && x > prev_x)) + if y != prev_y { + changes.append(.insert(offset: prev_y, element: b[prev_y], associatedWith: nil)) } else { - let (x, y) = self[d - 1, k - 1] - - // There was a match before this remove, so add a segment. - if y != endpoints.storage[i].y { - i -= 1; endpoints.storage[i] = (a.index(after: x), y) - isInsertion = nil - } - - // If the previous segment is also a remove, overwrite it. - if isInsertion != .some(false) { i -= 1 } - endpoints.storage[i] = (x, y) - - isInsertion = false - k -= 1 + changes.append(.remove(offset: prev_x, element: a[prev_x], associatedWith: nil)) } - } - - if pathStart != endpoints.storage[i] { - i -= 1; endpoints.storage[i] = pathStart - } - - let pathStorage = endpoints.storage - endpoints.storage = [] - return _CollectionChanges(pathStorage: pathStorage, pathStartIndex: i) - } -} - -/// An index that counts its offset from the start of its collection. -private struct _CountingIndex: Equatable { - /// The position in the underlying collection. - let base: Base - /// The offset from the start index of the collection or `nil` if `self` is - /// the end index. - let offset: Int? -} - -extension _CountingIndex: Comparable { - fileprivate static func <(lhs: _CountingIndex, rhs: _CountingIndex) -> Bool { - return (lhs.base, lhs.offset ?? Int.max) < (rhs.base, rhs.offset ?? Int.max) - } -} - -/// A collection that counts the offset of its indices from its start index. -/// -/// You can use `_CountingIndexCollection` with algorithms on `Collection` to -/// calculate offsets of significance: -/// -/// if let i = _CountingIndexCollection("Café").index(of: "f") { -/// print(i.offset) -/// } -/// // Prints "2" -/// -/// - Note: The offset of `endIndex` is `nil` -private struct _CountingIndexCollection { - private let base: Base - - fileprivate init(_ base: Base) { - self.base = base - } -} - -extension _CountingIndexCollection : BidirectionalCollection { - fileprivate typealias Index = _CountingIndex - fileprivate typealias Element = Base.Element - - fileprivate var startIndex: Index { - return Index(base: base.startIndex, offset: base.isEmpty ? nil : 0) - } - - fileprivate var endIndex: Index { - return Index(base: base.endIndex, offset: nil) - } - - fileprivate func index(after i: Index) -> Index { - let next = base.index(after: i.base) - return Index( - base: next, offset: next == base.endIndex ? nil : i.offset! + 1) - } - - fileprivate func index(before i: Index) -> Index { - let prev = base.index(before: i.base) - return Index( - base: prev, offset: prev == base.endIndex ? nil : i.offset! + 1) - } - - fileprivate subscript(position: Index) -> Element { - return base[position.base] - } -} - -/// Returns the nth [triangular number]. -/// -/// [triangular number]: https://en.wikipedia.org/wiki/Triangular_number -fileprivate func _triangularNumber(_ n: Int) -> Int { - return n * (n + 1) / 2 -} - -/// A square matrix that only provides subscript access to elements on, or -/// below, the main diagonal. -/// -/// A [lower triangular matrix] can be dynamically grown: -/// -/// var m = _LowerTriangularMatrix() -/// m.appendRow(repeating: 1) -/// m.appendRow(repeating: 2) -/// m.appendRow(repeating: 3) -/// -/// assert(Array(m.rowMajorOrder) == [ -/// 1, -/// 2, 2, -/// 3, 3, 3, -/// ]) -/// -/// [lower triangular matrix]: http://en.wikipedia.org/wiki/Triangular_matrix -fileprivate struct _LowerTriangularMatrix { - /// The matrix elements stored in [row major order][rmo]. - /// - /// [rmo]: http://en.wikipedia.org/wiki/Row-_and_column-major_order - fileprivate var storage: [Element] = [] - - /// The dimension of the matrix. - /// - /// Being a square matrix, the number of rows and columns are equal. - fileprivate var dimension: Int = 0 - - fileprivate subscript(row: Int, column: Int) -> Element { - get { - _internalInvariant((0...row).contains(column)) - return storage[_triangularNumber(row) + column] - } - set { - _internalInvariant((0...row).contains(column)) - storage[_triangularNumber(row) + column] = newValue - } - } - - fileprivate mutating func appendRow(repeating repeatedValue: Element) { - dimension += 1 - storage.append(contentsOf: repeatElement(repeatedValue, count: dimension)) - } -} - -extension _LowerTriangularMatrix { - /// A collection that visits the elements in the matrix in [row major - /// order][rmo]. - /// - /// [rmo]: http://en.wikipedia.org/wiki/Row-_and_column-major_order - fileprivate struct RowMajorOrder : RandomAccessCollection { - private var base: _LowerTriangularMatrix - fileprivate init(base: _LowerTriangularMatrix) { - self.base = base + x = prev_x + y = prev_y } - fileprivate var startIndex: Int { - return base.storage.startIndex - } - - fileprivate var endIndex: Int { - return base.storage.endIndex - } - - fileprivate func index(after i: Int) -> Int { - return i + 1 - } - - fileprivate func index(before i: Int) -> Int { - return i - 1 - } - - fileprivate subscript(position: Int) -> Element { - return base.storage[position] - } - } - - fileprivate var rowMajorOrder: RowMajorOrder { - return RowMajorOrder(base: self) - } - - fileprivate subscript(row r: Int) -> Slice { - return rowMajorOrder[_triangularNumber(r)..<_triangularNumber(r + 1)] - } -} - -extension _LowerTriangularMatrix: CustomStringConvertible { - fileprivate var description: String { - var rows: [[Element]] = [] - for row in 0..( + for values: C, + _ body: (UnsafeBufferPointer) throws -> R + ) rethrows -> R { + if let result = try values.withContiguousStorageIfAvailable(body) { return result } + let array = ContiguousArray(values) + return try array.withUnsafeBufferPointer(body) + } + + return _withContiguousStorage(for: old) { a in + return _withContiguousStorage(for: new) { b in + return CollectionDifference(_formChanges(from: a, to: b, using:_descent(from: a, to: b)))! } - return String(describing: rows) } } diff --git a/test/stdlib/Diffing.swift b/test/stdlib/Diffing.swift index 1a88a198cc7bc..0d07c700e2fb0 100644 --- a/test/stdlib/Diffing.swift +++ b/test/stdlib/Diffing.swift @@ -8,8 +8,7 @@ let suite = TestSuite("Diffing") // This availability test has to be this awkward because of // rdar://problem/48450376 - Availability checks don't apply to top-level code -// FIXME(availability-5.1) -if #available(macOS 9999, iOS 9999, tvOS 9999, watchOS 9999, * ) { +if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { suite.test("Diffing empty collections") { let a = [Int]() From 7041d5c5110ff5d843117120cabd31a161790d69 Mon Sep 17 00:00:00 2001 From: Scott Perry Date: Thu, 27 Jun 2019 15:37:29 -0700 Subject: [PATCH 2/3] Fix build on platforms < (macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) by disabling diffing tests --- test/stdlib/Diffing.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/stdlib/Diffing.swift b/test/stdlib/Diffing.swift index 0d07c700e2fb0..28434593b34c0 100644 --- a/test/stdlib/Diffing.swift +++ b/test/stdlib/Diffing.swift @@ -657,9 +657,6 @@ if #available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) { } } } -} // if #available -else { - fatalError("Unexpected failure of future availability") } runAllTests() From 3d741f7f2e45a5545d760a9b92d206bacec2f65c Mon Sep 17 00:00:00 2001 From: Scott Perry Date: Thu, 27 Jun 2019 16:01:17 -0700 Subject: [PATCH 3/3] Restore call to reserveCapacity --- stdlib/public/core/Diffing.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/stdlib/public/core/Diffing.swift b/stdlib/public/core/Diffing.swift index 7c865e6bff74b..e840202e68aa3 100644 --- a/stdlib/public/core/Diffing.swift +++ b/stdlib/public/core/Diffing.swift @@ -290,6 +290,7 @@ fileprivate func _myers( using trace: [_V] ) -> [CollectionDifference.Change] { var changes = [CollectionDifference.Change]() + changes.reserveCapacity(trace.count) var x = a.count var y = b.count