2015-01-19 12 views
18

Einige Beratungsarbeit für eine größere deutsche Firma Future Technologies Group Ich habe etwa 6000 Zeilen Java-Server-Software auf Dart portiert. Dies sollte helfen, die Frage zu beantworten, ob Dart effizient auf dem Server verwendet werden kann. (Die selbst würde ein grünes Licht für Dart geben aufgrund der nach Vorteil einer Sprache für die Client- und Server-Seite-Programmierung.)Wie verbessert man die Dart-Performance der Datenkonvertierung in/aus der Binärdatei?

über Dart Lernen hat mir (was ich wirklich genossen die Arbeit mit) eine Erwartung einer Leistung Strafe von 30-50% relativ zu Java, aber in jedem Fall nicht schlechter als 100% (doppelt so langsam), was der Grenzwert für den oben erwähnten Entscheidungsprozess ist.

Der Port lief reibungslos. Ich habe viel gelernt. Unit Tests waren in Ordnung. Aber die Performance erwies sich als extrem schlecht ... SEVEN mal langsamer im Vergleich zum Java-Programm.

Das Profiling des Codes ergab zwei Hauptschuldige: Datenkonvertierung und Datei-I/O. Vielleicht mache ich etwas falsch? Bevor ich zu meinem Kunden zurückkehre und die Dart-Forschung abbricht, möchte ich einen Ratschlag zur Verbesserung der Dinge suchen. Beginnen wir mit der Datenkonvertierung, der Umwandlung von nativen Dart-Datentypen in verschiedene Binärformate, die für die effektive Übertragung und Speicherung von Daten verwendet werden können.

Normalerweise sind diese Konvertierungen einfach und sehr schnell, da nichts wirklich vom verwendeten internen Format konvertiert werden muss, sondern größtenteils in einem Puffer gespeichert wird. Ich habe ein Benchmark-Programm, das irgendwie die typische Verwendung dieser Umwandlungen in meinem Programm widerspiegelt:

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

Es ist auf dem Benchmark-Gurtzeug von https://github.com/dart-lang/benchmark_harness basiert. Für comparisions verwendete ich das folgende Java-Programm basiert auf einem Port des Geschirrs Dart-Benchmark 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(); 
    } 
} 

Der Java-Code läuft fast genau 10-mal schneller als die Dart-Code auf meinem Intel Windows 7 Maschine. Beide laufen im Produktionsmodus auf ihren jeweiligen VMs.

Gibt es einen offensichtlichen Fehler im Code? Oder gibt es verschiedene Dart-Klassen für diese Aufgabe? Irgendeine Erklärung, warum Dart mit diesen einfachen Konvertierungen so viel langsamer ist? Oder habe ich völlig falsche Erwartungen in Bezug auf die Performance von Dart VM?

+1

ist Umstellung auf Big-Endian wichtig für Deine Ziele? wir haben es nicht optimiert. Das Übergeben von 'Endianness.HOST_ENDIAN' würde die Leistung um den Faktor 3 verbessern. Ich schaue mir gerade den Code an, um zu sehen, wo ein weiterer Faktor von 3 sich versteckt. Ich melde mich wieder, wenn ich mehr weiß. –

+1

In der portierten Software ist Big Endian eine Vorbedingung, um mit Java generierten Datensätzen kompatibel zu bleiben. Für neue Software ist es vielleicht nicht so wichtig. Aber warum macht der Austausch von 2 oder 4 Bytes einen so großen Leistungsunterschied? Das kann ohne zusätzliche Prüfung gemacht werden, oder? –

+2

