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

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

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

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

Профилирование кода выявило двух основных виновников: преобразование данных и файловый ввод-вывод. Может я что не так делаю? Прежде чем я вернусь к своему клиенту, и он отменит свое исследование Dart, я хотел бы поискать несколько советов о том, как улучшить ситуацию. Начнем с преобразования данных, преобразования собственных типов данных 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 намного медленнее с этими простыми преобразованиями? Или у меня совершенно неверные ожидания относительно производительности Dart VM?


person Tina Hildebrandt    schedule 19.01.2015    source источник
comment
важно ли преобразование в big endian для ваших целей? мы его не оптимизировали. передача Endianness.HOST_ENDIAN повысит производительность в 3 раза. В настоящее время я просматриваю код, чтобы увидеть, где скрывается еще один фактор, равный 3, и вернусь к вам, когда узнаю больше.   -  person Vyacheslav Egorov    schedule 19.01.2015
comment
В портированном программном обеспечении прямой порядок байтов является предварительным условием для обеспечения совместимости с наборами данных, созданными Java. Для нового программного обеспечения это может быть не так важно. Но почему замена 2 или 4 байтов дает такую ​​огромную разницу в производительности? Это можно сделать без дополнительной проверки, или?   -  person Tina Hildebrandt    schedule 19.01.2015
comment
Понятно, достаточно честно. У него есть успех, потому что мы не оптимизировали преобразование (раньше никто не использовал его и не уведомлял нас о медлительности): чтобы преобразовать значение BE - ›LE, нам пришлось бы перепрыгнуть через множество обручей. Однако это несложно оптимизировать - я начал с этого и должен получить предварительные цифры сегодня [code.google.com/p/dart/issues/detail?id=22107].   -  person Vyacheslav Egorov    schedule 19.01.2015
comment
@VyacheslavEgorov Зачем нужны два slow нативных вызова (ByteData_ToEndianIntXX и TypedData_SetIntXX)? Вы можете поменять местами байты за один вызов. Или вы можете просто поменять их местами в Dart. И зачем нужно столько проверок, если собственный код вызывается только из доверенного кода Dart?   -  person mezoni    schedule 20.01.2015
comment
@mezoni есть только один медленный звонок toEndianXYZ. Другой встроен оптимизатором. Проверки существуют, потому что лучше безопасно, чем сожалеть, у Dart есть отражение, поэтому лучше проверять все собственные записи, чтобы предотвратить случайную атаку через отражение. Оптимизатор предназначен для встраивания этого материала и исключения проверок. _toEndianXYZ существует потому, что а) он не оптимизирован, б) в Dart нет неродного способа обмена байтами.   -  person Vyacheslav Egorov    schedule 20.01.2015
comment
Еще одно наблюдение (по Intel): явная передача LITTLE_ENDIAN вместо HOST_ENDIAN повышает производительность еще в два раза.   -  person sgjesse    schedule 20.01.2015
comment
@sgjesse, скорее всего, из-за нашей эвристики встраивания - мы используем количество аргументов констант, чтобы решить, хотим ли мы встраивать (среди прочего). Я собираюсь внести эти методы в белый список для встраивания, так что это больше не имеет значения.   -  person Vyacheslav Egorov    schedule 20.01.2015


Ответы (2)


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

А пока вы можете обойти эту досадную регрессию производительности, свернув собственное преобразование в big 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

person Vyacheslav Egorov    schedule 20.01.2015
comment
Я могу подтвердить улучшения в программе тестирования. Еще придется адаптировать мой серверный программный пакет. Я сделал те же тесты для чтения двоичных данных, и здесь разница с версией Java еще хуже: в 12 раз медленнее. Попробую создать читалку, похожую на вашего писателя или уже что-то подобное делал? - person Tina Hildebrandt; 20.01.2015
comment
@TinaHildebrandt Я этого не делал - но похоже (просто инвертируйте writeXYZ методы). Напишите мне, если возникнут проблемы. И извините за проблемы с производительностью, мы решим их. - person Vyacheslav Egorov; 20.01.2015

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

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

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

На мой взгляд, наличие одного языка для клиентских и серверных приложений может быть очень хорошим аргументом в пользу 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);
  });
}
person Tina Hildebrandt    schedule 22.01.2015
comment
Забыл упомянуть: версия Java использует файловые блокировки. Они не существуют для Дарта (и их очень не хватает). Но на их долю приходится всего около 3% снижения производительности. - person Tina Hildebrandt; 22.01.2015
comment
Спасибо за обновления! Это довольно интересно! Мы будем работать над тем, чтобы сократить этот 30% разрыв ... Кстати, я уже приземлился (code.google.com/p/dart/source/detail?r=43038) некоторые изменения в ByteData методах, которые ускоряют ваш исходный тест в 6-7 раз, и у меня в разработке есть другие улучшения - после того, как мы закончим просмотр этого кода, отпадет необходимость в специальных процедурах преобразования порядка байтов. - person Vyacheslav Egorov; 24.01.2015