バイナリ間のデータ変換における Dart のパフォーマンスを向上させるにはどうすればよいでしょうか? 質問する

バイナリ間のデータ変換における Dart のパフォーマンスを向上させるにはどうすればよいでしょうか? 質問する

ドイツの大企業 Future Technologies Group のコンサルティング業務で、約 6,000 行の Java サーバー側ソフトウェアを Dart に移植しました。これにより、Dart をサーバー上で効率的に使用できるかどうかという疑問に答えられるはずです。(クライアント側とサーバー側のプログラミングに 1 つの言語を使用するという、求められていた利点により、Dart が承認されることになります。)

Dart (実際に使ってみてとても楽しかった) について学んだ結果、Java に比べてパフォーマンスが 30 ~ 50% 低下するだろうと予想しましたが、いずれにしても、上記の決定プロセスのカットオフである 100% (2 倍遅い) より悪くはなりませんでした。

移植はスムーズに進みました。多くのことを学びました。ユニット テストは問題ありませんでした。しかし、パフォーマンスは極めて悪く、Java プログラムと比較して全体的に 7 倍も遅くなりました。

コードをプロファイリングすると、データ変換とファイル I/O という 2 つの主な原因が明らかになりました。何か間違っているのでしょうか? クライアントのところに戻って 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比較のために、私は以下のDartベンチマークハーネスの移植版に基づくJavaプログラムを使用しました。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 コードは、私の Intel Windows 7 マシン上で Dart コードよりもほぼ 10 倍高速に実行されます。どちらもそれぞれの VM 上で運用モードで実行されます。

コードに明らかなエラーがありますか? または、この作業を実行するために使用できる別の Dart クラスがありますか? これらの単純な変換で Dart がなぜそれほど遅くなるかについての説明はありますか? それとも、Dart VM のパフォーマンスに関して私が完全に間違った期待を抱いているのでしょうか?

ベストアンサー1

ByteData.setXYZDart VMでは、バイトデータメソッド(および)のパフォーマンスは、直接型付き配列アクセスに比べてかなり悪いのは事実ですByteData.getXYZ。私たちはこの問題に取り組み始めており、初期の結果は有望です[1]。

その間、型付き配列を使用してビッグエンディアンへの変換を独自に実行することで、この残念なパフォーマンスの低下を回避することができます(完全なコードは[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

おすすめ記事