Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: improve equality comparison performance #173

Merged
merged 17 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 116 additions & 38 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,82 +11,160 @@ Benchmarks used to measure the performance of equality comparisons using `packag

## Results

**JIT**

```
EmptyEquatable
total runs: 2 064 037
total runs: 2 729 471
total time: 2.0000 s
average run: 0 μs
runs/second: Infinity
units: 100
units: 100
units/second: Infinity
time per unit: 0.0000 μs

PrimitiveEquatable
total runs: 729 555
total runs: 669 972
total time: 2.0000 s
average run: 2 μs
runs/second: 500 000
units: 100
units/second: 50 000 000
runs/second: 500 000
units: 100
units/second: 50 000 000
time per unit: 0.0200 μs

CollectionEquatable (static, small)
total runs: 51 944
total runs: 144 932
total time: 2.0000 s
average run: 38 μs
runs/second: 26 316
units: 100
units/second: 2 631 579
time per unit: 0.3800 μs
average run: 13 μs
runs/second: 76 923
units: 100
units/second: 7 692 308
time per unit: 0.1300 μs

CollectionEquatable (static, medium)
total runs: 44 572
total runs: 84 533
total time: 2.0000 s
average run: 44 μs
runs/second: 22 727
units: 100
units/second: 2 272 727
time per unit: 0.4400 μs
average run: 23 μs
runs/second: 43 478
units: 100
units/second: 4 347 826
time per unit: 0.2300 μs

CollectionEquatable (static, large)
total runs: 21 027
total runs: 16 457
total time: 2.0001 s
average run: 95 μs
runs/second: 10 526
units: 100
units/second: 1 052 632
time per unit: 0.9500 μs
average run: 121 μs
runs/second: 8 264.5
units: 100
units/second: 826 446
time per unit: 1.2100 μs

CollectionEquatable (dynamic, small)
total runs: 388 236
total time: 2.0000 s
average run: 5 μs
runs/second: 200 000
units: 100
units/second: 20 000 000
time per unit: 0.0500 μs

CollectionEquatable (dynamic, medium)
total runs: 382 155
total time: 2.0000 s
average run: 5 μs
runs/second: 200 000
units: 100
units/second: 20 000 000
time per unit: 0.0500 μs

CollectionEquatable (dynamic, large)
total runs: 390 713
total time: 2.0000 s
average run: 5 μs
runs/second: 200 000
units: 100
units/second: 20 000 000
time per unit: 0.0500 μs
```

**AOT**

```
EmptyEquatable
total runs: 1 615 534
total time: 2.0000 s
average run: 1 μs
runs/second: 1 000 000
units: 100
units/second: 100 000 000
time per unit: 0.0100 μs

PrimitiveEquatable
total runs: 928 013
total time: 2.0000 s
average run: 2 μs
runs/second: 500 000
units: 100
units/second: 50 000 000
time per unit: 0.0200 μs

CollectionEquatable (static, small)
total runs: 128 224
total time: 2.0000 s
average run: 15 μs
runs/second: 66 667
units: 100
units/second: 6 666 667
time per unit: 0.1500 μs

CollectionEquatable (static, medium)
total runs: 104 624
total time: 2.0000 s
average run: 19 μs
runs/second: 52 632
units: 100
units/second: 5 263 158
time per unit: 0.1900 μs

CollectionEquatable (static, large)
total runs: 33 653
total time: 2.0000 s
average run: 59 μs
runs/second: 16 949
units: 100
units/second: 1 694 915
time per unit: 0.5900 μs

CollectionEquatable (dynamic, small)
total runs: 400 934
total runs: 483 177
total time: 2.0000 s
average run: 4 μs
runs/second: 250 000
units: 100
units/second: 25 000 000
runs/second: 250 000
units: 100
units/second: 25 000 000
time per unit: 0.0400 μs

CollectionEquatable (dynamic, medium)
total runs: 400 408
total runs: 488 550
total time: 2.0000 s
average run: 4 μs
runs/second: 250 000
units: 100
units/second: 25 000 000
runs/second: 250 000
units: 100
units/second: 25 000 000
time per unit: 0.0400 μs

