Подтвердить что ты не робот

Как улучшить эффективность Dart преобразования данных в/из двоичного?

Выполняя некоторые консалтинговые работы для более крупных немецких компаний Future Technologies Group, я портировал около 6000 строк программного обеспечения на стороне сервера Java на Dart. Это должно помочь ответить на вопрос, может ли Dart эффективно использоваться на сервере. (Что само по себе дало бы зеленый свет для Дарта из-за поиска в пользу наличия одного языка для программирования на стороне клиента и на стороне сервера.)

Изучение Dart (с которым мне действительно понравилось работать) дало мне надежду на снижение производительности на 30-50% относительно Java, но в любом случае не хуже 100% (в два раза медленнее), что является отсечкой для вышеупомянутый процесс принятия решений.

Порт прошел гладко. Я многому научился. Единичные тесты были прекрасными. Но производительность оказалась крайне плохой... СЕМЬ времени медленнее по сравнению с Java-программой.

Профилирование кода выявило двух основных виновников: преобразование данных и ввода-вывода файлов. Может быть, я делаю что-то не так? Прежде чем я вернусь к своему клиенту, и они отменили исследование Дарта, я хотел бы найти несколько советов о том, как улучшить ситуацию. Позвольте начать с преобразования данных, преобразование собственных типов данных Dart в различные двоичные форматы, которые могут быть использованы для эффективной передачи и хранения данных.

Обычно эти преобразования просты и очень быстры, потому что ничего не нужно переводить из используемого внутреннего формата, но в основном хранятся в буфере. Я создал тестовую программу, которая каким-то образом отражает типичное использование этих преобразований в моей программе:

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

// Create a new benchmark by extending BenchmarkBase
class ConversionBenchmark extends BenchmarkBase {

  Uint8List result;

  ConversionBenchmark() : super("Conversion");

  // The benchmark code.
  void run() {
    const int BufSize = 262144; // 256kBytes
    const int SetSize = 64;     // one "typical" set of data, gets repeated
    ByteData buffer = new ByteData(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 < buffer.lengthInBytes / SetSize; j++) {
      // The following represents some "typical" conversion mix:
      buffer.setFloat64(offset, doubleContent); offset += 8; doubleContent += 0.123;
      for (int k = 0; k < 8; k++) { // main use case
        buffer.setFloat32(offset, doubleContent); offset += 4; doubleContent += 0.123;
      }
      buffer.setInt32(offset, intContent); offset += 4; intContent++;
      buffer.setInt32(offset, intContent); offset += 4; intContent++;
      buffer.setInt16(offset, intContent); offset += 2; intContent++;
      buffer.setInt16(offset, intContent); offset += 2; intContent++;
      buffer.setInt8(offset, intContent); offset += 1; intContent++;
      buffer.setInt8(offset, intContent); offset += 1; intContent++;
      buffer.buffer.asUint8List(offset).setAll(0, "AsciiStrng".codeUnits); offset += 10;
        // [ByteData] knows no other mechanism to transfer ASCII strings in
      assert((offset % SetSize) == 0); // ensure the example content fits [SetSize] bytes
    }
    result = buffer.buffer.asUint8List(); // only this can be used for further processing
  }
}

main() {
  new ConversionBenchmark().report();
}

Он основан на контрольной привязке от https://github.com/dart-lang/benchmark_harness. Для сравнения я использовал следующую Java-программу, основанную на порте тестовой привязки Dart от https://github.com/bono8106/benchmark_harness_java:

package ylib.tools;

import java.nio.ByteBuffer;

public class ConversionBenchmark extends BenchmarkBase {

  public ByteBuffer result;

  public ConversionBenchmark() { super("Conversion"); }

  // The benchmark code.
  @Override protected void run() {
    final int BufSize = 262144; // 256kBytes
    final int SetSize = 64;     // one "typical" set of data, gets repeated
    ByteBuffer buffer = ByteBuffer.allocate(BufSize);
    double doubleContent = 0.0; // used to simulate double content
    int intContent = 0;         // used to simulate int content
    for (int j = 0; j < (buffer.capacity() / SetSize); j++) {
      // The following represents some "typical" conversion mix:
      buffer.putDouble(doubleContent); doubleContent += 0.123;
      for (int k = 0; k < 8; k++) { // main use case
        buffer.putFloat((float)doubleContent); doubleContent += 0.123;
      }
      buffer.putInt(intContent); intContent++;
      buffer.putInt(intContent); intContent++;
      buffer.putShort((short)intContent); intContent++;
      buffer.putShort((short)intContent); intContent++;
      buffer.put((byte)intContent); intContent++;
      buffer.put((byte)intContent); intContent++;
      buffer.put("AsciiStrng".getBytes());
      //assert((buffer.position() % SetSize) == 0); // ensure the example content fits [SetSize] bytes
    }
    buffer.flip(); // needed for further processing
    result = buffer; // to avoid the compiler optimizing away everything
  }

  public static void main(String[] args) {
    new ConversionBenchmark().report();
  }
}

Код Java работает почти в 10 раз быстрее, чем код Dart на моей машине Intel Windows 7. Оба запускаются в режиме производства на своих виртуальных машинах.

Есть ли явная ошибка в коде? Или существуют различные классы Дарт, которые могут выполнять эту работу? Любое объяснение того, почему Дарт настолько медленнее с этими простыми преобразованиями? Или я полностью ошибаюсь в отношении производительности Dart VM?

4b9b3361

Ответ 1

Верно, что производительность методов байтовых данных (ByteData.setXYZ и ByteData.getXYZ) довольно плохая для Dart VM по сравнению с прямым доступом к типизированному массиву. Мы начали работу над проблемой, и исходные результаты обещают [1].

В то же время вы можете обойти эту неудачную регрессию производительности, скопировав свое собственное преобразование в большой endian с помощью типизированных массивов (полный код в [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;
  }
}

Бенчмаркинг этого ручного преобразования на вашем тестировании дает около 6-кратного улучшения:

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

Ответ 2

Я хочу добавить некоторые сведения о том, как я, наконец, решил проблему производительности и как выглядят результаты.

Сначала я использовал postet by by Vyacheslav Egorov и разработал из него свой собственный класс конвертера данных, который обеспечивает конверсии в обоих направлениях. Это все еще не производственный код, но он очень хорошо работал для моего сервера программного обеспечения, поэтому я добавил его ниже. Я намеренно сохранил [buffer] общедоступную переменную. Это может не обеспечить идеальную инкапсуляцию, но позволяет легко записывать и считывать из буфера, например. через [RandomAccessFile.readInto] и [RandomAccessFile.writeFrom]. Все простые и эффективные!

На самом деле оказалось, что эти преобразования данных, где виновник медленной начальной производительности в семь раз медленнее, чем версия Java. С изменением разрыв в производительности значительно сократился. Версия Dart серверного приложения 6000 строк теперь отслеживает версию Java примерно на 30%. Лучше, чем я ожидал от языка с такой гибкой концепцией набора текста. Это оставит Дарта в хорошем положении для моих будущих решений в области технологий.

В моих глазах наличие одного языка для клиентских и серверных приложений может быть очень хорошим аргументом для Dart.

И вот код для конвертера данных, который используется для этого проекта:

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;
  }
}

И очень маленькая и базовая тестовая единица:

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);
  });
}