perf: improve equality comparison performance (#173)

Co-authored-by: Felix Angelov <felangelov@gmail.com>
diff --git a/benchmarks/README.md b/benchmarks/README.md
index 474db23..fe90cb0 100644
--- a/benchmarks/README.md
+++ b/benchmarks/README.md
@@ -11,82 +11,160 @@
 
 ## 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:    400 934   
+          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:    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"
diff --git a/benchmarks/pubspec.yaml b/benchmarks/pubspec.yaml
index 380774d..4ab5d9e 100644
--- a/benchmarks/pubspec.yaml
+++ b/benchmarks/pubspec.yaml
@@ -2,7 +2,7 @@
 publish_to: none
 
 environment:
-  sdk: ">=2.12.0 <3.0.0"
+  sdk: ">=3.5.0 <4.0.0"
 
 dependencies:
   equatable: ^2.0.0
diff --git a/lib/src/equatable.dart b/lib/src/equatable.dart
index a23801f..6b4445b 100644
--- a/lib/src/equatable.dart
+++ b/lib/src/equatable.dart
@@ -46,7 +46,7 @@
     return identical(this, other) ||
         other is Equatable &&
             runtimeType == other.runtimeType &&
-            equals(props, other.props);
+            iterableEquals(props, other.props);
   }
 
   @override
diff --git a/lib/src/equatable_mixin.dart b/lib/src/equatable_mixin.dart
index b4d376d..b69db15 100644
--- a/lib/src/equatable_mixin.dart
+++ b/lib/src/equatable_mixin.dart
@@ -22,7 +22,7 @@
     return identical(this, other) ||
         other is EquatableMixin &&
             runtimeType == other.runtimeType &&
-            equals(props, other.props);
+            iterableEquals(props, other.props);
   }
 
   @override
diff --git a/lib/src/equatable_utils.dart b/lib/src/equatable_utils.dart
index 301d81f..615b002 100644
--- a/lib/src/equatable_utils.dart
+++ b/lib/src/equatable_utils.dart
@@ -6,32 +6,62 @@
   return _finish(props == null ? 0 : props.fold(0, _combine));
 }
 
-const DeepCollectionEquality _equality = DeepCollectionEquality();
-
-/// 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;
-
-  for (var i = 0; i < length; i++) {
-    final unit1 = list1[i];
-    final unit2 = list2[i];
-
-    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 iterables are equal.
+@pragma('vm:prefer-inline')
+bool iterableEquals(Iterable<Object?> a, Iterable<Object?> b) {
+  assert(
+    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 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;
+}
+
+/// 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;
+}
+
+/// 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;
 }
