Skip to content

Commit

Permalink
Merge pull request #242 from objectbox/fb-read-speedup
Browse files Browse the repository at this point in the history
Read speedup
  • Loading branch information
vaind authored May 17, 2021
2 parents 0523f3d + d57c303 commit 6629058
Show file tree
Hide file tree
Showing 17 changed files with 247 additions and 167 deletions.
41 changes: 39 additions & 2 deletions benchmark/bin/native_pointers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:ffi';
import 'dart:typed_data';

import 'package:ffi/ffi.dart';
import 'package:objectbox/src/native/bindings/nativemem.dart';
import 'package:objectbox_benchmark/benchmark.dart';

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

void main() async {
final sizeInBytes = 256;
final sizeInBytes = 1024;
await AsTypedList(sizeInBytes).report();

// just checking if using a larger underlying type would help with anything
await AsTypedListUint64((sizeInBytes / 8).floor()).report();
// await AsTypedListUint64((sizeInBytes / 8).floor()).report();

await TypedListMemCopy(sizeInBytes).report();
}

class AsTypedList extends Benchmark {
Expand Down Expand Up @@ -73,3 +76,37 @@ class AsTypedListUint64 extends Benchmark {
super.teardown();
}
}

class TypedListMemCopy extends Benchmark {
final int length;
late final Pointer<Uint8> nativePtr;
late final Pointer<Uint8> nativePtr2;
late final ByteBuffer buffer;
late final ByteData data;

TypedListMemCopy(this.length)
: super('${TypedListMemCopy}', iterations: 1000);

@override
void runIteration(int i) {
memcpy(nativePtr, nativePtr2, length);
ByteData.view(buffer, length);
// actually using the data (read flatbuffers) doesn't matter here
}

@override
void setup() {
nativePtr = malloc<Uint8>(length);
nativePtr2 = malloc<Uint8>(length);
assert(nativePtr.asTypedList(length).offsetInBytes == 0);
buffer = nativePtr.asTypedList(length).buffer;
data = ByteData.view(buffer, 0);
}

@override
void teardown() {
malloc.free(nativePtr);
malloc.free(nativePtr2);
super.teardown();
}
}
4 changes: 2 additions & 2 deletions generator/lib/src/code_chunks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,8 @@ class CodeChunks {
}
});

