[Dart翻译]对dart:mirrors的恐惧

原文地址:mrale.ph/blog/2017/0…

原文作者:mrale.ph/

发布时间:2017年1月8日

dart:mirrors可能是Dart核心库中最被误解、误解和忽视的组件。它从一开始就是Dart语言的一部分,但仍然被不确定的迷雾所包围,被标记为状态。即使API已经很久没有变化了。

dart:mirrors很少受到关注,因为它是一个能够实现元编程的库,因此普通的Dart用户很少需要直接处理它–相反,它是框架/库作者必须处理的问题。

事实上,我写这篇文章的过程并不是从dart:mirrors开始的,而是从完全不同的东西开始的。

JSON反序列化

2016年12月,我在Dart的Slack频道上闲逛(现在已经不存在了,改用dart-lang Gitter),看到一个用户,我们叫他Maximilian,询问在Dart中解析JSON的方法。如果你来自JavaScript,那么这种问题可能会让你吃惊:在Dart中解析JSON不是和在JavaScript中做JSON.parse(…)一样容易吗?

是的,这很容易:你可以简单地使用内置的dart:convert库中的JSON.decode(…),但是有一个问题。JavaScript的JSON.parse可以给你返回一个对象,因为JavaScript对象是无形状的属性云,像这样的代码是完全可以的。

let userData = JSON.parse(str);
console.log(`Got user ${userData.name} from ${userData.city}`);
复制代码

然而每个Dart对象都有一个固定的类,完全决定了它的形状。Dart的JSON.Decode可以给你一个Map,但它不能给你一个自定义类型的对象。

Map userData = JSON.decode(str);
console.log("Got user ${userData['name']} from ${userData['city']}");  // OK

class UserData {
  String name;
  String city;
}

UserData userData = JSON.decode(str);  // NOT OK
console.log("Got user ${userData.name} from ${userData.city}");
复制代码

解决这个问题的一个常见方法是编写marshaling帮助器,可以从Maps中创建你的对象。

class UserData {
  String name;
  String city;

  UserData(this.name, this.city);

  UserDate.fromJson(Map m)
      : this(m['name'], m['city']);
}

UserData userData = new UserDate.fromJson(JSON.decode(str));
console.log("Got user ${userData.name} from ${userData.city}");
复制代码

但是写这种模板式的代码,很少有人会喜欢用这种方式来度过一天。这就是为什么pub中包含了很多可以自动化的包,而且……鼓掌……其中一些使用了dart:mirrors。

什么是dart:mirrors?

如果你不是一个语言工作者,你很可能从来没有听说过与编程语言有关的镜像这个词。它是一个词的游戏:镜像API允许程序反思自己。从历史上看,它起源于SELF,就像许多其他伟大的虚拟机技术一样。如果你想了解更多关于镜像及其在其他系统中的作用,请查看Gilad Bracha的文章并跟随链接。

镜像的存在是为了回答反射性的问题,比如。”给定对象的类型是什么?”、”它有哪些字段/方法?”、”字段的类型是什么?”、”给定方法的参数的类型是什么?”以及执行反射性动作,如 “从该对象获取这个字段的值!”和 “在该对象上调用这个方法!”。

在JavaScript中,对象本身就具有反思能力:要动态地通过名字获得一个属性,你可以只做obj[key],用一个动态的参数列表obj[name].apply(obj, args)来调用一个方法的名字。

另一方面,Dart在dart:mirrors库中封装了这些功能。

import 'dart:mirrors' as mirrors;

setField(obj, name, value) {
  // Get an InstanceMirror that allows to access obj state
  final mirror = mirrors.reflect(obj);
  // Set the field through the mirror.
  mirror.setField(new Symbol(name), value);
}
复制代码

这个API乍看之下可能有点啰嗦,但实际上像new Symbol(name)这样的东西存在是有原因的:它们提醒你,给类、方法和字段起的名字不是永远存在的–它们可能被改变,被编译器篡改,试图减少生成代码的大小。请记住这一点,我们稍后会回到这个问题上。

回到JSON反序列化

基于dart:mirrors的反序列化库通常非常容易使用:你只需在你的类上加一个注解就可以了。例如,Maximilian在Slack上提出的问题中的数据模型在dart:mirrors的dartson注解下是什么样子。

我重新命名了所有的字段和类,使其匿名化。数据模型的结构被保留了。]

import 'package:dartson/dartson.dart';

