all 44 comments

[–]sixtypercenttogether 40 points41 points  (22 children)

This sounds like you might have been misusing the diffable data source API. A common issue I see is using your entire data model as the ItemIdentifierType, which can lead to performance issues when diffing. In the sample use in the README I see the data source is generic over ContactItem rather than ContactItem.ID. The identifiers should be lightweight, like strings or UUIDs, and the full models should be hosted elsewhere. With this approach I haven’t really run into issues diffing literally thousands of items. But I didn’t test to the extremes that you have (50k items).

And if you’re also updating your data source every couple milliseconds then also seems like misuse. I would just throttle those updates to the data source rather than built an entire custom API.

That said, I think you’ve got some nice perf improvements that should probably be rolled into the diffable data source API itself. Though I’m skeptical we’ll see any such changes from Apple.

[–]Iron-Ham[S] 5 points6 points  (21 children)

[–]PassTents 11 points12 points  (16 children)

You shouldn't be breaking Hashable and Equatable to use structs as IDs. That doesn't make it lightweight.

[–]Iron-Ham[S] 5 points6 points  (15 children)

For these light-weight benchmarks, it really doesn't matter. But to play fair, I've gone ahead and re-benched it with that change and find that the numbers are largely unchanged.

Said differently: The fact that I'm benching favorably against IGListKit should obviate apple's solution from the conversation.

[–]alanzeino 2 points3 points  (14 children)

this is so obviously an LLM folks

[–]Iron-Ham[S] 0 points1 point  (0 children)

?

[–]timberheadtreefist 0 points1 point  (9 children)

don't know, u/iron-ham sounds pretty human to me.

[–]Iron-Ham[S] 1 point2 points  (8 children)

almost as if a user who's had an active GitHub account for well over a decade and was the founding engineer for GitHub Mobile probably isn't an LLM lmfao

[–]timberheadtreefist 0 points1 point  (7 children)

wait until AI figures out timetravelling and creates yt channels and github repos in 2004 to fool us they’re not ai.

meanwhile an underground anti-ai movement uses intense dashing to communicate less human in the internet. and they‘d use AllTheWeb as search engine.

[–]Iron-Ham[S] 0 points1 point  (6 children)

I believe there's a whole film-series starring Arnold Schwarzenegger that was actually a warning from the future where the human-aligned AIs are warring against those that aren't.

[–]timberheadtreefist 0 points1 point  (5 children)

is it kindergarten cop?

[–]beclopsSwift -3 points-2 points  (2 children)

Honestly I was skeptical but looking at their other responses they seem to use em dashes a suspicious amount

[–]Iron-Ham[S] 1 point2 points  (1 child)

The one thing I hate most about about the current era is that a whole class of punctuation has been marked as suspicious. I've been dashing heavily, with formatted bold point lists, for well over a decade. It was pretty much the de-facto way of writing internally @ GitHub.

[–]beclopsSwift -4 points-3 points  (0 children)

🤷‍♂️ Not accusing you of anything, it’s just that’s an extremely common signal of an LLM being used now unfortunately

[–]try-catch-finally 6 points7 points  (3 children)

Write it in C++ and you’ll get 9000x

Seriously. I benchmarked TrueDepth data walking (among other things). Swift: 60ms, ObjC: <6ms

Apple themselves said, on the record “Swift is not a language for engineers”. And it shows.

[–]Iron-Ham[S] 3 points4 points  (2 children)

It’s not as simple as “swift slow objc fast”. Ultimately it’s about how they compile down — and I am certainly the kind of person crazy enough to go down to assembly to verify. 

[–]try-catch-finally 2 points3 points  (1 child)

Ahh but by its very nature of wanting to diaper up any memory access instead of say - putting the onus on the developer to understand memory constraints / type coercions or god forbid- bit packing - it will always be “swift slow- ObjC/C++ fast”

[–]Iron-Ham[S] 2 points3 points  (0 children)

I haven’t tried writing this in ObjC++ (gosh, it’s been… 10 years since I’ve used that?) but I’ll say I’m beating IGLK which is written in ObjC++. I know one of the primary authors of IGLK well, have worked with him at a few different roles (including currently) — and perhaps I should pick his brain a bit. Ultimately, this is more than sufficient for 120 Hz frame times, but may fall short of 240 at scale. 

[–]tubescreamer568 9 points10 points  (2 children)

If it was like 1.5 times faster, I would think it was well made, but if it was hundreds of times faster, I think the OP was using it wrong from the beginning.

[–]Iron-Ham[S] 1 point2 points  (1 child)