return '''(Store store, Uint8List fbData) {
final buffer = fb.BufferContext.fromBytes(fbData);
return '''(Store store, ByteData fbData) {
final buffer = fb.BufferContext(fbData);
final rootOffset = buffer.derefObject(0);
${preLines.join('\n')}
final object = ${entity.name}(${constructorLines.join(', \n')})${cascadeLines.join('\n')};
Expand Down
5 changes: 3 additions & 2 deletions objectbox/lib/flatbuffers/flat_buffers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ class BufferContext {
factory BufferContext.fromBytes(List<int> byteList) {
Uint8List uint8List = _asUint8List(byteList);
ByteData buf = new ByteData.view(uint8List.buffer, uint8List.offsetInBytes);
return new BufferContext._(buf);
return new BufferContext(buf);
}

BufferContext._(this._buffer);
BufferContext(this._buffer);

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

class Int64Reader extends Reader<int> {
const Int64Reader() : super();

@override
int get size => _sizeofInt64;

Expand Down
2 changes: 1 addition & 1 deletion objectbox/lib/src/modelinfo/entity_definition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import 'modelentity.dart';
class EntityDefinition<T> {
final ModelEntity model;
final int Function(T, fb.Builder) objectToFB;
final T Function(Store, Uint8List) objectFromFB;
final T Function(Store, ByteData) objectFromFB;
final int? Function(T) getId;
final void Function(T, int) setId;
final List<ToOne> Function(T) toOneRelations;
Expand Down
3 changes: 2 additions & 1 deletion objectbox/lib/src/native/bindings/data_visitor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Pointer<NativeFunction<obx_data_visitor>> dataVisitor(
Pointer<NativeFunction<obx_data_visitor>> objectCollector<T>(
List<T> list, Store store, EntityDefinition<T> entity) =>
dataVisitor((Pointer<Uint8> data, int size) {
list.add(entity.objectFromFB(store, data.asTypedList(size)));
list.add(entity.objectFromFB(
store, InternalStoreAccess.reader(store).access(data, size)));
return true;
});
60 changes: 33 additions & 27 deletions objectbox/lib/src/native/bindings/flatbuffers.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import 'dart:ffi';
import 'dart:io' show Platform;
import 'dart:typed_data';

import 'package:ffi/ffi.dart';

import '../../../flatbuffers/flat_buffers.dart' as fb;
import 'nativemem.dart';

// ignore_for_file: public_member_api_docs

Expand Down Expand Up @@ -42,12 +42,6 @@ class BuilderWithCBuffer {
Allocator get allocator => _allocator;
}

// FFI signature
typedef _dart_memset = void Function(Pointer<Uint8>, int, int);
typedef _c_memset = Void Function(Pointer<Uint8>, Int32, IntPtr);

_dart_memset? fbMemset;

class Allocator extends fb.Allocator {
// We may, in practice, have only two active allocations: one used and one
// for resizing. Therefore, we use a ring buffer of a fixed size (2).
Expand Down Expand Up @@ -98,34 +92,46 @@ class Allocator extends fb.Allocator {
void clear(ByteData data, bool isFresh) {
if (isFresh) return; // freshly allocated data is zero-ed out (see [calloc])

if (fbMemset == null) {
if (Platform.isWindows) {
try {
// DynamicLibrary.process() is not available on Windows, let's load a
// lib that defines 'memset()' it - should be mscvr100 or mscvrt DLL.
// mscvr100.dll is in the frequently installed MSVC Redistributable.
fbMemset = DynamicLibrary.open('msvcr100.dll')
.lookupFunction<_c_memset, _dart_memset>('memset');
} catch (_) {
// fall back if we can't load a native memset()
fbMemset = (Pointer<Uint8> ptr, int byte, int size) =>
ptr.cast<Uint8>().asTypedList(size).fillRange(0, size, byte);
}
} else {
fbMemset = DynamicLibrary.process()
.lookupFunction<_c_memset, _dart_memset>('memset');
}
}

// only used for sanity checks:
assert(_data[_index] == data);
assert(_allocs[_index].address != 0);

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

void freeAll() {
if (_allocs[0].address != 0) calloc.free(_allocs[0]);
if (_allocs[1].address != 0) calloc.free(_allocs[1]);
}
}

/// Implements a native data access wrapper to circumvent Pointer.asTypedList()
/// slowness. The idea is to reuse the same buffer and rather memcpy the data,
/// which ends up being faster than calling asTypedList(). Hopefully, we will
/// be able to remove this if (when) asTypedList() gets optimized in Dart SDK.
class ReaderWithCBuffer {
// See /benchmark/bin/native_pointers.dart for the max buffer size where it
// still makes sense to use memcpy. On Linux, memcpy starts to be slower at
// about 10-15 KiB. TODO test on other platforms to find an optimal limit.
static const _maxBuffer = 4 * 1024;
final _bufferPtr = malloc<Uint8>(_maxBuffer);
late final ByteBuffer _buffer = _bufferPtr.asTypedList(_maxBuffer).buffer;

ReaderWithCBuffer() {
assert(_bufferPtr.asTypedList(_maxBuffer).offsetInBytes == 0);
}

void clear() => malloc.free(_bufferPtr);

ByteData access(Pointer<Uint8> dataPtr, int size) {
if (size > _maxBuffer) {
final uint8List = dataPtr.asTypedList(size);
return ByteData.view(uint8List.buffer, uint8List.offsetInBytes, size);
} else {
memcpy(_bufferPtr, dataPtr, size);
return ByteData.view(_buffer, 0, size);
}
}
}
11 changes: 6 additions & 5 deletions objectbox/lib/src/native/bindings/helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import '../../common.dart';
import '../../modelinfo/entity_definition.dart';
import '../store.dart';
import 'bindings.dart';
import 'flatbuffers.dart';

// ignore_for_file: public_member_api_docs

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

final bool _isWrite;
late final Pointer<Pointer<Void>> dataPtrPtr;
late final Pointer<Pointer<Uint8>> dataPtrPtr;

late final Pointer<IntPtr> sizePtr;

Expand All @@ -106,8 +108,7 @@ class CursorHelper<T> {
}
}

Uint8List get readData =>
dataPtrPtr.value.cast<Uint8>().asTypedList(sizePtr.value);
ByteData get readData => _reader.access(dataPtrPtr.value, sizePtr.value);

EntityDefinition<T> get entity => _entity;

Expand All @@ -131,13 +132,13 @@ class CursorHelper<T> {
}

T withNativeBytes<T>(
Uint8List data, T Function(Pointer<Void> ptr, int size) fn) {
Uint8List data, T Function(Pointer<Uint8> ptr, int size) fn) {
final size = data.length;
assert(size == data.lengthInBytes);
final ptr = malloc<Uint8>(size);
try {
ptr.asTypedList(size).setAll(0, data); // copies `data` to `ptr`
return fn(ptr.cast<Void>(), size);
return fn(ptr, size);
} finally {
malloc.free(ptr);
}
Expand Down
25 changes: 25 additions & 0 deletions objectbox/lib/src/native/bindings/nativemem.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'dart:ffi';
import 'dart:io';

/// Provides native memory manipulation, operating on FFI Pointer<Void>.
/// memset(ptr, value, num) sets the first num bytes of the block of memory
/// pointed by ptr to the specified value (interpreted as an uint8).
final _dart_memset memset =
_stdlib.lookupFunction<_c_memset, _dart_memset>('memset');

/// memcpy (destination, source, num) copies the values of num bytes from the
/// data pointed to by source to the memory block pointed to by destination.
final _dart_memcpy memcpy =
_stdlib.lookupFunction<_c_memcpy, _dart_memcpy>('memcpy');

// FFI signature
typedef _dart_memset = void Function(Pointer<Uint8>, int, int);
typedef _c_memset = Void Function(Pointer<Uint8>, Int32, IntPtr);

typedef _dart_memcpy = void Function(Pointer<Uint8>, Pointer<Uint8>, int);
typedef _c_memcpy = Void Function(Pointer<Uint8>, Pointer<Uint8>, IntPtr);

final DynamicLibrary _stdlib = Platform.isWindows // no .process() on windows
? DynamicLibrary.open('vcruntime140.dll') // required by objectbox.dll
: DynamicLibrary.process();
Loading

0 comments on commit 6629058

Please sign in to comment.