diff --git a/test/equatable_utils_test.dart b/test/equatable_utils_test.dart
new file mode 100644
index 0000000..bea9f88
--- /dev/null
+++ b/test/equatable_utils_test.dart
@@ -0,0 +1,266 @@
+import 'package:equatable/equatable.dart';
+import 'package:equatable/src/equatable_utils.dart';
+import 'package:test/test.dart';
+
+class Person with EquatableMixin {
+  Person({required this.name});
+
+  final String name;
+
+  @override
+  List<Object?> get props => [name];
+}
+
+void main() {
+  final bob = Person(name: 'Bob');
+  final alice = Person(name: 'Alice');
+  final aliceCopy = Person(name: 'Alice');
+
+  group('iterableEquals', () {
+    test('returns true for identical props', () {
+      final value = [Object()];
+      expect(iterableEquals(value, value), isTrue);
+    });
+
+    test('returns true for empty iterables', () {
+      expect(iterableEquals([], []), isTrue);
+    });
+
+    test('returns false when props differ in length', () {
+      final object = Object();
+      expect(iterableEquals([object], [object, object]), isFalse);
+    });
+
+    test('uses == when props are equatable', () {
+      expect(iterableEquals([alice], [aliceCopy]), isTrue);
+      expect(iterableEquals([bob], [bob]), isTrue);
+      expect(iterableEquals([alice], [bob]), isFalse);
+      expect(iterableEquals([bob], [alice]), isFalse);
+      expect(iterableEquals([alice, null], [alice, -1]), isFalse);
+    });
+
+    test('returns false for iterables with different elements', () {
+      final iterable1 = [1, 2, 3];
+      final iterable2 = [1, 2, 4];
+      expect(iterableEquals(iterable1, iterable2), isFalse);
+    });
+
+    test(
+        'returns false for iterable with same elements '
+        'but different order', () {
+      final iterable1 = [1, 2, 3];
+      final iterable2 = [1, 3, 2];
+      expect(iterableEquals(iterable1, iterable2), isFalse);
+    });
+
+    test('returns true for nested identical iterables', () {
+      final iterable1 = [
+        [bob, alice],
+        [alice, bob],
+      ];
+      final iterable2 = [
+        [bob, alice],
+        [alice, bob],
+      ];
+      expect(iterableEquals(iterable1, iterable2), isTrue);
+    });
+
+    test('returns false for nested iterables with different elements', () {
+      final iterable1 = [
+        [bob, 2],
+        [3, 4],
+      ];
+      final iterable2 = [
+        [bob, 2],
+        [3, 5],
+      ];
+      expect(iterableEquals(iterable1, iterable2), isFalse);
+    });
+  });
+
+  group('setEquals', () {
+    test('returns true for identical sets', () {
+      final set1 = {1, 2, 3};
+      final set2 = {1, 2, 3};
+      expect(setEquals(set1, set2), isTrue);
+    });
+
+    test('returns true for identical sets with elements in different order',
+        () {
+      final set1 = {1, 3, 2};
+      final set2 = {1, 2, 3};
+      expect(setEquals(set1, set2), isTrue);
+    });
+
+    test('returns false for sets of different lengths', () {
+      final set1 = {1, 2, 3};
+      final set2 = {1, 2};
+      expect(setEquals(set1, set2), isFalse);
+    });
+
+    test('returns false for sets with different elements', () {
+      final set1 = {1, 2, 3};
+      final set2 = {1, 2, 4};
+      expect(setEquals(set1, set2), isFalse);
+    });
+
+    test('uses == when props are equatable', () {
+      expect(setEquals({alice}, {aliceCopy}), isTrue);
+      expect(setEquals({bob}, {bob}), isTrue);
+      expect(setEquals({alice}, {bob}), isFalse);
+      expect(setEquals({bob}, {alice}), isFalse);
+      expect(setEquals({alice, null}, {alice, -1}), isFalse);
+    });
+
+    test('returns true for nested identical sets', () {
+      final set1 = {
+        {alice, bob},
+        {alice, bob},
+      };
+      final set2 = {
+        {alice, bob},
+        {alice, bob},
+      };
+      expect(setEquals(set1, set2), isTrue);
+    });
+
+    test('returns false for nested sets with different elements', () {
+      final set1 = {
+        {bob, 2},
+        {3, 4},
+      };
+      final set2 = {
+        {bob, 2},
+        {3, 5},
+      };
+      expect(setEquals(set1, set2), isFalse);
+    });
+
+    test('returns true for empty sets', () {
+      expect(setEquals({}, {}), isTrue);
+    });
+
+    test('returns false for sets with different types', () {
+      final set1 = {1, '2', 3};
+      final set2 = {1, 2, 3};
+      expect(setEquals(set1, set2), isFalse);
+    });
+  });
+
+  group('mapEquals', () {
+    test('returns true for identical maps', () {
+      final map1 = {'a': 1, 'b': 2, 'c': 3};
+      final map2 = {'a': 1, 'b': 2, 'c': 3};
+      expect(mapEquals(map1, map2), isTrue);
+    });
+
+    test(
+      'returns true for identical maps with elements in different order',
+      () {
+        final map1 = {'a': 1, 'c': 3, 'b': 2};
+        final map2 = {'a': 1, 'b': 2, 'c': 3};
+        expect(mapEquals(map1, map2), isTrue);
+      },
+    );
+
+    test('uses == when props are equatable', () {
+      expect(mapEquals({'a': alice}, {'a': aliceCopy}), isTrue);
+      expect(mapEquals({alice: 'a'}, {aliceCopy: 'a'}), isTrue);
+      expect(mapEquals({'a': bob}, {'a': bob}), isTrue);
+    });
+
+    test('returns false for maps of different lengths', () {
+      final map1 = {'a': 1, 'b': 2, 'c': 3};
+      final map2 = {'a': 1, 'b': 2};
+      expect(mapEquals(map1, map2), isFalse);
+    });
+
+    test('returns false for maps with different keys', () {
+      final map1 = {'a': 1, 'b': 2, 'c': 3};
+      final map2 = {'a': 1, 'b': 2, 'd': 3};
+      expect(mapEquals(map1, map2), isFalse);
+    });
+
+    test('returns false for maps with different values', () {
+      final map1 = {'a': 1, 'b': 2, 'c': 3};
+      final map2 = {'a': 1, 'b': 2, 'c': 4};
+      expect(mapEquals(map1, map2), isFalse);
+    });
+
+    test('returns true for nested identical maps', () {
+      final map1 = {
+        'a': {'x': 1, 'y': 2},
+        'b': {'z': 3},
+      };
+      final map2 = {
+        'a': {'x': 1, 'y': 2},
+        'b': {'z': 3},
+      };
+      expect(mapEquals(map1, map2), isTrue);
+    });
+
+    test('returns false for nested maps with different elements', () {
+      final map1 = {
+        'a': {'x': 1, 'y': 2},
+        'b': {'z': 3},
+      };
+      final map2 = {
+        'a': {'x': 1, 'y': 2},
+        'b': {'z': 4},
+      };
+      expect(mapEquals(map1, map2), isFalse);
+    });
+
+    test('returns true for empty maps', () {
+      expect(mapEquals({}, {}), isTrue);
+    });
+
+    test('returns false for maps with same keys but different values', () {
+      final map1 = {'a': 1, 'b': '2', 'c': 3};
+      final map2 = {'a': 1, 'b': 2, 'c': 3};
+      expect(mapEquals(map1, map2), isFalse);
+    });
+  });
+
+  group('objectsEquals', () {
+    test('returns true for identical objects', () {
+      final object = Object();
+      expect(objectsEquals(object, object), isTrue);
+    });
+
+    test('returns true for same objects', () {
+      const object1 = 'object';
+      // ignore: prefer_const_declarations
+      final object2 = 'object';
+      expect(objectsEquals(object1, object2), isTrue);
+    });
+
+    test('returns true for equatable objects', () {
+      expect(objectsEquals(alice, aliceCopy), isTrue);
+      expect(objectsEquals(bob, bob), isTrue);
+    });
+
+    test('returns false for different objects', () {
+      expect(objectsEquals(alice, bob), isFalse);
+      expect(objectsEquals(bob, alice), isFalse);
+    });
+
+    test('returns true for same lists', () {
+      expect(objectsEquals([1, 2, 3], [1, 2, 3]), isTrue);
+    });
+
+    test('returns true for same sets', () {
+      expect(objectsEquals({1, 2, 3}, {1, 2, 3}), isTrue);
+      expect(objectsEquals({1, 3, 2}, {1, 2, 3}), isTrue);
+    });
+
+    test('returns true for same maps', () {
+      expect(objectsEquals({'a': 1, 'b': 2}, {'a': 1, 'b': 2}), isTrue);
+      expect(objectsEquals({'c': 3, 'b': 2}, {'b': 2, 'c': 3}), isTrue);
+    });
+
+    test('returns false for different types', () {
+      expect(objectsEquals(1, '1'), isFalse);
+    });
+  });
+}