Skip to content

Commit 6629058

Browse files
authored
Merge pull request #242 from objectbox/fb-read-speedup
Read speedup
2 parents 0523f3d + d57c303 commit 6629058

17 files changed

+247
-167
lines changed

benchmark/bin/native_pointers.dart

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:ffi';
22
import 'dart:typed_data';
33

44
import 'package:ffi/ffi.dart';
5+
import 'package:objectbox/src/native/bindings/nativemem.dart';
56
import 'package:objectbox_benchmark/benchmark.dart';
67

78
// Results (Dart SDK 2.12):
@@ -19,11 +20,13 @@ import 'package:objectbox_benchmark/benchmark.dart';
1920
// which is consistent with profiling objectbox-dart Box.read().
2021

2122
void main() async {
22-
final sizeInBytes = 256;
23+
final sizeInBytes = 1024;
2324
await AsTypedList(sizeInBytes).report();
2425

2526
// just checking if using a larger underlying type would help with anything
26-
await AsTypedListUint64((sizeInBytes / 8).floor()).report();
27+
// await AsTypedListUint64((sizeInBytes / 8).floor()).report();
28+
29+
await TypedListMemCopy(sizeInBytes).report();
2730
}
2831

2932
class AsTypedList extends Benchmark {
@@ -73,3 +76,37 @@ class AsTypedListUint64 extends Benchmark {
7376
super.teardown();
7477
}
7578
}
79+
80+
class TypedListMemCopy extends Benchmark {
81+
final int length;
82+
late final Pointer<Uint8> nativePtr;
83+
late final Pointer<Uint8> nativePtr2;
84+
late final ByteBuffer buffer;
85+
late final ByteData data;
86+
87+
TypedListMemCopy(this.length)
88+
: super('${TypedListMemCopy}', iterations: 1000);
89+
90+
@override
91+
void runIteration(int i) {
92+
memcpy(nativePtr, nativePtr2, length);
93+
ByteData.view(buffer, length);
94+
// actually using the data (read flatbuffers) doesn't matter here
95+
}
96+
97+
@override
98+
void setup() {
99+
nativePtr = malloc<Uint8>(length);
100+
nativePtr2 = malloc<Uint8>(length);
101+
assert(nativePtr.asTypedList(length).offsetInBytes == 0);
102+
buffer = nativePtr.asTypedList(length).buffer;
103+
data = ByteData.view(buffer, 0);
104+
}
105+
106+
@override
107+
void teardown() {
108+
malloc.free(nativePtr);
109+
malloc.free(nativePtr2);
110+
super.teardown();
111+
}
112+
}

generator/lib/src/code_chunks.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,8 +412,8 @@ class CodeChunks {
412412
}
413413
});
414414