@Entity()
class Data {
  String something0;
  String something1;
  List<Data1> sublist0;
  List<Data2> sublist1;
  List<Data3> sublist2;
}

@Entity()
class Data1 {
  String something0;
  String something1;
  String something2;
  String something3;
  String something4;
  String something5;
  String something6;
}

@Entity()
class Data2 {
  String something0;
  String something1;
}

@Entity()
class Data3 {
  String something0;
  String something1;
}

final DSON = new Dartson.JSON();

// Deserialize Data object from a JSON encoded string.
Data d = DSON.decode(string, new Data());
复制代码

有人可能会问:如果事情是如此简单和顺利,那么问题是什么呢?

事实证明,dartson真的非常非常慢。当Maximilian拿着一个9Mb的数据文件,其中包含了他需要的数据,他看到了以下的情况。

$ dart decode-benchmark.dart --with-dartson
loaded data.json: 9389696 bytes
DSON.decode(...) took: 2654ms
$ d8 decode-benchmark.js
loaded data.json: 9389696 bytes
JSON.parse(...) took: 118ms
复制代码

这些数字看起来不是很好。让我们再做一些测量。

$ dart decode-benchmark.dart --with-json
loaded data.json: 9389696 bytes
JSON.decode(...) took: 236ms
复制代码

那么,仅仅是将JSON字符串解码成非结构化的Maps和Lists森林就比使用dartson快10倍?这是相当令人沮丧的,而dart:mirrors显然是这里的罪魁祸首! 至少Slack频道对这个问题的共识是这样的。

在我们深入挖掘之前,我想在这里提出一个看法。V8的JSON解析器是用C++编写的,Dart的实际上是用Dart编写的,它是一个流式解析器。当你意识到这一点时,Dart的JSON.decode(…)和V8的JSON.parse(…)之间的2倍差异看起来并不那么糟糕。

另一个有趣的事情是尝试用其他语言来做基准,用反射来反序列化JSON:Maximilian尝试了Go的encoding/json包。

import (
  "io/ioutil"
  "encoding/json"
  "fmt"
  "time"
)

type Data struct {
  Something0 string `json:"something0"`
  Something1 string `json:"something1"`
  Sublist0 []Data1 `json:"sublist0"`
  Sublist1 []Data2 `json:"sublist1"`
  Sublist2 []Data3 `json:"sublist2"`
}

var data Data
err := json.Unmarshal(data, &data)
复制代码
$ go run decode-benchmark.go
loaded data.json: 9389696 bytes
json.Unmarshal(...) took: 279ms
复制代码

因此,在Go中,将JSON反序列化为一个结构所需的时间与在Dart中将JSON反序列化为一个非结构化的Maps所需的时间大致相同,尽管编码/json确实使用反射来完成。

下面是Maximilian解析相同的9MB输入文件所需时间的图表(在同一进程中运行30次)。

image.png

dartson没有出现在图片中,因为它至少比其他东西慢10倍……

深入研究dartson的性能

和以往一样,研究Dart代码性能的最简单的方法之一是使用Observatory。

$ dart --observe decode-benchmark.dart --dartson
Observatory listening on http://127.0.0.1:8181/
loaded data.json: 9389696 bytes
...
复制代码

查看观察站中的CPU配置文件页面,发现了令人不安的画面。

image.png

这张图片告诉我们,dartson花了大量的时间在插值字符串上。看一下源代码就会发现,dartson做了很多这样的记录。

void _fillObject(InstanceMirror objMirror, Map filler) {
  // ...
  _log.fine("Filled object completly: ${filler}");
}
复制代码

而像这样

Object _convertValue(TypeMirror valueType, Object value, String key) {
  // ...
  _log.finer('Convert "${key}": $value to ${symbolName}');
  // ...
}
复制代码

这显然是浪费了大量的时间,因为即使在禁用日志的情况下,也要进行字符串插值。因此,实现更高性能的反序列化的第一步是完全删除所有这些日志。

$ dart decode-benchmark.dart --with-dartson
loaded data.json: 9389696 bytes
DSON.decode(...) took: 1542ms
复制代码

Voilà! 我们刚刚通过改变一些与镜像或JSON无关的东西,使用dartson进行JSON反序列化的速度提高了42%。如果我们再看一下资料,就会发现事情终于开始变得有趣了。

image.png

在这里,我们似乎反复要求镜像计算与某些声明相关的元数据。让我们看看Dartson._fillObject