Ich sehe, fair genug. Es hat einen Treffer, weil wir die Konvertierung nicht optimiert haben (niemand hat es vorher benutzt oder uns nicht über die Langsamkeit informiert): um einen Wert BE -> LE zu konvertieren, würden wir durch viele Hoops springen. Dies ist jedoch nicht schwer zu optimieren - ich habe damit begonnen und sollte heute vorläufige Zahlen erhalten [https://code.google.com/p/dart/issues/detail?id=22107]. –

Antwort

12

Es ist wahr, dass die Leistung von Byte-Datenmethoden (ByteData.setXYZ und ByteData.getXYZ) ziemlich schlecht auf Dart VM im Vergleich zu direkten typisierten Array-Zugriff ist. Wir haben begonnen, an dem Thema zu arbeiten und die ersten Ergebnisse sind vielversprechend [1].

In der Zwischenzeit können Sie Ihre eigene Umwandlung in Big-Endian mit typisierten Arrays (vollständigem Code auf [2]) durch Walzen, um diese unglücklichen Leistung Regression arbeiten können:

/// 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 dieser manuellen Konvertierung auf Ihre Testausbeuten um 6x Verbesserung:

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

+1

Ich kann die Verbesserungen im Benchmark-Programm bestätigen. Müssen noch meine Server-Software anpassen. Ich habe die gleichen Benchmarks für das Lesen von Binärdaten gemacht und hier ist der Unterschied zur Java-Version noch schlimmer: 12 mal langsamer. Ich werde versuchen, einen Leser zu erstellen, der deinem Schreiber ähnlich ist oder schon so etwas gemacht habe? –

+1

@TinaHildebrandt Ich habe es nicht gemacht - aber es ist ähnlich (invertiere einfach 'writeXYZ' Methoden). Ping mich, wenn Sie irgendwelche Probleme haben. Und entschuldigen Sie die Leistungsprobleme, wir werden sie aussortieren. –

4

Ich möchte einige Details hinzufügen, wie ich das Leistungsproblem endlich gelöst habe und wie die Ergebnisse aussehen.

Zuerst habe ich den Ansatz postet von Vyacheslav Egorov und daraus meine eigenen Datenwandler-Klasse entwickelt, die Umwandlungen in beiden Richtungen zur Verfügung stellt. Es ist immer noch kein Produktionscode, aber es funktionierte sehr gut für meinen Server-Software-Port und deshalb habe ich es unten angehängt. Ich habe absichtlich den [Puffer] eine öffentliche Variable behalten. Dies führt möglicherweise nicht zu einer perfekten Kapselung, sondern ermöglicht ein einfaches direktes Schreiben in den Puffer und Lesen aus dem Puffer, z. über ein [RandomAccessFile.readInto] und [RandomAccessFile.writeFrom]. Alles klar und effizient!

Es stellte sich heraus, dass diese wirklich Datenkonvertierungen, wo der Schuldige für die langsame Anfangsleistung von sieben mal langsamer als die Java-Version. Mit der Änderung hat sich die Leistungslücke erheblich verkleinert. Die Dart-Version der 6000-Zeilen-Server-Anwendung folgt jetzt nur noch etwa 30% der Java-Version. Besser als ich es von einer Sprache mit einem solch flexiblen Tippkonzept erwartet hatte. Das wird Dart in eine gute Position für zukünftige technologische Entscheidungen meiner Kunden bringen.

In meinen Augen könnte eine Sprache für Client- und Serveranwendungen ein sehr gutes Argument für Dart sein.

Und hier kommt der Code für den Datenkonverter als für dieses Projekt verwendet:

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

Und ein sehr kleines und einfaches Testgerät:

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

Ich habe vergessen zu erwähnen: Die Java-Version verwendet Dateisperren. Sie existieren nicht für Dart (und werden sehr vermisst). Aber sie machen nur etwa 3% Leistungsstrafe aus. –

+0

Danke für das Update! Es ist ziemlich aufregend! Wir werden daran arbeiten, diese Lücke von 30% weiter zu schließen ... BTW, ich bin bereits gelandet (https://code.google.com/p/dart/source/detail?r=43038), einige Änderungen an den 'ByteData'-Methoden Beschleunigen Sie Ihren ursprünglichen Benchmark um 6x-7x und ich habe mehr Verbesserungen in der Pipeline - sobald wir fertig sind, diesen Code durchzugehen, sollte es keine Notwendigkeit mehr für benutzerdefinierte Konvertierungsroutinen geben. –