415-
return '''(Store store, Uint8List fbData) {
416-
final buffer = fb.BufferContext.fromBytes(fbData);
415+
return '''(Store store, ByteData fbData) {
416+
final buffer = fb.BufferContext(fbData);
417417
final rootOffset = buffer.derefObject(0);
418418
${preLines.join('\n')}
419419
final object = ${entity.name}(${constructorLines.join(', \n')})${cascadeLines.join('\n')};

objectbox/lib/flatbuffers/flat_buffers.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ class BufferContext {
4444
factory BufferContext.fromBytes(List<int> byteList) {
4545
Uint8List uint8List = _asUint8List(byteList);
4646
ByteData buf = new ByteData.view(uint8List.buffer, uint8List.offsetInBytes);
47-
return new BufferContext._(buf);
47+
return new BufferContext(buf);
4848
}
4949

50-
BufferContext._(this._buffer);
50+
BufferContext(this._buffer);
5151

5252
@pragma('vm:prefer-inline')
5353
int derefObject(int offset) {
@@ -883,6 +883,7 @@ class Float32Reader extends Reader<double> {
883883

884884
class Int64Reader extends Reader<int> {
885885
const Int64Reader() : super();
886+
886887
@override
887888
int get size => _sizeofInt64;
888889

objectbox/lib/src/modelinfo/entity_definition.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import 'modelentity.dart';
1313
class EntityDefinition<T> {
1414
final ModelEntity model;
1515
final int Function(T, fb.Builder) objectToFB;
16-
final T Function(Store, Uint8List) objectFromFB;
16+
final T Function(Store, ByteData) objectFromFB;
1717
final int? Function(T) getId;
1818
final void Function(T, int) setId;
1919
final List<ToOne> Function(T) toOneRelations;

objectbox/lib/src/native/bindings/data_visitor.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Pointer<NativeFunction<obx_data_visitor>> dataVisitor(
3636
Pointer<NativeFunction<obx_data_visitor>> objectCollector<T>(
3737
List<T> list, Store store, EntityDefinition<T> entity) =>
3838
dataVisitor((Pointer<Uint8> data, int size) {
39-
list.add(entity.objectFromFB(store, data.asTypedList(size)));
39+
list.add(entity.objectFromFB(
40+
store, InternalStoreAccess.reader(store).access(data, size)));
4041
return true;
4142
});

objectbox/lib/src/native/bindings/flatbuffers.dart

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import 'dart:ffi';
2-
import 'dart:io' show Platform;
32
import 'dart:typed_data';
43

54
import 'package:ffi/ffi.dart';
65

76
import '../../../flatbuffers/flat_buffers.dart' as fb;
7+
import 'nativemem.dart';
88

99
// ignore_for_file: public_member_api_docs
1010

@@ -42,12 +42,6 @@ class BuilderWithCBuffer {
4242
Allocator get allocator => _allocator;
4343
}
4444

45-
// FFI signature
46-
typedef _dart_memset = void Function(Pointer<Uint8>, int, int);
47-
typedef _c_memset = Void Function(Pointer<Uint8>, Int32, IntPtr);
48-
49-
_dart_memset? fbMemset;
50-
5145
class Allocator extends fb.Allocator {
5246
// We may, in practice, have only two active allocations: one used and one
5347
// for resizing. Therefore, we use a ring buffer of a fixed size (2).
@@ -98,34 +92,46 @@ class Allocator extends fb.Allocator {
9892
void clear(ByteData data, bool isFresh) {
9993
if (isFresh) return; // freshly allocated data is zero-ed out (see [calloc])
10094

101-
if (fbMemset == null) {
102-
if (Platform.isWindows) {
103-
try {
104-
// DynamicLibrary.process() is not available on Windows, let's load a
105-
// lib that defines 'memset()' it - should be mscvr100 or mscvrt DLL.
106-
// mscvr100.dll is in the frequently installed MSVC Redistributable.
107-
fbMemset = DynamicLibrary.open('msvcr100.dll')
108-
.lookupFunction<_c_memset, _dart_memset>('memset');
109-
} catch (_) {
110-
// fall back if we can't load a native memset()
111-
fbMemset = (Pointer<Uint8> ptr, int byte, int size) =>
112-
ptr.cast<Uint8>().asTypedList(size).fillRange(0, size, byte);
113-
}
114-
} else {
115-
fbMemset = DynamicLibrary.process()
116-
.lookupFunction<_c_memset, _dart_memset>('memset');
117-
}
118-
}
119-
12095
// only used for sanity checks:
12196
assert(_data[_index] == data);
12297
assert(_allocs[_index].address != 0);
12398

124-
fbMemset!(_allocs[_index], 0, data.lengthInBytes);
99+
// TODO - there are other options to clear the builder, see how other
100+
// FlatBuffer implementations do it.
101+
memset(_allocs[_index], 0, data.lengthInBytes);
125102
}
126103

127104
void freeAll() {
128105
if (_allocs[0].address != 0) calloc.free(_allocs[0]);
129106
if (_allocs[1].address != 0) calloc.free(_allocs[1]);
130107
}
131108
}
109+
110+
/// Implements a native data access wrapper to circumvent Pointer.asTypedList()
111+
/// slowness. The idea is to reuse the same buffer and rather memcpy the data,
112+
/// which ends up being faster than calling asTypedList(). Hopefully, we will
113+
/// be able to remove this if (when) asTypedList() gets optimized in Dart SDK.
114+
class ReaderWithCBuffer {
115+
// See /benchmark/bin/native_pointers.dart for the max buffer size where it
116+
// still makes sense to use memcpy. On Linux, memcpy starts to be slower at
117+
// about 10-15 KiB. TODO test on other platforms to find an optimal limit.
118+
static const _maxBuffer = 4 * 1024;
119+
final _bufferPtr = malloc<Uint8>(_maxBuffer);
120+
late final ByteBuffer _buffer = _bufferPtr.asTypedList(_maxBuffer).buffer;
121+
122+
ReaderWithCBuffer() {
123+
assert(_bufferPtr.asTypedList(_maxBuffer).offsetInBytes == 0);
124+
}
125+
126+
void clear() => malloc.free(_bufferPtr);
127+
128+
ByteData access(Pointer<Uint8> dataPtr, int size) {
129+
if (size > _maxBuffer) {
130+
final uint8List = dataPtr.asTypedList(size);
131+
return ByteData.view(uint8List.buffer, uint8List.offsetInBytes, size);
132+
} else {
133+
memcpy(_bufferPtr, dataPtr, size);
134+
return ByteData.view(_buffer, 0, size);
135+
}
136+
}
137+
}

objectbox/lib/src/native/bindings/helpers.dart

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import '../../common.dart';
77
import '../../modelinfo/entity_definition.dart';
88
import '../store.dart';
99
import 'bindings.dart';
10+
import 'flatbuffers.dart';
1011

1112
// ignore_for_file: public_member_api_docs
1213

@@ -87,9 +88,10 @@ class CursorHelper<T> {
8788
final EntityDefinition<T> _entity;
8889
final Store _store;
8990
final Pointer<OBX_cursor> ptr;
91+
late final ReaderWithCBuffer _reader = InternalStoreAccess.reader(_store);
9092

9193
final bool _isWrite;
92-
late final Pointer<Pointer<Void>> dataPtrPtr;
94+
late final Pointer<Pointer<Uint8>> dataPtrPtr;
9395

9496
late final Pointer<IntPtr> sizePtr;
9597

@@ -106,8 +108,7 @@ class CursorHelper<T> {
106108
}
107109
}
108110

109-
Uint8List get readData =>
110-
dataPtrPtr.value.cast<Uint8>().asTypedList(sizePtr.value);
111+
ByteData get readData => _reader.access(dataPtrPtr.value, sizePtr.value);
111112

112113
EntityDefinition<T> get entity => _entity;
113114

@@ -131,13 +132,13 @@ class CursorHelper<T> {
131132
}
132133

133134
T withNativeBytes<T>(
134-
Uint8List data, T Function(Pointer<Void> ptr, int size) fn) {
135+
Uint8List data, T Function(Pointer<Uint8> ptr, int size) fn) {
135136
final size = data.length;
136137
assert(size == data.lengthInBytes);
137138
final ptr = malloc<Uint8>(size);
138139
try {
139140
ptr.asTypedList(size).setAll(0, data); // copies `data` to `ptr`
140-
return fn(ptr.cast<Void>(), size);
141+
return fn(ptr, size);
141142
} finally {
142143
malloc.free(ptr);
143144
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import 'dart:ffi';
2+
import 'dart:io';
3+
4+
/// Provides native memory manipulation, operating on FFI Pointer<Void>.
5+
6+
/// memset(ptr, value, num) sets the first num bytes of the block of memory
7+
/// pointed by ptr to the specified value (interpreted as an uint8).
8+
final _dart_memset memset =
9+
_stdlib.lookupFunction<_c_memset, _dart_memset>('memset');
10+
11+
/// memcpy (destination, source, num) copies the values of num bytes from the
12+
/// data pointed to by source to the memory block pointed to by destination.
13+
final _dart_memcpy memcpy =
14+
_stdlib.lookupFunction<_c_memcpy, _dart_memcpy>('memcpy');
15+
16+
// FFI signature
17+
typedef _dart_memset = void Function(Pointer<Uint8>, int, int);
18+
typedef _c_memset = Void Function(Pointer<Uint8>, Int32, IntPtr);
19+
20+
typedef _dart_memcpy = void Function(Pointer<Uint8>, Pointer<Uint8>, int);
21+
typedef _c_memcpy = Void Function(Pointer<Uint8>, Pointer<Uint8>, IntPtr);
22+
23+
final DynamicLibrary _stdlib = Platform.isWindows // no .process() on windows
24+
? DynamicLibrary.open('vcruntime140.dll') // required by objectbox.dll
25+
: DynamicLibrary.process();

0 commit comments

Comments
 (0)