void _fillObject(InstanceMirror objMirror, Map filler) {
  ClassMirror classMirror = objMirror.type;

  classMirror.declarations.forEach((sym, decl) {
    // Look at the declaration (e.g. a field) and make a decision
    // how to deserialize it from the given [filler] based on
    // the metadata associated with it and field's type.
  });
}
复制代码

每当我面对一个优化问题时,我总是问自己的第一个问题是 “这段代码是否重复了一些操作?它可以被缓存吗?”。对于Dartson._fillObject来说,答案是Yes和Yes。上面的代码重复地浏览了给定类中的所有声明,并一次又一次地做出了非常相同的决定。这些决定可以被缓存起来,因为Dart中的类不会动态变化–它们不会得到新的字段,字段也不会改变它们的声明类型。让我们重构代码。

// For each class this cache contains a list of deserialization actions:
// "take a value from JSON map, convert it and store it in the given field".
// Actions are stored as triplets linearly in the list:
//
//     [jsonName_0, fieldName_0, convertion_0,
//      jsonName_1, fieldName_1, convertion_1,
//      ...]
//
final Map<ClassMirror, List> fillActionsCache = <ClassMirror, List>{};

/// Puts the data of the [filler] into the object in [objMirror]
/// Throws [IncorrectTypeTransform] if json data types doesn't match.
void _fillObject(InstanceMirror objMirror, Map filler) {
  ClassMirror classMirror = objMirror.type;

  var actions = fillActionsCache[classMirror];
  if (actions == null) {
    // We did not hit the cache and we need to compute a list of actions.
  }

  // Iterate all actions and execute those that have a matching field
  // in the [filler].
  for (var i = 0; i < actions.length; i += 3) {
    final jsonName = actions[i];
    final value = filler[jsonName];
    if (value != null) {
      final fieldName = actions[i + 1];
      final convert = actions[i + 2];
      objMirror.setField(fieldName, convert(jsonName, value));
    }
  }
}
复制代码

请注意,我们分别缓存了源jsonName和目标fieldName(作为一个Symbol),因为字段名不一定与源JSON属性名一致,因为dartson支持通过@Property(name: …)注解进行重命名。另一件事是,我们把类型转换作为一个闭包来缓存,以避免反复查找字段的类型,并找出要应用的转换。

当我们第一次用某个类进入_fillObject时,我们需要计算这个类的反序列化动作的列表。从本质上说,这与执行反序列化的代码相同,但我们现在不是填充一个实际的对象,而是填充一个要执行的动作列表。

actions = [];
classMirror.declarations.forEach((Symbol fieldName, decl) {
  // We are only interested in public non-constant fields and setters.
  if (!decl.isPrivate &&
      ((decl is VariableMirror && !decl.isFinal && !decl.isConst) ||
       (decl is MethodMirror && decl.isSetter))) {
    String jsonName = _getName(fieldName);
    TypeMirror valueType;

    if (decl is MethodMirror) { // Setter.
      // Setters are called `name=`. Remove trailing `=`.
      jsonName = jsonName.substring(0, jsonName.length - 1);
      valueType = decl.parameters[0].type;
    } else {  // Field.
      valueType = decl.type;
    }

    // Check if the property was renamed via @Property(name: ...)
    final Property prop = _getProperty(decl);
    if (prop?.name != null) {
      jsonName = prop.name;
    }

    // Populate actions.
    actions.add(jsonName);
    actions.add(fieldName);
    actions.add(_valueConverter(valueType));
  }
});

// No more actions will be added so convert actions list to non-growable array
// to reduce amount of indirections.
fillActionsCache[classMirror] = actions = actions.toList(growable: false);
复制代码

除了这段代码外,我们还必须重写Dartson._convertValue。在原来的Dartson中,这个函数接收TypeMirror和一个要转换的值,并返回转换后的值。

Object _convertValue(TypeMirror valueType, Object value, String key) {
  if (valueType is ClassMirror &&
      !valueType.isOriginalDeclaration &&
      valueType.hasReflectedType &&
      !_hasOnlySimpleTypeArguments(valueType)) {
    ClassMirror classMirror = valueType;
    // handle generic lists
    if (classMirror.originalDeclaration.qualifiedName == _QN_LIST) {
      return _convertGenericList(classMirror, value);
    } else if (classMirror.originalDeclaration.qualifiedName == _QN_MAP) {
      // handle generic maps
      return _convertGenericMap(classMirror, value);
    }
  } // else if (...) {

  // ... various types handled here ...

  return value;
}
复制代码

