GithubHelp home page GithubHelp logo

justjavac / v8-source-read Goto Github PK

View Code? Open in Web Editor NEW
45.0 7.0 7.0 14 KB

V8 探秘

Home Page: https://zhuanlan.zhihu.com/v8core

License: GNU General Public License v3.0

JavaScript 100.00%
v8 javascript v8-javascript-engine v8-engine

v8-source-read's Introduction

v8-source-read's People

Contributors

justjavac avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

v8-source-read's Issues

V8 使用“常量折叠”优化技巧,导致幂(**)运算有时候不等于 Math.pow()

在如今的主流 Web 编程语言中,如 PHP 或 Python 等,都包含幂运算符(一般来说符号是 ^ 或者 **)。而最新的 ES7 中也增加了幂运算,使用符号 **,最新的 Chrome 已经提供了对幂运算的支持。

但是在 javascript 中,** 运算有时候并不等于 Math.pow(a,b),在最新的 Chrome 55 中:

Math.pow(99,99) 的结果是 3.697296376497263e+197,但是 99**99 的结果是 3.697296376497268e+197

两者并不相等

3.697296376497263e+197 
3.697296376497268e+197

而且 Math.pow(99,99) - 99**99 的结果也不是 0 而是 -5.311379928167671e+182

因此我们猜测,** 操作符只是幂运算的另一个实现。但是当我们写一个函数时,幂运算又表现出诡异的特性:

function diff(x) {
  return Math.pow(x,x) - x**x;
}

调用 diff(99) 返回 0。WTF?两者又相等了!

猜猜下面代码输出什么?

var x = 99;
x**x - 99**99;

这段代码的运行结果是 -5.311379928167671e+182

这简直是薛定谔的幂。

究其原因,V8 引擎使用了常量折叠(const folding)。常量折叠是一种编译器的编译优化技术。

考虑如下代码:

for (let i = 0; i < 100*100*100; i++){
  // 循环体
}

该循环的条件 i<100*100*100 是一个表达式(expression),如果放到判断时再求值那么 100*100*100 的计算将会进行 1000000 次。如果编译器在语法分析阶段进行常量合并,该循环将会变为这样:

for (let i = 0; i < 1000000; i++){
  // 循环体
}

而上文中提到的 99**99 的计算也使用到了常量折叠。也就是说 99**99 是在编译时进行计算(常量折叠),而 Math.pow 总是在运行时进行计算。当我们使用变量进行幂运算时(例 a**b)此时不存在常量折叠,因此 a ** b 的值在运行时进行计算,** 会被编译成 Math.pow 调用。

在源码 src/parsing/parser.cc 文件中,编译时计算代码:

