From afd2d29eecfac2231d2bcf815c76e844c98d838e Mon Sep 17 00:00:00 2001 From: Ryan Nystrom Date: Tue, 13 Feb 2018 10:07:48 -0800 Subject: [PATCH] Add diffing optimization when result is all deletes or inserts Summary: In an attempt to best other diffing libraries, I noticed that pure insert/delete diffing results would be almost 5x slower than changes between existing arrays. This is pretty much a benchmarking enhancement, but improves diffing performance by ~5x when the from-array or to-array is empty. ``` // OLD only inserts avg: 0.007469, min: 0.006998, max: 0.016550, p50: 0.007254, p75: 0.007712, p90: 0.007899, p95: 0.008345, p99: 0.016550 // NEW avg: 0.001392, min: 0.001256, max: 0.006772, p50: 0.001289, p75: 0.001348, p90: 0.001533, p95: 0.001614, p99: 0.006772 ``` ``` // OLD only deletes avg: 0.005821, min: 0.005669, max: 0.006511, p50: 0.005766, p75: 0.005852, p90: 0.006030, p95: 0.006204, p99: 0.006511 // NEW avg: 0.001184, min: 0.001096, max: 0.001673, p50: 0.001123, p75: 0.001212, p90: 0.001378, p95: 0.001467, p99: 0.001673 ``` Note the average time improvements (seconds). Benchmarking done on a 4s w/ this project: https://pxl.cl/bLBB Reviewed By: manicakes Differential Revision: D6968683 fbshipit-source-id: 0d8e058f0aaa9ce756ca69326527d04504ac6429 --- CHANGELOG.md | 2 ++ Source/Common/IGListDiff.mm | 62 +++++++++++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e68b4abf1..3f4113e4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ The changelog for `IGListKit`. Also see the [releases](https://github.com/instag - Add support for UICollectionView's interactive reordering in iOS 9+. Updates include `-[IGListSectionController canMoveItemAtIndex:]` to enable the behavior, `-[IGListSectionController moveObjectFromIndex:toIndex:]` called when items within a section controller were moved through reordering, `-[IGListAdapterDataSource listAdapter:moveObject:from:to]` called when section controllers themselves were reordered (only possible when all section controllers contain exactly 1 object), and `-[IGListUpdatingDelegate moveSectionInCollectionView:fromIndex:toIndex]` to enable custom updaters to conform to the reordering behavior. The update also includes two new examples `ReorderableSectionController` and `ReorderableStackedViewController` to demonstrate how to enable interactive reordering in your client app. [Jared Verdi](https://github.com/jverdi) [(#976)](https://github.com/Instagram/IGListKit/pull/976) +- 5x improvement to diffing performance when result is only inserts or deletes. [Ryan Nystrom](https://github.com/rnystrom) [(tbd)](tbd) + ### Fixes 3.2.0 diff --git a/Source/Common/IGListDiff.mm b/Source/Common/IGListDiff.mm index 30b782188..178492ddd 100644 --- a/Source/Common/IGListDiff.mm +++ b/Source/Common/IGListDiff.mm @@ -84,6 +84,16 @@ static void addIndexToCollection(BOOL useIndexPaths, __unsafe_unretained id coll } }; +static NSArray *indexPathsAndPopulateMap(__unsafe_unretained NSArray> *array, NSInteger section, __unsafe_unretained NSMapTable *map) { + NSMutableArray *paths = [NSMutableArray new]; + [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:section]; + [paths addObject:path]; + [map setObject:paths forKey:[obj diffIdentifier]]; + }]; + return paths; +} + static id IGListDiffing(BOOL returnIndexPaths, NSInteger fromSection, NSInteger toSection, @@ -94,6 +104,55 @@ static id IGListDiffing(BOOL returnIndexPaths, const NSInteger newCount = newArray.count; const NSInteger oldCount = oldArray.count; + NSMapTable *oldMap = [NSMapTable strongToStrongObjectsMapTable]; + NSMapTable *newMap = [NSMapTable strongToStrongObjectsMapTable]; + + // if no new objects, everything from the oldArray is deleted + // take a shortcut and just build a delete-everything result + if (newCount == 0) { + if (returnIndexPaths) { + return [[IGListIndexPathResult alloc] initWithInserts:[NSArray new] + deletes:indexPathsAndPopulateMap(oldArray, fromSection, oldMap) + updates:[NSArray new] + moves:[NSArray new] + oldIndexPathMap:oldMap + newIndexPathMap:newMap]; + } else { + [oldArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + addIndexToMap(returnIndexPaths, fromSection, idx, obj, oldMap); + }]; + return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet new] + deletes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, oldCount)] + updates:[NSIndexSet new] + moves:[NSArray new] + oldIndexMap:oldMap + newIndexMap:newMap]; + } + } + + // if no old objects, everything from the newArray is inserted + // take a shortcut and just build an insert-everything result + if (oldCount == 0) { + if (returnIndexPaths) { + return [[IGListIndexPathResult alloc] initWithInserts:indexPathsAndPopulateMap(newArray, toSection, newMap) + deletes:[NSArray new] + updates:[NSArray new] + moves:[NSArray new] + oldIndexPathMap:oldMap + newIndexPathMap:newMap]; + } else { + [newArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + addIndexToMap(returnIndexPaths, toSection, idx, obj, newMap); + }]; + return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newCount)] + deletes:[NSIndexSet new] + updates:[NSIndexSet new] + moves:[NSArray new] + oldIndexMap:oldMap + newIndexMap:newMap]; + } + } + // symbol table uses the old/new array diffIdentifier as the key and IGListEntry as the value // using id as the key provided by https://lists.gnu.org/archive/html/discuss-gnustep/2011-07/msg00019.html unordered_map, IGListEntry, IGListHashID, IGListEqualID> table; @@ -185,9 +244,6 @@ static id IGListDiffing(BOOL returnIndexPaths, mMoves = [NSMutableArray new]; } - NSMapTable *oldMap = [NSMapTable strongToStrongObjectsMapTable]; - NSMapTable *newMap = [NSMapTable strongToStrongObjectsMapTable]; - // track offsets from deleted items to calculate where items have moved vector deleteOffsets(oldCount), insertOffsets(newCount); NSInteger runningOffset = 0;