felangel / equatable

A Dart package that helps to implement value based equality without needing to explicitly override == and hashCode.

Home Page:https://pub.dev/packages/equatable

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Storing the hashCode

MelbourneDeveloper opened this issue · comments

Immutable types don't change, so there is no need to recalculate the hashCode repeatedly. It's possible to squeeze some performance out of the hashCode getter by either calculating and storing it in the constructor or storing it after the first time it gets called. I made the changes to EquatableMixin in this branch of my fork in this PR to see how it goes. It was straightforward and doesn't affect any tests. I added a test to make sure it works. I haven't made any changes to Equatable in this draft PR.

Here is some benchmark code using benchmark_harness.

import 'package:benchmark_harness/benchmark_harness.dart';
import 'package:equatable/equatable.dart';

class A with EquatableMixin {
  A(
    this.name,
    this.count,
    this.rate,
    this.b,
  );

  final String name;
  final int count;
  final double rate;
  final B b;

  @override
  List<Object?> get props => [
        name,
        count,
        rate,
        b,
      ];
}

class B with EquatableMixin {
  B(
    this.name,
    this.count,
    this.rate,
  );

  final String name;
  final int count;
  final double rate;

  @override
  List<Object?> get props => [
        name,
        count,
        rate,
      ];
}

class TemplateBenchmark extends BenchmarkBase {
  const TemplateBenchmark() : super('Template');

  static void main() {
    const TemplateBenchmark().report();
  }

  // The benchmark code.
  @override
  void run() {
    A a = A(
      'abcdefghijklmnop',
      1,
      3243434.434,
      B(
        'kjsdfksdfkhsdf',
        2,
        3243434.434,
      ),
    );

    final hash1 = a.hashCode;
    final hash2 = a.hashCode;
  }

  @override
  void setup() {}

  @override
  void teardown() {}
}

void main() {
  TemplateBenchmark.main();
}

On my old Linux laptop, the average for the original is:

Template(RunTime): 3.0033961621384506 us.

New version:

Template(RunTime): 1.535801732099134 us.

I can do some tests on macOS and Windows if that is necessary. You can see that there is a fair bit of difference when there are multiple calls to get the hashCode.

The obvious downside is that it will cause issues with any types that are not immutable, but they will have issues anyway.

@felangel

I created a package doing exactly that, a couple of months back: https://github.com/FaFre/fast_equatable maybe you can find some inspirations in there.

By adding a mutable property on EquatableMixin, all instances would no longer be immutable.

For example:

import 'package:meta/meta.dart';

mixin EquatableMixin on Object {
  int? _hashCode;
}

@immutable
class Person with EquatableMixin {}

The above code will result in a warning because Person should be immutable but has non-final fields:

Screen Shot 2022-10-10 at 9 41 56 AM

@felangel I don't think anything can be done to fully satisfy the immutable requirement for the mixin.

There would be a similar issue with the Equatable class. Technically this is still immutable:

image

But, the class loses the const constructor. Still, there is something unwholesome about repeatedly hashing data that doesn't change.