case Token::EXP: {
double value = Pow(x_val, y_val);
int int_value = static_cast<int>(value);
*x = factory()->NewNumberLiteral(
    int_value == value && value != -0.0 ? int_value : value, pos,
    has_dot);
return true;

可以看到使用了 Pow 函数计算了幂运算的求值结果。Pow 是一个 inline 函数,内部做了一些常规优化,对不能优化的情况则使用了 std::pow(x, y) 来计算最终结果。

Math.pow 的算法为:

// ES6 section 20.2.2.26 Math.pow ( x, y )
TF_BUILTIN(MathPow, CodeStubAssembler) {
  Node* x = Parameter(1);
  Node* y = Parameter(2);
  Node* context = Parameter(5);
  Node* x_value = TruncateTaggedToFloat64(context, x);
  Node* y_value = TruncateTaggedToFloat64(context, y);
  Node* value = Float64Pow(x_value, y_value);
  Node* result = ChangeFloat64ToTagged(value);
  Return(result);
}

可见两者使用了不同的算法。但是当不做常量折叠的时候,** 则转换成了 Math.pow 函数调用:

Expression* Parser::RewriteExponentiation(
    Expression* left, 
    Expression* right,
    int pos) {
  ZoneList<Expression*>* args = new (zone()) ZoneList<Expression*>(2, zone());
  args->Add(left, zone());
  args->Add(right, zone());
  return factory()->NewCallRuntime(Context::MATH_POW_INDEX, args, pos);
}

于是就造成了 ** 有时不等于 Math.pow 的怪异问题。再看看如下代码:

console.log(99**99);
a = 99, b = 99;
console.log(a**b);
console.log(Math.pow(99, 99));

分别输出:

3.697296376497268e+197
3.697296376497263e+197
3.697296376497263e+197

其实:

9999=369729637649726772657187905628805440595668764281741102430259972423552570455277523421410650010128232727940978889548326540119429996769494359451621570193644014418071060667659301384999779999159200499899

因此第一个结果更接近准确的值。

上周(2017年1月16日)这个怪异的行为已经作为一个 bug 提交给了 V8 项目,bug 编号 #5848

问题来源:Why is Math.pow() (sometimes) not equal to ** in JavaScript?


欢迎订阅我的微信公众帐号 (justjavac-blog):

justjavac 微信公众帐号

开启 V8 对象属性的“fast”模式

在 Bluebird 库中有一段匪夷所思的代码(/src/util.js):

function toFastProperties(obj) {
    /*jshint -W027*/
    function f() {}
    f.prototype = obj;
    ASSERT("%HasFastProperties", true, obj);
    return f;
    eval(obj);
}

所有的 javascript 最佳实践都告诉我们不要使用 eval。更奇怪的是,这段代码却在函数 return 之后又调用了 eval,于是添加了一行注释来禁止 jshint 的警告信息。

Unreachable 'eval' after 'return'. (W027)

那么这段代码真的有那么神奇,可以加速对象中属性的访问速度吗?

在 V8 引擎中,对象有 2 中访问模式:Dictionary mode(字典模式) 和 Fast mode(快速模式)。

  • Dictionary mode(字典模式):字典模式也成为哈希表模式,V8 引擎使用哈希表来存储对象。
  • Fast mode(快速模式):快速模式使用类似 C 语言的 struct 来表示对象,如果你不知道什么是 struct,可以理解为是只有属性没有方法的 class。

当动态地添加太多属性、删除属性、使用不合法标识符命名属性,那么对象就会变为字典模式(基准测试)。

Test Dictionary Mode

速度差了近 3 倍。

javascript 作为一名灵活的动态语言,开发者有很多种方式可以创建对象,还可以在创建完对象以后动态的添加和删除对象的属性,因此高效而灵活的表示一个对象比静态语言要困难很多。

根据 ECMA-262 标准,对象的属性都是字符串,即使使用了数字作为属性也会被转换为字符串。因此:

var b;
var a = {};
a.b = 1;
a[b] = 2;

此时 a 对象的值是:

{
  b: 1,
  undefined: 2
}

V8 中所有的变量都继承 Value。原始值都继承 Primitive,对象的类型为 Object,继承 Value,函数的类型为 Function,继承 Object。而原始值的包装类也都有各自的类型,比如 Number 的包装类是 NumberObject,也继承 Object。

Object 的属性通过 2 中方式访问:

/**
 * A JavaScript object (ECMA-262, 4.3.3)
 */
class V8_EXPORT Object : public Value {
 public:
  V8_DEPRECATE_SOON("Use maybe version",
                    bool Set(Local<Value> key, Local<Value> value));
  V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context,
                    Local<Value> key, Local<Value> value);

  V8_DEPRECATE_SOON("Use maybe version",
                    bool Set(uint32_t index, Local<Value> value));
  V8_WARN_UNUSED_RESULT Maybe<bool> Set(Local<Context> context, 
                   uint32_t index, Local<Value> value);

在快速模式下对象的 properties 是由 Heap::AllocateFixedArray 创建的普通 FixedArray。在字典模式下,对象的 properties 是由 NameDictionary::Allocate 创建的 NameDictionary

在视频 https://www.youtube.com/watch?v=hWhMKalEicY 中,V8 的开发者 Lars Bak 解释了对象的两种访问模式以及快速模式是如何运行的。

Vyacheslav Egorov 的 Understanding V8 中 Understanding Objects 章节也解释了 Hidden Class 是如何工作的。

当一个 JS 对象被设置为某个函数的原型的时候,它会退出字典模式:

Accessors::FunctionSetPrototype(JSObject*, Object*, void*)
↓
static JSFunction::SetPrototype(Handle<JSFunction>, Handle<Object>)
↓
static JSFunction::SetInstancePrototype(Handle<JSFunction>, Handle<Object>)
↓
static JSObject::OptimizeAsPrototype(Handle<JSObject>) 
↓
JSObject::OptimizeAsPrototype() 
↓
JSObject::TransformToFastProperties(0) 
↓
NameDictionary::TransformPropertiesToFastFor(obj, 0)

我们可以看看 V8 源码中关于 fast-prototype 的测试用例:

function test(use_new, add_first, set__proto__, same_map_as) {
  var proto = use_new ? new Super() : {};
  // New object is fast.
  assertTrue(%HasFastProperties(proto));
  if (add_first) {
    AddProps(proto);
    // Adding this many properties makes it slow.
    assertFalse(%HasFastProperties(proto));
    DoProtoMagic(proto, set__proto__);
    // Making it a prototype makes it fast again.
    assertTrue(%HasFastProperties(proto));
  } else {
    DoProtoMagic(proto, set__proto__);
    // Still fast
    assertTrue(%HasFastProperties(proto));
    AddProps(proto);
    // After we add all those properties it went slow mode again :-(
    assertFalse(%HasFastProperties(proto));
  }
  if (same_map_as && !add_first) {
    assertTrue(%HaveSameMap(same_map_as, proto));
  }
  return proto;
}

如果觉得难懂,直接看我加粗的注释,我们可以知道:

  • 新建的对象是 fast 模式
  • 添加太多的属性,变 slow
  • 设置为其它对象的 prototype,变 fast

因此 Bluebird 代码中 f.prototype = obj 是使属性访问变快的关键。当把一个对象设置为另一个对象的 prototype 时,V8 引擎对对象的结构重新进行了优化。

V8 中关于对象的代码定义在 objects.cc 中:

void JSObject::OptimizeAsPrototype(Handle<JSObject> object,
                                   PrototypeOptimizationMode mode) {
  if (object->IsJSGlobalObject()) return;
  if (mode == FAST_PROTOTYPE && PrototypeBenefitsFromNormalization(object)) {
    // First normalize to ensure all JSFunctions are DATA_CONSTANT.
    JSObject::NormalizeProperties(object, KEEP_INOBJECT_PROPERTIES, 0,
                                  "NormalizeAsPrototype");
  }
  Handle<Map> previous_map(object->map());
  if (object->map()->is_prototype_map()) {
    if (object->map()->should_be_fast_prototype_map() &&
        !object->HasFastProperties()) {
      JSObject::MigrateSlowToFast(object, 0, "OptimizeAsPrototype");
    }
  } else {
    if (object->map() == *previous_map) {
      Handle<Map> new_map = Map::Copy(handle(object->map()), "CopyAsPrototype");
      JSObject::MigrateToMap(object, new_map);
    }
    object->map()->set_is_prototype_map(true);

JSObject::MigrateSlowToFast 将对象的字典模式变成了快速模式。https://v8.paulfryzel.com/docs/master/classv8_1_1internal_1_1_j_s_object_a663c5f054f780e77e595402eef1c4d1e_cgraph.svg

MigrateSlowToFast 的源码比较长,原理就是使用 FixedArray 替换了 NameDictionary

SetPrototype 函数中有一段:

  // Set the new prototype of the object.
  Handle<Map> map(real_receiver->map());

  // Nothing to do if prototype is already set.
  if (map->prototype() == *value) return value;

  if (value->IsJSObject()) {
    JSObject::OptimizeAsPrototype(Handle<JSObject>::cast(value));
  }

OptimizeAsPrototype 的代码:

void JSObject::OptimizeAsPrototype(Handle<JSObject> object) {
  if (object->IsGlobalObject()) return;

  // Make sure prototypes are fast objects and their maps have the bit set
  // so they remain fast.
  if (!object->HasFastProperties()) {
    MigrateSlowToFast(object, 0);
  }
}

相关阅读:


欢迎订阅我的微信公众帐号 (justjavac-blog):

justjavac 微信公众帐号

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.