How to improve Dart performance of data conversion to/from binary? How to improve Dart performance of data conversion to/from binary? dart dart

How to improve Dart performance of data conversion to/from binary?


It is true that performance of byte data methods (ByteData.setXYZ and ByteData.getXYZ) is pretty bad on Dart VM compared to direct typed array access. We started working on the issue and initial results are promising[1].

In the mean time you can work around this unfortunate performance regression by rolling your own conversion to big endian using typed arrays (full code at[2]):

/// Writer wraps a fixed size Uint8List and writes values into it using/// big-endian byte order.class Writer {  /// Output buffer.  final Uint8List out;  /// Current position within [out].  var position = 0;  Writer._create(this.out);  factory Writer(size) {    final out = new Uint8List(size);    if (Endianness.HOST_ENDIAN == Endianness.LITTLE_ENDIAN) {      return new _WriterForLEHost._create(out);    } else {      return new _WriterForBEHost._create(out);    }  }  writeFloat64(double v);}/// Lists used for data convertion (alias each other).final Uint8List _convU8 = new Uint8List(8);final Float32List _convF32 = new Float32List.view(_convU8.buffer);final Float64List _convF64 = new Float64List.view(_convU8.buffer);/// Writer used on little-endian host.class _WriterForLEHost extends Writer {  _WriterForLEHost._create(out) : super._create(out);  writeFloat64(double v) {    _convF64[0] = v;    out[position + 7] = _convU8[0];    out[position + 6] = _convU8[1];    out[position + 5] = _convU8[2];    out[position + 4] = _convU8[3];    out[position + 3] = _convU8[4];    out[position + 2] = _convU8[5];    out[position + 1] = _convU8[6];    out[position + 0] = _convU8[7];    position += 8;  }}

Benchmarking this manual conversion on your test yields around 6x improvement:

