Top 3 largest expenses
Requirement
From a month of expenses, print the three largest — merchant and amount, biggest first. The data is in the code below; both versions must print the lines shown under Expected output.
Expected output
Airline Ticket $289.99 New Headphones $129.00 Electric Co $60.34
Side by side
Native Dart
FxDart
Why they differ
They barely do — this one is a tie on the page. Both sides sort on a
negated key to get descending order and take the first three;
package:collection's sortedBy is every bit as
direct as FxDart's sortBy (core List.sort alone
would mutate in place and need an explicit comparator, but
collection is a standard dependency). The only real
difference in the listing is where the vocabulary lives: an extension
method from a package vs a step in a chain that also offers
scan, chunk, and async variants. Pick either
with a clear conscience.
The listing is a tie; the clock is not. At a million rows the bars below have FxDart about 2.6× faster (200 ms vs 521 ms), and that is not a smarter big-O — both copy the list and run a stable O(n log n) merge sort. The difference is what one comparison costs.
Native sortedBy is collection's mergeSortBy:
it sorts the rows and calls the key extractor
inside every comparison. A million rows is about twenty
million calls to (t) => -t.amount. The key type is a
generic K extends Comparable — here num —
so each of those keys is a heap-allocated boxed double
compared through a virtual compareTo.
FxDart's sortBy extracts first. One walk of the list
writes every key into a Float64List (it noticed they
were all double). Then it merges the keys and the rows
together, sequentially: each comparison is two machine
doubles from a typed array, using a VM compare on this data (the
amounts are ordinary finite positives, so there is no NaN or
-0.0 to force the slower compareTo path).
One extraction per row, no boxing, no dispatch, and no random index
chase. An earlier sortBy did decorate by sorting a list
of indices with List.sort; that is gone. The merge is
stable by construction — equal keys keep their input order on both
sides now.
The honest limit: when the keys are not uniformly
double, int, or String,
sortBy falls back to a generic comparator and the
advantage disappears. Memory at a million rows is close (about
123 MB vs 130 MB) — both keep the row copy plus a scratch buffer.
The amounts here are all distinct, so stability does not show on
the printed three lines.
Benchmark
N = 100
Time Tie
Peak memory Tie
N = 10,000
Time FxDart wins
Peak memory FxDart wins
N = 1,000,000
Time FxDart wins
Peak memory Native wins
Bars are medians of repeated timed iterations in fresh processes per side (small N is batched for timer resolution). Sides within 5% of each other — or within 0.6 ms, a difference no person can perceive — count as a tie; close relative races are re-measured up to 5 runs. In an app, anything under a few milliseconds is invisible to the user regardless of which bar is shorter. Memory is peak process RSS. The Dart VM and the dataset are identical on both sides, so the difference between the two bars is what the pipeline itself holds onto.