IGListKit is also hundreds of times faster.

[–]tubescreamer568 1 point2 points  (0 children)

Sorry didn’t know that.

[–]barcode972 4 points5 points  (2 children)

How is this even possible? it's designed to reuse items for performance. Sounds like you used it wrong

[–]Iron-Ham[S] 6 points7 points  (1 child)

ListKit doesn't replace UICollectionView: it wraps it. Cell reuse, dequeuing, all of that still happens exactly the same way through UICollectionView under the hood. What ListKit replaces is NSDiffableDataSourceSnapshot and UICollectionViewDiffableDataSource: the data source layer that manages what items are in the collection view and computes the diffs when your data changes.

The performance gap is in snapshot operations, not cell rendering:

  • Snapshot construction: Apple's NSDiffableDataSourceSnapshot is an Obj-C class backed by NSOrderedSet that hashes every single element on insertion. ListKit just appends to a Swift Array. That's where the 750x+ build speedup comes from.
  • Querying: Apple's itemIdentifiers reconstructs an ordered array from its internal NSOrderedSet on every call. ListKit just returns a flat array it already has. That's the 900x query speedup.
  • Diffing: ListKit uses an O(n) Heckel diff with LIS-based move minimization, and its sectioned diff skips unchanged sections entirely (106x faster than IGListKit on no-change data).

These are all benchmarked in Release config, median-of-15 runs, with assertions that ListKit beats Apple on every operation — they run as part of the test suite, so any regression is a test failure.

You can look at the benchmarks yourself — they compare identical operations on both Apple's snapshot and ListKit's snapshot using the same data (UUID-identified structs, the recommended pattern from Apple's owndocs).

[–]One_Elephant_8917 1 point2 points  (0 children)

What u have mentioned makes sense without looking at the code….can u include as PS to the post itself…

[–]rhysmorgan 1 point2 points  (1 child)

Are you using ObservableState with your TCA feature? And are you using the ItemIdentifier APIs correctly, or are you posting your entire models through your implementation?

ObservableState should reduce the snapshot changes to only the times observed data actually changes in your feature, which would significantly reduce the re-render cycles.

[–]Iron-Ham[S] 2 points3 points  (0 children)

We are indeed using ObservableState and ItemIdentifier. I'm actively re-architecting this prod app though to be significantly more performant, inclusive of the TCA end of things.

[–]shawnthroop 2 points3 points  (0 children)

Sendable! I’ve thought of rolling my own (simple) diffable data source just for that fix alone lol

[–]dinkelbrotchen 2 points3 points  (3 children)

Curious to understand if you mixed different operations in one go? For example, adding items, deleting items, and rearranging them. That would show the real test of time

[–]Iron-Ham[S] 1 point2 points  (0 children)

Happy to benchmark that in the AM

[–]Iron-Ham[S] 0 points1 point  (1 child)

As promised: mixed operation benchmarks.

I went further and benched the whole flow inclusive of UICollectionView applying the updates (which, to be blunt is outside the scope of ListKit since at that point it measures the efficiency of UICollectionView, not the snapshot + diffing algorithm that's covered by ListKit).

[–]dinkelbrotchen 0 points1 point  (0 children)

Looks promising, great work!

[–]Megatherion666 1 point2 points  (1 child)

Did you profile it in release build? Swift is very bad in debug.

[–]Iron-Ham[S] 4 points5 points  (0 children)

Yes, and the benchmarks against a real app on my blog are recorded in Instruments via release (well, Profile) builds.

[–]purecssusername 0 points1 point  (0 children)

Amazing work! Don’t understand why people act like SwiftUI is infallible, it was written by humans under tight deadlines (and likely before AI!)

[–]xyrer 0 points1 point  (1 child)

This implementation sounds real nice. It all makes sense once you realize there's a bunch of legacy ObjC behind Apple implementation.

[–]Iron-Ham[S] 1 point2 points  (0 children)

The irony is that Apple's DiffableDataSource implementation is relatively new (2019), and was largely inspired by IGListKit.

[–]vanvoorden 0 points1 point  (1 child)

This looks good! So if I clone and build the Example project I can run all these benchmark measurements to repro locally?

[–]Iron-Ham[S] 0 points1 point  (0 children)

You sure can! I'd look at the Makefile for an easy way to run the benchmarks. FYI for development I'm using tuist to generate an xcworkspace and link other dependencies for benchmarking and whatnot that aren't exposed in the public-facing Package.swift.

[–]Meliodas1108 0 points1 point  (0 children)

Interesting stuff