我们遵循与应用于_fillObject相同的想法,而不是执行转换,我们计算如何执行转换。

// Function that converts a [value] read from the JSON property to expected
// type.
typedef Object ValueConverter(String jsonName, Object value);

// Compute [ValueConverter] based on the property of the field where the
// the value taken from JSON map will be stored.
ValueConverter _valueConverter(TypeMirror valueType) {
  if (valueType is ClassMirror &&
      !valueType.isOriginalDeclaration &&
      valueType.hasReflectedType &&
      !_hasOnlySimpleTypeArguments(valueType)) {
    ClassMirror classMirror = valueType;
    // handle generic lists
    if (classMirror.originalDeclaration.qualifiedName == _QN_LIST) {
      return (jsonName, value) => _convertGenericList(classMirror, value); // ⚠
    } else if (classMirror.originalDeclaration.qualifiedName == _QN_MAP) {
      // handle generic maps
      return (jsonName, value) => _convertGenericMap(classMirror, value); // ⚠
    }
  } // else if (...) {

  // ... various types handled here ...

  // Identity convertion.
  return (jsonName, value) => value; // ⚠
}
复制代码

通过这种优化,dartson达到了新的性能高度。

$ dart decode-benchmark.dart --with-dartson
loaded data.json: 9389696 bytes
DSON.decode(...) took: 488ms
复制代码

这比原来的性能快了82%–这样一来,我们终于开始接近我们从Go中看到的数字了。

我们可以进一步推动它吗?当然可以。通过阅读代码和分析,我们发现在一些明显的地方,我们可以通过应用之前的优化模式(使用反射来计算要做什么,而不是做什么)来避免重复的反射开销。例如,_convertGenericList包含以下代码。

List _convertGenericList(ClassMirror listMirror, List fillerList) {
  ClassMirror itemMirror = listMirror.typeArguments[0];
  InstanceMirror resultList = _initiateClass(listMirror);
  fillerList.forEach((item) {
    (resultList.reflectee as List)
        .add(_convertValue(itemMirror, item, "@LIST_ITEM"));
  });
  return resultList.reflectee;
}
复制代码

这段代码有多个问题。

  • 一个接一个地将项目添加到列表中,导致增长和重新定位–即使列表是提前知道的。
  • 重复地通过反射找出如何转换每一个单独的项目。

使用我们的_valueConverter帮助器可以很容易地重写代码。

List _convertGenericList(ClassMirror listMirror, List fillerList) {
  ClassMirror itemMirror = listMirror.typeArguments[0];
  InstanceMirror resultList = _initiateClass(listMirror);
  final List result = resultList.reflectee as List;

  // Compute how to convert list items based on itemMirror *once*
  // outside of the loop.
  final convert = _valueConverter(itemMirror);

  // Presize the list.
  result.length = fillerList.length;

  for (var i = 0; i < fillerList.length; i++) {
    result[i] = convert("@LIST_ITEM", fillerList[i]);
  }

  return result;
}
复制代码

另一个可以在dartson代码中做的小优化是缓存类启动器,并远离。

// code from _valueConverter handling conversion from Map
// to object.
return (key, value) {
  var obj = _initiateClass(valueType);
  // ...
  // fill obj from value
  // ...
  return obj.reflectee;
};
复制代码

final init = _classInitiator(valueType);
return (key, value) {
  var obj = init();
  // ...
  // fill obj from value
  // ...
  return obj.reflectee;
};
复制代码

这又是和我们之前应用的模式一模一样,所以我不会用实现细节来打扰你。

$ dart decode-benchmark.dart --with-dartson
loaded data.json: 9389696 bytes
DSON.decode(...) took: 304ms
复制代码

我们现在已经使dartson比原来的性能快了89%! 此外,事实证明,这段代码的热身性能实际上可以和Go媲美。

image.png

我们可以进一步提高性能吗?当然可以! 如果我们看一下我们的反序列化链:从String到Map到实际的对象森林,那么第一步就显得多余了。我们似乎在分配Map,只是为了从中填充一个实际的对象。我们可以直接将字符串解析成一个对象吗?

如果我们仔细看一下Dart VM的JSON解析器的来源,我们会发现它被分成两部分:实际的解析器负责浏览输入(单个字符串或异步到达的块序列),并将beginObject、beginArray等事件发送给构建对象的监听器。不幸的是,_ChunkedJsonParser和监听器接口_JsonListener都隐藏在dart:convert中,用户代码无法使用它们。