CollectionEquatable (dynamic, large)
total runs: 400 966
total runs: 494 041
total time: 2.0000 s
average run: 4 μs
runs/second: 250 000
units: 100
units/second: 25 000 000
runs/second: 250 000
units: 100
units/second: 25 000 000
time per unit: 0.0400 μs
```

_Last Updated: June 3, 2024 using `725b76c9ef072695f3ae4f036c4fa5e015528f13`_
_Last Updated: November 20, 2024 using `29e6c77a2e6b25e35cce66276bc2afeab1c805bd`_

_MacBook Pro (M1 Pro, 16GB RAM)_

Dart SDK version: 3.5.0-218.0.dev (dev) (Mon Jun 3 13:02:57 2024 -0700) on "macos_arm64"
Dart SDK version: Dart SDK version: 3.5.4 (stable) (Wed Oct 16 16:18:51 2024 +0000) on "macos_arm64"
2 changes: 1 addition & 1 deletion benchmarks/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: equatable_benchmarks
publish_to: none

environment:
sdk: ">=2.12.0 <3.0.0"
sdk: ">=3.5.0 <4.0.0"

dependencies:
equatable: ^2.0.0
Expand Down
2 changes: 1 addition & 1 deletion lib/src/equatable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ abstract class Equatable {
return identical(this, other) ||
other is Equatable &&
runtimeType == other.runtimeType &&
equals(props, other.props);
iterableEquals(props, other.props);
}

@override
Expand Down
2 changes: 1 addition & 1 deletion lib/src/equatable_mixin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ mixin EquatableMixin {
return identical(this, other) ||
other is EquatableMixin &&
runtimeType == other.runtimeType &&
equals(props, other.props);
iterableEquals(props, other.props);
}

@override
Expand Down
68 changes: 49 additions & 19 deletions lib/src/equatable_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,62 @@ int mapPropsToHashCode(Iterable<Object?>? props) {
return _finish(props == null ? 0 : props.fold(0, _combine));
}

const DeepCollectionEquality _equality = DeepCollectionEquality();
/// Determines whether two iterables are equal.
@pragma('vm:prefer-inline')
bool iterableEquals(Iterable<Object?> a, Iterable<Object?> b) {
assert(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on calling setEquals instead of asserting here?

if (a is Set && b is Set) return setEquals(a, b);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't expect anyone to use this function. I'm not sure it's even exported. So I'd prefer to leave it simple, without extra ifs

a is! Set && b is! Set,
"iterableEquals doesn't support Sets. Use setEquals instead.",
);
if (identical(a, b)) return true;
if (a.length != b.length) return false;
for (var i = 0; i < a.length; i++) {
if (!objectsEquals(a.elementAt(i), b.elementAt(i))) return false;
}
return true;
}

/// Determines whether [list1] and [list2] are equal.
bool equals(List<Object?>? list1, List<Object?>? list2) {
if (identical(list1, list2)) return true;
if (list1 == null || list2 == null) return false;
final length = list1.length;
if (length != list2.length) return false;
/// Determines whether two sets are equal.
bool setEquals(Set<Object?> a, Set<Object?> b) {
if (identical(a, b)) return true;
if (a.length != b.length) return false;
for (final element in a) {
if (!b.any((e) => objectsEquals(element, e))) return false;
}
return true;
}

for (var i = 0; i < length; i++) {
final unit1 = list1[i];
final unit2 = list2[i];
/// Determines whether two maps are equal.
bool mapEquals(Map<Object?, Object?> a, Map<Object?, Object?> b) {
if (identical(a, b)) return true;
if (a.length != b.length) return false;
for (final key in a.keys) {
if (!objectsEquals(a[key], b[key])) return false;
}
return true;
}

if (_isEquatable(unit1) && _isEquatable(unit2)) {
if (unit1 != unit2) return false;
} else if (unit1 is Iterable || unit1 is Map) {
if (!_equality.equals(unit1, unit2)) return false;
} else if (unit1?.runtimeType != unit2?.runtimeType) {
return false;
} else if (unit1 != unit2) {
return false;
}
/// Determines whether two objects are equal.
@pragma('vm:prefer-inline')
bool objectsEquals(Object? a, Object? b) {
if (identical(a, b)) return true;
if (_isEquatable(a) && _isEquatable(b)) {
return a == b;
} else if (a is Set && b is Set) {
return setEquals(a, b);
} else if (a is Iterable && b is Iterable) {
return iterableEquals(a, b);
} else if (a is Map && b is Map) {
return mapEquals(a, b);
} else if (a?.runtimeType != b?.runtimeType) {
return false;
} else if (a != b) {
return false;
}
return true;
}

@pragma('vm:prefer-inline')
bool _isEquatable(Object? object) {
return object is Equatable || object is EquatableMixin;
}
Expand Down
Loading