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

RDART-1007: Compare 8 bytes at a time for Uint8Lists #1628

Merged
merged 4 commits into from
Apr 17, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Added support for specifying schema version on `Configuration.flexibleSync`. This allows you to take advantage of an upcoming server-side feature that will allow schema migrations for synchronized Realms. (Issue [#1599](https://github.com/realm/realm-dart/issues/1599))
* The default base url in `AppConfiguration` has been updated to point to `services.cloud.mongodb.com`. See https://www.mongodb.com/docs/atlas/app-services/domain-migration/ for more information. (Issue [#1549](https://github.com/realm/realm-dart/issues/1549))
* Don't ignore private fields on realm models. (Issue [#1367](https://github.com/realm/realm-dart/issues/1367))
* Improve performance of `RealmValue.operator==` when containing binary data. (PR [#1628](https://github.com/realm/realm-dart/pull/1628))

### Fixed
* Using valid const, but non-literal expressions, such as negation of numbers, as an initializer would fail. (Issue [#1606](https://github.com/realm/realm-dart/issues/1606))
Expand Down
43 changes: 36 additions & 7 deletions packages/realm_common/lib/src/realm_types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import 'dart:math';
import 'dart:typed_data';
import 'package:objectid/objectid.dart';
import 'package:sane_uuid/uuid.dart';
import 'package:collection/collection.dart';

Type _typeOf<T>() => T;

Expand Down Expand Up @@ -187,7 +186,7 @@ enum RealmValueType {
bool get isCollection => this == RealmValueType.list || this == RealmValueType.map;
}

/// A type that can represent any valid realm data type, except collections and embedded objects.
/// A type that can represent any valid realm data type, except embedded objects.
///
/// You can use [RealmValue] to declare fields on realm models, in which case it must be non-nullable,
/// but it can wrap a null-value. List of [RealmValue] (`List<RealmValue>`) are also legal.
Expand Down Expand Up @@ -278,21 +277,29 @@ class RealmValue {
}

@override
operator ==(Object? other) {
operator ==(Object other) {
// TODO(kn):
// Should we not start by testing for identical? Will break current
// test, but I think the tests are wrong
//if (identical(this, other)) return true;

// We always return false when comparing two RealmValue collections.
if (type.isCollection) {
return false;
}

final v = value;

if (other is RealmValue) {
if (value is Uint8List && other.value is Uint8List) {
return ListEquality().equals(value as Uint8List, other.value as Uint8List);
final ov = other.value;
if (v is Uint8List && ov is Uint8List) {
return memEquals(v, ov);
}

return type == other.type && value == other.value;
return type == other.type && v == ov;
}

return value == other;
return v == other;
}

@override
Expand All @@ -302,6 +309,28 @@ class RealmValue {
String toString() => 'RealmValue($value)';
}

/// Compares two [Uint8List]s by comparing 8 bytes at a time.
bool memEquals(Uint8List x, Uint8List y) {
nielsenko marked this conversation as resolved.
Show resolved Hide resolved
if (identical(x, y)) return true;
if (x.lengthInBytes != y.lengthInBytes) return false;

var words = x.lengthInBytes ~/ 8; // number of full words
var xW = x.buffer.asUint64List(0, words);
var yW = y.buffer.asUint64List(0, words);

// compare words
for (var i = 0; i < xW.length; i += 1) {
if (xW[i] != yW[i]) return false;
}

// compare remaining bytes
for (var i = words * 8; i < x.lengthInBytes; i += 1) {
if (x[i] != y[i]) return false;
}

return true; // no diff, they are equal
}

/// A base type for the supported geospatial shapes.
sealed class GeoShape {}

Expand Down
52 changes: 49 additions & 3 deletions packages/realm_dart/test/realm_value_test.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// Copyright 2022 MongoDB, Inc.
// SPDX-License-Identifier: Apache-2.0

import 'dart:math';
import 'dart:typed_data';

import 'package:test/test.dart' hide test, throws;
import 'package:collection/collection.dart';
import 'package:realm_common/realm_common.dart' show memEquals;
import 'package:realm_dart/realm.dart';
import 'package:test/test.dart' hide test, throws;

import 'test.dart';
import 'session_test.dart' show validateSessionStates;
import 'test.dart';

part 'realm_value_test.realm.dart';

Expand Down Expand Up @@ -60,7 +63,7 @@ void main() {
ObjectId.fromHexString('64c13ab08edf48a008793cac'),
Uuid.fromString('7a459a5e-5eb6-45f6-9b72-8f794e324105'),
Decimal128.fromDouble(128.128),
Uint8List.fromList([1, 2, 0])
Uint8List.fromList(List.generate(21, (index) => index)), // 21 % 8 != 0 so not a multiple of 8 bytes
];

for (final x in primitiveValues) {
Expand Down Expand Up @@ -2418,4 +2421,47 @@ void main() {
expect(result.value, isNull);
});
});

test('memEquals', () {
const samples = 100;
const max = 100000;
const half = max ~/ 2;
final listEquals = ListEquality<int>().equals;

final memEqualsClock = Stopwatch();
final listEqualsClock = Stopwatch();
for (int i = 0; i < samples; i++) {
final rnd = Random(i);
final length = rnd.nextInt(half) + half; // 50-100 KB.
final list = List.generate(length, (index) => index)..shuffle(rnd);
final bin1 = Uint8List.fromList(list);
final bin2 = Uint8List.fromList(list);

// Check correctness.
expect(identical(bin1, bin2), isFalse); // Different instances.
expect(listEquals(bin1, bin2), isTrue); // Sanity check.
expect(memEquals(bin1, bin2), isTrue); // Check correctness.
expect(RealmValue.from(bin1), RealmValue.from(bin2));

// Measure performance.
listEqualsClock.start();
final resultListEquals = listEquals(bin1, bin2);
listEqualsClock.stop();

memEqualsClock.start();
final resultMemEquals = memEquals(bin1, bin2);
memEqualsClock.stop();

expect(resultListEquals, resultMemEquals); // Sanity check.

bin2[rnd.nextInt(bin2.length)]--; // Change one byte.

expect(listEquals(bin1, bin2), isFalse); // Sanity check.
expect(memEquals(bin1, bin2), isFalse); // Check correctness.
expect(RealmValue.from(bin1), isNot(RealmValue.from(bin2)));
}

// Not quite 8 times speedup, but close enough.
expect(listEqualsClock.elapsedTicks ~/ 5, greaterThan(memEqualsClock.elapsedTicks));
});
}
Loading