至少在我们应用这个小补丁之前。

diff --git a/runtime/lib/convert_patch.dart b/runtime/lib/convert_patch.dart
index 9606568e0e..0ceae279f5 100644
--- a/runtime/lib/convert_patch.dart
+++ b/runtime/lib/convert_patch.dart
@@ -21,6 +21,15 @@ import "dart:_internal" show POWERS_OF_TEN;
   return listener.result;
 }

+parseJsonWithListener(String json, JsonListener listener) {
+  var parser = new _JsonStringParser(listener);
+  parser.chunk = json;
+  parser.chunkEnd = json.length;
+  parser.parse(0);
+  parser.close();
+  return listener.result;
+}
+
 @patch class Utf8Decoder {
   @patch
   Converter<List<int>, T> fuse<T>(
@@ -67,7 +76,7 @@ class _JsonUtf8Decoder extends Converter<List<int>, Object> {
 /**
  * Listener for parsing events from [_ChunkedJsonParser].
  */
-abstract class _JsonListener {
+abstract class JsonListener {
   void handleString(String value) {}
   void handleNumber(num value) {}
   void handleBool(bool value) {}
复制代码

有了JsonListener和parseJsonWithListener从dart:convert中暴露出来,我们可以建立我们自己的JsonListener,它不创建任何中间映射,而是在解析器浏览字符串时填充实际对象。

我已经快速制作了这样的解析器原型,虽然我不打算让你看完每一行代码,但我要强调我的一些设计决定。完整的代码可以在这个gist中找到。

提前一次将Type转换为辅助数据结构

/// Deserialization descriptor built from a [Type] or a [mirrors.TypeMirror].
/// Describes how to instantiate an object and how to fill it with properties.
class TypeDesc {
  /// Tag describing what kind of object this is: expected to be
  /// either [tagArray] or [tagObject]. Other tags ([tagString], [tagNumber],
  /// [tagBoolean]) are not used for [TypeDesc]s.
  final int tag;

  /// Constructor closure for this object.
  final Constructor ctor;

  /// Either map from property names to property descriptors for objects or
  /// element [TypeDesc] for lists.
  final /* Map<String, Property> | TypeDesc */ properties;
}

/// Deserialization descriptor built from a [mirrors.VariableMirror].
/// Describes what kind of value is expected for this property and how
/// how to store it in the object.
class Property {
  /// Either [TypeDesc] if the property is a [List] or an object or
  /// [tagString], [tagBool], [tagNumber] if the property has primitive type.
  final /* int | TypeDesc */ desc;

  /// Setter callback.
  final Setter assign;

  Property(this.desc, this.assign);
}
复制代码

正如我们在dartson上的工作所显示的那样,只使用一次mirror来建立以某种形式描述反序列化动作的辅助数据结构是值得的。

在我的基于镜像的反序列化器中,我遵循同样的路线,但不是懒散地做,而是急切地做,并缓存结果。

动态地适应JSON中属性的顺序

通过观察JSON,可以发现一个有趣的现象:同一类型的对象的属性通常是以相同的顺序到达的。

这意味着我们可以从V8的游戏手册中吸取经验,动态地适应这种顺序,以避免为每个新属性在TypeDesc.properties中进行字典查询。

我在原型中实现这一点的方式非常简单–我只是按照给定TypeDesc的第一个对象的顺序记录属性,然后尝试遵循记录的线索。这种方法对我们的样本JSON很有效,但对于现实世界中的完整或可选属性来说可能太天真了。

class TypeDesc {
  /// Mode determining whether this [TypeDesc] is trying to adapt to
  /// a particular order of properties in the incoming JSON:
  /// the expectation here is that if JSON contains several serialized objects
  /// of the same type they will all have the same order of properties inside.
  int mode = modeAdapt;

  /// A sequence of triplets (property name, hydrate callback, assign callback)
  /// recorded while trying to adapt to the property order in the incoming JSON.
  /// If [mode] is set to [modeFollow] then [HydratingListener] will attempt
  /// to follow the trail.
  List<dynamic> propertyTrail = [];
}

class HydratingListener extends JsonListener {
  @override
  void propertyName() {
    if (desc.mode == TypeDesc.modeNone || desc.mode == TypeDesc.modeAdapt) {
      // This is either the first time we encountered an object with such
      // [TypeDesc], which means we are currently recording the [propertyTrail]
      // or we have already failed to follow the [propertyTrail] and have fallen
      // back to simple dictionary based property lookups.
      final p = desc.properties[value];
      if (p == null) {
        throw "Unexpected property ${name}, only expect: ${desc.properties.keys
            .join(', ')}";
      }

      if (desc.mode == TypeDesc.modeAdapt) {
        desc.propertyTrail.add(value);
        desc.propertyTrail.add(p.desc);
        desc.propertyTrail.add(p.assign);
      }
      prop = p;
      expect(p.desc);
    } else {
      // We are trying to follow the trail.
      final name = desc.propertyTrail[prop++];
      if (name != value) {
        // We failed to follow the trail. Fall back to the simple dictionary
        // based lookup.
        desc.mode = TypeDesc.modeNone;
        desc.propertyTrail = null;
        return propertyName();
      }

      // We are still on the trail.
      final propDesc = desc.propertyTrail[prop++];
      expect(propDesc);
    }
  }
}
复制代码

动态生成的setter闭包

尽管我们在解析过程中大多避免使用镜像,但在填充对象时,我们仍然被迫使用它们来设置字段。

final Setter setField = (InstanceMirror obj, dynamic value) {
  obj.setField(fieldNameSym, value);
};
复制代码

如果我们研究一下Dart VM的源代码,我们会发现它使用动态代码生成来加速InstanceMirror.setField:它生成并缓存了形式为(x, v) => x.$fieldName = v的小闭包,并使用它们来分配字段,而不是通过通用运行时路径。

这里有一个明显的优化机会:与其将setField定义为调用InstanceMirror.setField的闭包,后者又会查找另一个闭包并调用它来设置字段,不如将setField定义为直接设置字段的那个闭包。

有两种方法可以实现这一点。

  • 按摩Dart VM的优化编译器,教它如何在第一个参数是常量或伪常量,即不可变的捕获变量时,对InstanceMirror.setField的调用站点进行专业化处理。这是一个非常复杂的路径。
  • 给Dart VM的源代码打一个小补丁,以暴露动态代码评估能力。
diff --git a/runtime/lib/mirrors_impl.dart b/runtime/lib/mirrors_impl.dart
index ec4ac55147..a61baa3e3c 100644
--- a/runtime/lib/mirrors_impl.dart
+++ b/runtime/lib/mirrors_impl.dart
@@ -393,6 +393,8 @@ abstract class _LocalObjectMirror extends _LocalMirror implements ObjectMirror {
   }
 }

+$evaluate(String expression) => _LocalInstanceMirror._eval(expression, null);
+
 class _LocalInstanceMirror extends _LocalObjectMirror
     implements InstanceMirror {
复制代码

并使用这些能力来重写我们的setField

final setField = mirrors.$evaluate('''(obj, value) {
  obj.reflectee.${fieldNameStr} = value;
}''');
复制代码

结果

image.png

我们用Dart编写的、使用mirrors的标有μDecode和μDecode*的原型在竞争中表现出色,接近V8使用的手工调整过的C++解析器的性能。

μDecode和μDecode*之间的唯一区别是,μDecode*使用mirrors.$evaluate来生成字段设置器闭包,而μDecode则坚持使用公共的dart:mirrors API。

你可能会注意到,这个情节还提到了一个新的球员source_gen,这是我们以前没有遇到过的。

镜像的问题

我希望能够证明,镜像在Dart VM提供的JIT编译环境中表现良好。不幸的是,如果我不介绍镜像在AOT编译环境中的表现,那么这幅图就不完整了。

目前,Dart有提前编译的目标。

  • dart2js是一个针对浏览器环境的AOT编译器,将Dart编译成JavaScript。
  • dart_boostrap是一个基于虚拟机的AOT编译器,针对本地代码。

镜像的情况真的很糟糕–VM的AOT配置目前完全不支持dart:mirrors,而dart2js只支持部分,并且在你编译导入dart:mirrors的应用程序时,会发出愤怒的警告。

AOT编译器对dart:mirrors望而却步的原因是它不受限制的反射能力,它允许你探索、修改和动态地调用任何你能触及的东西。AOT编译器通常在封闭世界的假设下运行,允许他们以不同程度的精度确定哪些类的实例有可能被分配,哪些方法有可能被调用(当然也有可能不被调用),甚至什么是到达一个特定字段的值。dart:mirrors的反思能力使全局数据流和控制流分析的质量大大复杂化和降低了。因此,AOT编译器有一个选择。

  • 拒绝实现dart:mirrors – 这就是VM选择做的事情。
  • 在使用dart:mirrors时,使分析变得保守–这就是dart2js选择的做法。
  • 实现更复杂的控制和数据流分析,以应对一些反射能力。
  • 修复dart:mirrors的API,使其更具有声明性,以帮助AOT编译。

我们稍后会回到最后一个选项,但让我们先看看当前dart2js的选择会给我们带来什么。如果我们通过dart2js将我们的基准编译成JavaScript,并在一个相对较新的V8中运行,我们会看到这样的结果。

image.png

【注:dartson带有一个转化器,可以消除dart:mirrors的使用,但我在这里故意不使用它,因为我想测量dart:mirrors的开销】

我们可以清楚地看到,与基线相比,我们使用mirrors至少要受到4倍的惩罚:dart2js的输出是使用JSON.parse的,所以在试图确定我们的抽象在本地JSON解析器上增加的开销时,使用V8 JSON作为基线是完全合理的。

此外,导入dart:mirrors可以完全禁用tree-shaker,因此输出包含了所有导入的库的所有代码–这些代码大部分都不会被应用程序本身使用,但dart2js必须保守地假设它们有可能通过mirrors到达。

网络开发者显然希望JavaScript的输出更小更快,所以像source_gen这样的库应运而生。

在source_gen之前有pub-transformers,但由于Dart生态系统正在远离它们,我不打算在此详述。变换器和source_gen之间的主要区别是source_gen在磁盘上生成文件,你应该随身携带,甚至提交到你的repo中,而变换器作为pub管道的一部分在内存中运行,使得它们对于没有与pub集成的工具来说是看不见的。变形器的功能非常强大,但也很笨重,这导致了它们的消亡。一般来说,你不想做昂贵的全局转换,特别是你不想在内存中做这些转换。你真正想要的是具有可缓存的中间结果的模块化转换,以加快增量构建。]

与其使用反射能力来抽象出模板代码,不如写一个工具来为你生成模板。例如,如果你在名为decode.dart的文件中写道。

import 'package:source_gen/generators/json_serializable.dart';
part 'decode.g.dart';


@JsonSerializable()
class Data extends Object with _$DataSerializerMixin {
  final String something0;
  final List<Data1> sublist0;
  final List<Data2> sublist1;
  final String something1;
  final List<Data3> sublist2;

  Data(this.something0, this.sublist0, this.sublist1, this.something1, this.sublist2);

  factory Data.fromJson(json) => _$DataFromJson(json);
}
复制代码

然后你运行source_gen,你会得到一个decode.g.dart文件,其中包含所有序列化/反序列化的模板。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of mirrors.src.source_gen.decode;

// **************************************************************************
// Generator: JsonSerializableGenerator
// Target: class Data
// **************************************************************************

Data _$DataFromJson(Map json) => new Data(
    json['something0'] as String,
    (json['sublist0'] as List)
        ?.map((v0) => v0 == null ? null : new Data1.fromJson(v0))
        ?.toList(),
    (json['sublist1'] as List)
        ?.map((v0) => v0 == null ? null : new Data2.fromJson(v0))
        ?.toList(),
    json['something1'] as String,
    (json['sublist2'] as List)
        ?.map((v0) => v0 == null ? null : new Data3.fromJson(v0))
        ?.toList());

abstract class _$DataSerializerMixin {
  String get something0;
  List get sublist0;
  List get sublist1;
  String get something1;
  List get sublist2;
  Map<String, dynamic> toJson() => <String, dynamic>{
        'something0': something0,
        'sublist0': sublist0,
        'sublist1': sublist1,
        'something1': something1,
        'sublist2': sublist2
      };
}
复制代码

这看起来很糟糕(而且仍然没有完全摆脱模板),但它是有效的。结果代码对AOT编译器来说是完全可见的,并且没有使用镜像–所以编译器的工作和你手工写同样的模板一样好–导致了更小更快的JavaScript输出。

作为比较,基于source_gen的基准的JavaScript输出要比基于dartson-mirror的输出小12倍,快2倍。

然而,基于source_gen的解决方案。

  • 它仍然比本地基线慢2倍左右。
  • 看上去很不顺眼。

为什么它比较慢?

它慢的主要原因是我们仍然分配了中间结果:我们没有直接将String转化为对象,而是首先调用dart:convert.JSON.decode来产生一个Map和List对象的森林,然后我们将其转化为实际的 “类型化 “对象。

如果我们看一下dart2js提供的dart:convert实现,我们会发现事情变得更加复杂,因为本地JSON.parse返回的JS对象与Dart的Map不兼容,我们需要把它变成与Map接口兼容的东西。

  • 如果我们不使用reviver来解析JSON,那么我们只需将本地JSON.parse返回的JS对象包裹到_JsonMap类中,该类实现了Map<String, dynamic>。这个封装器就像一个合适的Dart Map,当用户第一次通过Map.operator[]访问任何嵌套对象时,确保懒散地封装或转换这些对象–防止 “原始 “JS对象逃到不知道如何处理它们的Dart世界。嵌套对象(至少那些不是数组的对象)以同样懒惰的方式转换,将它们包装成_JsonMap。
  • 如果我们用reviver解析JSON,那么我们会急切地将JSON.parse的输出转换为Dart的Maps和Lists。

所有这些间接和懒惰的数据在三个世界(JavaScript ⇒ Dart Map ⇒ Dart类实例)之间的转换是有代价的,而且说实话,目前还不清楚是否有真正简单的方法来改善它。通过在JSON.decode中传递一个空的reviver,可以消除懒惰的marshalling的开销,这使得new Data.fromJson(JSON.decode(str, reviver: _empty))比new Data.fromJson(JSON.decode(str))快10%,但多重复制仍然是一个问题。

【感谢Brian Slesinsky指出了一个使用空reviver的技巧】。

有一点是明确的–dart2js可能会对JSON提供一流的支持,以类似@JsonSerializable的特殊注解的形式。然后,它可以通过最小的复制实现对JSON序列化/反序列化的支持(例如,通过懒惰地修补由本地JSON.parse返回的对象的原型)。

如何让它更漂亮?

虽然dart2js可以实现对JSON的一流支持,但它只是世界上众多序列化格式中的一种,所以似乎需要一个更好的解决方案。 source_gen做了这项工作,但它并不漂亮。dart:mirrors很漂亮,但在AOT环境中会有影响。我们能做什么?

这里至少有两个可能的解决方案。

用更多的声明来替代dart:mirrors

dart:mirrors的问题是,它是命令式的,不受限制的能力使它很难进行静态分析。如果Dart语言提供了一个反射库,要求你静态地声明。

  • 一组你要使用的反射功能。
  • 一组你要应用这些能力的类。

这听起来可能有点限制性–但是如果你回想一下我们的JSON反序列化的例子,你就会发现。

  • 我们只需要访问标有 @Entity() 注解的类。
  • 我们需要能够调用这些类的默认构造函数。
  • 我们只需要想迭代它们的字段,知道它们的类型,并知道如何分配这些字段。

镜像的很多其他应用也有类似的限制:有一个他们正在寻找的注解,并且他们只在被注解的类上执行某个有限的动作子集。

这一观察导致了作为实验的reflectable包的开发。这个包的中心思想是声明性地指定反射能力

不幸的是,reflectable的开发停滞不前,它从未成为核心的一部分,这意味着它必须依靠可怕的转化器来在dart2js上生成小而快速的输出。希望在某个时候我们能重新评估reflectable的好处,并使其恢复。

编译时元编程/宏系统

你可能注意到,source_gen和reflectable都是从类似的角度来处理问题的:它们根据你写的代码来生成代码。因此,Dart似乎真的可以从一个好的宏系统中受益。

这似乎是来自PL梦想之地的东西,因为没有真正的主流语言拥有良好的宏系统。然而,我认为很多开发者开始意识到,语法级别的抽象是一个非常有用和强大的工具–而且语言设计者不再害怕提供过于强大的工具。以下是ScalaRustJulia中现代宏系统的一些例子。

对于Dart来说,宏系统在很长一段时间内似乎是不可能的,因为有一大堆解析器在运行(VM有一个用C++写的,dart2js有一个用Dart写的,dart_analyzer有第三个最初用Java写的,现在用Dart)。然而,随着最近试图将所有的Dart工具统一到用Dart编写的通用前端和生成内核,终于有可能尝试引入一个宏系统,该系统将被所有的工具统一支持,从IDE到VM和dart2js编译器。

这仍然是一个梦想,但这个梦想似乎是可以把握的。


通过www.DeepL.com/Translator(免费版)翻译

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享