import 'dart:typed_data';import 'package:benchmark_harness/benchmark_harness.dart';import 'writer.dart';class ConversionBenchmarkManual extends BenchmarkBase {  Uint8List result;  ConversionBenchmarkManual() : super("Conversion (MANUAL)");  // The benchmark code.  void run() {    const int BufSize = 262144; // 256kBytes    const int SetSize = 64;     // one "typical" set of data, gets repeated    final w = new Writer(BufSize);    double doubleContent = 0.0; // used to simulate double content    int intContent = 0;         // used to simulate int content    int offset = 0;    for (int j = 0; j < (BufSize / SetSize); j++) {      // The following represents some "typical" conversion mix:      w.writeFloat64(doubleContent); doubleContent += 0.123;      for (int k = 0; k < 8; k++) { // main use case        w.writeFloat32(doubleContent); doubleContent += 0.123;      }      w.writeInt32(intContent); intContent++;      w.writeInt32(intContent); intContent++;      w.writeInt16(intContent); intContent++;      w.writeInt16(intContent); intContent++;      w.writeInt8(intContent);  intContent++;      w.writeInt8(intContent);  intContent++;      w.writeString("AsciiStrng");      assert((offset % SetSize) == 0); // ensure the example content fits [SetSize] bytes    }    result = w.out; // only this can be used for further processing  }}

[1] https://code.google.com/p/dart/issues/detail?id=22107

[2] https://gist.github.com/mraleph/4eb5ccbb38904075141e


I want to add some details on how I have finally solved the performance problem and how the results look like.

First I used the approach postet by Vyacheslav Egorov and developed from it my own data converter class which provides conversions in both directions. It is still not production code but it worked very well for my server software port and therefore I have appended it below. I intentionally kept the [buffer] a public variable. This might not make for a perfect encapsulation but allows for an easy direct write into and read from the buffer, e.g. via a [RandomAccessFile.readInto] and [RandomAccessFile.writeFrom]. All plain and efficient!

It really turned out that these data conversions where the culprit for the slow initial performance of seven times slower than the Java version. With the change the performance gap narrowed considerably. The Dart version of the 6000 lines server application now trails the Java version by just about 30%. Better than I expected from a language with such a flexible typing concept. That will leave Dart in good position for my customers future technology decisions.

In my eyes having one language for client and server applications could be one very good argument for Dart.

And here comes the code for the data converter as used for this project:

part of ylib;/// [DataConverter] wraps a fixed size [Uint8List] and converts values from and into it/// using big-endian byte order.///abstract class DataConverter {  /// Buffer.  final Uint8List buffer;  /// Current position within [buffer].  int _position = 0;  DataConverter._create(this.buffer);  /// Creates the converter with its associated [buffer].  ///  factory DataConverter(size) {    final out = new Uint8List(size);    if (Endianness.HOST_ENDIAN == Endianness.LITTLE_ENDIAN) {      return new _ConverterForLEHost._create(out);    } else {      return new _ConverterForBEHost._create(out);    }  }  int get length => buffer.length;  int get position => _position;  set position(int position) {    if ((position < 0) || (position > buffer.lengthInBytes)) throw new ArgumentError(position);    _position = position;  }  double getFloat64();  putFloat64(double v);  double getFloat32();  putFloat32(double v);  static const int _MaxSignedInt64plus1 = 9223372036854775808;  static const int _MaxSignedInt32plus1 = 2147483648;  static const int _MaxSignedInt16plus1 = 32768;  static const int _MaxSignedInt8plus1 = 128;  int getInt64() {    int v =      buffer[_position + 7] | (buffer[_position + 6] << 8) | (buffer[_position + 5] << 16) |      (buffer[_position + 4] << 24) | (buffer[_position + 3] << 32) |      (buffer[_position + 2] << 40) | (buffer[_position + 1] << 48) | (buffer[_position] << 56);    _position += 8;    if (v >= _MaxSignedInt64plus1) v -= 2 * _MaxSignedInt64plus1;    return v;  }  putInt64(int v) {    assert((v < _MaxSignedInt64plus1) && (v >= -_MaxSignedInt64plus1));    buffer[_position + 7] = v;    buffer[_position + 6] = (v >> 8);    buffer[_position + 5] = (v >> 16);    buffer[_position + 4] = (v >> 24);    buffer[_position + 3] = (v >> 32);    buffer[_position + 2] = (v >> 40);    buffer[_position + 1] = (v >> 48);    buffer[_position + 0] = (v >> 56);    _position += 8;  }  int getInt32() {    int v = buffer[_position + 3] | (buffer[_position + 2] << 8) | (buffer[_position + 1] << 16) |            (buffer[_position] << 24);    _position += 4;    if (v >= _MaxSignedInt32plus1) v -= 2 * _MaxSignedInt32plus1;    return v;  }  putInt32(int v) {    assert((v < _MaxSignedInt32plus1) && (v >= -_MaxSignedInt32plus1));    buffer[_position + 3] = v;    buffer[_position + 2] = (v >> 8);    buffer[_position + 1] = (v >> 16);    buffer[_position + 0] = (v >> 24);    _position += 4;  }//  The following code which uses the 'double' conversion methods works but is about 50% slower!////  final Int32List _convI32 = new Int32List.view(_convU8.buffer);////  int getInt32() {//    _convU8[0] = out[_position + 0]; _convU8[1] = out[_position + 1];//    _convU8[2] = out[_position + 2]; _convU8[3] = out[_position + 3];//    _position += 4;//    return _convI32[0];//  }////  putInt32(int v) {//    _convI32[0] = v;//    out[_position + 0] = _convU8[0]; out[_position + 1] = _convU8[1];//    out[_position + 2] = _convU8[2]; out[_position + 3] = _convU8[3];//    _position += 4;//  }  int getInt16() {    int v = buffer[_position + 1] | (buffer[_position] << 8);    _position += 2;    if (v >= _MaxSignedInt16plus1) v -= 2 * _MaxSignedInt16plus1;    return v;  }  putInt16(int v) {    assert((v < _MaxSignedInt16plus1) && (v >= -_MaxSignedInt16plus1));    buffer[_position + 1] = v;    buffer[_position + 0] = (v >> 8);    _position += 2;  }  int getInt8() {    int v = buffer[_position++];    if (v >= _MaxSignedInt8plus1) v -= 2 * _MaxSignedInt8plus1;    return v;  }  putInt8(int v) {    assert((v < _MaxSignedInt8plus1) && (v >= -_MaxSignedInt8plus1));    buffer[_position] = v;    _position++;  }  String getString(int length) {    String s = new String.fromCharCodes(buffer, _position, _position + length);    _position += length;    return s;  }  putString(String str) {    buffer.setAll(_position, str.codeUnits);    _position += str.codeUnits.length;  }}/// Lists used for data convertion (alias each other).final Uint8List _convU8 = new Uint8List(8);final Float32List _convF32 = new Float32List.view(_convU8.buffer);final Float64List _convF64 = new Float64List.view(_convU8.buffer);/// Writer used on little-endian host.class _ConverterForLEHost extends DataConverter {  _ConverterForLEHost._create(out) : super._create(out);  double getFloat64() {    _convU8[0] = buffer[_position + 7]; _convU8[1] = buffer[_position + 6];    _convU8[2] = buffer[_position + 5]; _convU8[3] = buffer[_position + 4];    _convU8[4] = buffer[_position + 3]; _convU8[5] = buffer[_position + 2];    _convU8[6] = buffer[_position + 1]; _convU8[7] = buffer[_position + 0];    _position += 8;    return _convF64[0];  }  putFloat64(double v) {    _convF64[0] = v;    buffer[_position + 7] = _convU8[0]; buffer[_position + 6] = _convU8[1];    buffer[_position + 5] = _convU8[2]; buffer[_position + 4] = _convU8[3];    buffer[_position + 3] = _convU8[4]; buffer[_position + 2] = _convU8[5];    buffer[_position + 1] = _convU8[6]; buffer[_position + 0] = _convU8[7];    _position += 8;  }  double getFloat32() {    _convU8[0] = buffer[_position + 3]; _convU8[1] = buffer[_position + 2];    _convU8[2] = buffer[_position + 1]; _convU8[3] = buffer[_position + 0];    _position += 4;    return _convF32[0];  }  putFloat32(double v) {    _convF32[0] = v;    assert(_convF32[0].isFinite || !v.isFinite); // overflow check    buffer[_position + 3] = _convU8[0]; buffer[_position + 2] = _convU8[1];    buffer[_position + 1] = _convU8[2]; buffer[_position + 0] = _convU8[3];    _position += 4;  }}/// Writer used on the big-endian host.class _ConverterForBEHost extends DataConverter {  _ConverterForBEHost._create(out) : super._create(out);  double getFloat64() {    _convU8[0] = buffer[_position + 0]; _convU8[1] = buffer[_position + 1];    _convU8[2] = buffer[_position + 2]; _convU8[3] = buffer[_position + 3];    _convU8[4] = buffer[_position + 4]; _convU8[5] = buffer[_position + 5];    _convU8[6] = buffer[_position + 6]; _convU8[7] = buffer[_position + 7];    _position += 8;    return _convF64[0];  }  putFloat64(double v) {    _convF64[0] = v;    buffer[_position + 0] = _convU8[0]; buffer[_position + 1] = _convU8[1];    buffer[_position + 2] = _convU8[2]; buffer[_position + 3] = _convU8[3];    buffer[_position + 4] = _convU8[4]; buffer[_position + 5] = _convU8[5];    buffer[_position + 6] = _convU8[6]; buffer[_position + 7] = _convU8[7];    _position += 8;  }  double getFloat32() {    _convU8[0] = buffer[_position + 0]; _convU8[1] = buffer[_position + 1];    _convU8[2] = buffer[_position + 2]; _convU8[3] = buffer[_position + 3];    _position += 4;    return _convF32[0];  }  putFloat32(double v) {    _convF32[0] = v;    assert(_convF32[0].isFinite || !v.isFinite); // overflow check    buffer[_position + 0] = _convU8[0]; buffer[_position + 1] = _convU8[1];    buffer[_position + 2] = _convU8[2]; buffer[_position + 3] = _convU8[3];    _position += 4;  }}

And a very small and basic test unit:

import 'package:ylib/ylib.dart';import 'package:unittest/unittest.dart';// -------- Test program for [DataConverter]: --------void main() {  DataConverter dc = new DataConverter(100);  test('Float64', () {    double d1 = 1.246e370, d2 = -0.0000745687436849437;    dc.position = 0;    dc..putFloat64(d1)..putFloat64(d2);    dc.position = 0; // reset it    expect(dc.getFloat64(), d1);    expect(dc.getFloat64(), d2);  });  test('Float32', () {    double d1 = -0.43478e32, d2 = -0.0;    dc.position = 0;    dc..putFloat32(d1)..putFloat32(d2);    dc.position = 0; // reset it    expect(dc.getFloat32(), closeTo(d1, 1.7e24));    expect(dc.getFloat32(), d2);  });  test('Int64', () {    int i1 = 9223372036854775807, i2 = -22337203685477580;    dc.position = 3;    dc..putInt64(i1)..putInt64(i2);    dc.position = 3; // reset it    expect(dc.getInt64(), i1);    expect(dc.getInt64(), i2);  });  test('Int32_16_8', () {    int i1 = 192233720, i2 = -7233, i3 = 32, i4 = -17;    dc.position = 0;    dc..putInt32(i1)..putInt16(i2)..putInt8(i3)..putInt32(i4);    dc.position = 0; // reset it    expect(dc.getInt32(), i1);    expect(dc.getInt16(), i2);    expect(dc.getInt8(), i3);    expect(dc.getInt32(), i4);  });  test('String', () {    String s1 = r"922337203!ยง$%&()=?68547/\75807", s2 = "-22337203685477580Anton";    int i1 = -33;    dc.position = 33;    dc..putString(s1)..putInt8(i1)..putString(s2);    dc.position = 33; // reset it    expect(dc.getString(s1.length), s1);    expect(dc.getInt8(), i1);    expect(dc.getString(s2.length), s2);  });}