《Chrome V8 源码》47. "Equal" 与 "StrictEqual" 为什么不同

1 介绍

substring、getDate、catch 等是常用的 JavaScript API,接下来的几篇文章将对 V8 中 API 的设计思想、源码和关键函数进行讲解,并通过例子讲解 JavaScript 在 V8 中的初始化、运行方式,以及它与解释器、编译器、字节码之间的关系。
本文讲解 API Equal 和 StrictEqual 的设计与实现。

2 Equal 和 StrictEqual 的调用方式

来看一段 JS 源码和它的字节码:

1.  var a="123";
2.  var b = a == 123;
3.  var c = a === 123;
4.  console.log(b);
5.  console.log(c);
6.  //分隔线............
7.  //省略...............................
8.  000000CFDD021F54 @   22 : 6a f9 04          TestEqual r1, [4]
9.  000000CFDD021F57 @   25 : 23 03 05          StaGlobal [3], [5]
10.  000000CFDD021F5A @   28 : 21 02 02          LdaGlobal [2], [2]
11.  000000CFDD021F5D @   31 : c2                Star1
12.  000000CFDD021F5E @   32 : 0d 7b             LdaSmi [123]
13.  000000CFDD021F60 @   34 : 6b f9 07          TestEqualStrict r1, [7]
14.  //省略...............................

上述代码中,第 2 行代码 '==' 的字节码是第 8 行代码 TestEqual;
第 3 行代码 '===' 的字节码是第 13 行代码 TestEqualStrict;
下面讲解字节码 TestEqual 和 TestEqualStrict,源码如下:

IGNITION_HANDLER(TestEqual, InterpreterCompareOpAssembler) {
  CompareOpWithFeedback(Operation::kEqual);
}
//分隔线............
IGNITION_HANDLER(TestEqualStrict, InterpreterCompareOpAssembler) {
  CompareOpWithFeedback(Operation::kStrictEqual);
}

上述两条字节码中都使用了 CompareOpWithFeedback(),区别是参数不同,CompareOpWithFeedback 源码如下:

1.    void CompareOpWithFeedback(Operation compare_op) {
2.      TNode lhs = LoadRegisterAtOperandIndex(0);
3.      TNode rhs = GetAccumulator();
4.      TNode context = GetContext();
5.  //省略.......................
6.      TVARIABLE(Smi, var_type_feedback);
7.      TNode result;
8.      switch (compare_op) {
9.        case Operation::kEqual:
10.          result = Equal(lhs, rhs, context, &var_type_feedback);
11.          break;
12.        case Operation::kStrictEqual:
13.          result = StrictEqual(lhs, rhs, &var_type_feedback);
14.          break;
15.        case Operation::kLessThan:
16.        case Operation::kGreaterThan:
17.        case Operation::kLessThanOrEqual:
18.        case Operation::kGreaterThanOrEqual:
19.          result = RelationalComparison(compare_op, lhs, rhs, context,
20.                                        &var_type_feedback);
21.          break;
22.        default:
23.          UNREACHABLE();
24.      }
25.  //省略.......................
26.      SetAccumulator(result);
27.      Dispatch();
28.    } 
 

上述代码中,第 2 行取出左操作数 lhs;
第 3 行取出右操作数 rhs。rhs 存储在累加器中,所以字节码 TestEqual 的操作数只有 r1,也就是 lhs。[4] 不属于 TestEqual 的操作数,它用于信息收集。TestEqualStrict 的情况也一样。
第 8 行代码根据 compare_op 选择 Equal 或是 StrictEqual。
第 15~19 行代码是小于、大于等操作的实现,本文不做讲解。

3 Equal 源码分析

图 1 给出了 Equal 的源码和函数调用堆栈,需要使用 mksnapshot 进行跟踪。
《Chrome V8 源码》47.
下面讲解 Equal 源码。

1.  TNode CodeStubAssembler::Equal(/*省略*/) {
2.    //省略................
3.  TVARIABLE(Object, var_left, left);
4.  TVARIABLE(Object, var_right, right);
5.    //省略...............
6.   BIND(&loop);
7.  {
8.  left = var_left.value();
9.  right = var_right.value();
10.  Label if_notsame(this);
11.  GotoIf(TaggedNotEqual(left, right), &if_notsame);
12.  {GenerateEqual_Same(left, &if_equal, &if_notequal, var_type_feedback);}
13.  BIND(&if_notsame);
14.  Label if_left_smi(this), if_left_not_smi(this);
15.  Branch(TaggedIsSmi(left), &if_left_smi, &if_left_not_smi);
16.  BIND(&if_left_smi);
17.  {
18.    Label if_right_smi(this), if_right_not_smi(this);
19.    CombineFeedback(var_type_feedback,
20.                    CompareOperationFeedback::kSignedSmall);
21.    Branch(TaggedIsSmi(right), &if_right_smi, &if_right_not_smi);
22.    BIND(&if_right_smi);
23.    { Goto(&if_notequal);   }
24.    BIND(&if_right_not_smi);
25.    { TNode right_map = LoadMap(CAST(right));
26.      Label if_right_heapnumber(this), if_right_oddball(this),
27.          if_right_bigint(this, Label::kDeferred),
28.          if_right_receiver(this, Label::kDeferred);
29.      GotoIf(IsHeapNumberMap(right_map), &if_right_heapnumber);
30.      TNode right_type = LoadMapInstanceType(right_map);
31.      GotoIf(IsStringInstanceType(right_type), &do_right_stringtonumber);
32.      GotoIf(IsOddballInstanceType(right_type), &if_right_oddball);
33.      GotoIf(IsBigIntInstanceType(right_type), &if_right_bigint);
34.      GotoIf(IsJSReceiverInstanceType(right_type), &if_right_receiver);
35.      CombineFeedback(var_type_feedback, CompareOperationFeedback::kAny);
36.      Goto(&if_notequal); }  }
37.  BIND(&if_left_not_smi);
38.  { GotoIf(TaggedIsSmi(right), &use_symmetry);
39.    Label if_left_symbol(this), if_left_number(this),
40.        if_left_string(this, Label::kDeferred),
41.        if_left_bigint(this, Label::kDeferred), if_left_oddball(this),
42.         if_left_receiver(this);
43.     TNode left_map = LoadMap(CAST(left));
44.     TNode right_map = LoadMap(CAST(right));
45.     TNode left_type = LoadMapInstanceType(left_map);
46.     TNode right_type = LoadMapInstanceType(right_map);
47.     GotoIf(IsStringInstanceType(left_type), &if_left_string);
48.     GotoIf(IsSymbolInstanceType(left_type), &if_left_symbol);
49.     GotoIf(IsHeapNumberInstanceType(left_type), &if_left_number);
50.     GotoIf(IsOddballInstanceType(left_type), &if_left_oddball);
51.     Branch(IsBigIntInstanceType(left_type), &if_left_bigint,
52.            &if_left_receiver);
53.     BIND(&if_left_string);
54.     { GotoIfNot(IsStringInstanceType(right_type), &use_symmetry);
55.       result =
56.           CAST(CallBuiltin(Builtin::kStringEqual, context(), left, right));
57.       CombineFeedback(var_type_feedback,
58.                       SmiOr(CollectFeedbackForString(left_type),
59.                             CollectFeedbackForString(right_type)));
60.       Goto(&end);      }
61.     BIND(&if_left_number);
62.     { Label if_right_not_number(this);
63.       CombineFeedback(var_type_feedback, CompareOperationFeedback::kNumber);
64.       GotoIf(Word32NotEqual(left_type, right_type), &if_right_not_number);
65.       var_left_float = LoadHeapNumberValue(CAST(left));
66.       var_right_float = LoadHeapNumberValue(CAST(right));
67.       Goto(&do_float_comparison);
68.       BIND(&if_right_not_number);
69.       { Label if_right_oddball(this);
70.         GotoIf(IsStringInstanceType(right_type), &do_right_stringtonumber);
71.          //省略...............
72.         Goto(&if_notequal);
73.         BIND(&if_right_oddball);
74.         { Label if_right_boolean(this);
75.           GotoIf(IsBooleanMap(right_map), &if_right_boolean);
76.           CombineFeedback(var_type_feedback,
77.                           CompareOperationFeedback::kOddball);
78.           Goto(&if_notequal);
79.           BIND(&if_right_boolean);
80.            {CombineFeedback(var_type_feedback,
81.                              CompareOperationFeedback::kBoolean);
82.              var_right =
83.                  LoadObjectField(CAST(right), Oddball::kToNumberOffset);
84.              Goto(&loop);
85.            }  }  }  }
86.      BIND(&if_left_bigint);
87.      {
88.        Label if_right_heapnumber(this), if_right_bigint(this),
89.            if_right_string(this), if_right_boolean(this);
90.        CombineFeedback(var_type_feedback, CompareOperationFeedback::kBigInt);
91.        GotoIf(IsStringInstanceType(right_type), &if_right_string);
92.         //省略...............
93.        BIND(&if_right_heapnumber);
94.        { CombineFeedback(var_type_feedback, CompareOperationFeedback::kNumber);
95.          result = CAST(CallRuntime(Runtime::kBigIntEqualToNumber,
96.                                    NoContextConstant(), left, right));
97.          Goto(&end);  }
98.        BIND(&if_right_bigint);
99.        {
100.           result = CAST(CallRuntime(Runtime::kBigIntEqualToBigInt,
101.                                     NoContextConstant(), left, right));
102.           Goto(&end); }
103.         BIND(&if_right_string);
104.         {
105.           CombineFeedback(var_type_feedback, CompareOperationFeedback::kString);
106.           result = CAST(CallRuntime(Runtime::kBigIntEqualToString,
107.                                     NoContextConstant(), left, right));
108.           Goto(&end); }
109.         BIND(&if_right_boolean);
110.         { CombineFeedback(var_type_feedback,
111.                           CompareOperationFeedback::kBoolean);
112.           var_right = LoadObjectField(CAST(right), Oddball::kToNumberOffset);
113.           Goto(&loop); } }
114.       BIND(&if_left_oddball);
115.       { Label if_left_boolean(this), if_left_not_boolean(this);
116.         GotoIf(IsBooleanMap(left_map), &if_left_boolean);
117.         if (var_type_feedback != nullptr) {
118.           CombineFeedback(var_type_feedback,
119.                           CompareOperationFeedback::kNullOrUndefined);
120.           GotoIf(IsUndetectableMap(left_map), &if_left_not_boolean);
121.         }
122.         Goto(&if_left_not_boolean);
123.         BIND(&if_left_not_boolean);
124.         {
125.           Label if_right_undetectable(this), if_right_number(this),
126.               if_right_oddball(this),
127.               if_right_not_number_or_oddball_or_undetectable(this);
128.           //省略...............
129.         }  }
130.       BIND(&if_left_receiver);
131.       {
132.         CSA_DCHECK(this, IsJSReceiverInstanceType(left_type));
133.         Label if_right_receiver(this), if_right_not_receiver(this);
134.         Branch(IsJSReceiverInstanceType(right_type), &if_right_receiver,
135.                &if_right_not_receiver);
136.         BIND(&if_right_receiver);
137.         {
138.           CombineFeedback(var_type_feedback,
139.                           CompareOperationFeedback::kReceiver);
140.           Goto(&if_notequal);   }
141.         BIND(&if_right_not_receiver);
142.         {
143.           Label if_right_undetectable(this),
144.               if_right_not_undetectable(this, Label::kDeferred);
145.           Branch(IsUndetectableMap(right_map), &if_right_undetectable,
146.                  &if_right_not_undetectable);
147.           BIND(&if_right_undetectable);
148.           {
149.             CSA_DCHECK(this, IsNullOrUndefined(right));
150.             if (var_type_feedback != nullptr) {
151.               *var_type_feedback = SmiConstant(
152.                   CompareOperationFeedback::kReceiverOrNullOrUndefined);  }
153.             Branch(IsUndetectableMap(left_map), &if_equal, &if_notequal);   }
154.           BIND(&if_right_not_undetectable);
155.           {
156.             CombineFeedback(var_type_feedback, CompareOperationFeedback::kAny);
157.             Callable callable = CodeFactory::NonPrimitiveToPrimitive(isolate());
158.             var_left = CallStub(callable, context(), left);
159.             Goto(&loop); } }}
160.     }
161.     BIND(&do_right_stringtonumber);
162.     {
163.       if (var_type_feedback != nullptr) {
164.         TNode right_map = LoadMap(CAST(right));
165.         TNode right_type = LoadMapInstanceType(right_map);
166.         CombineFeedback(var_type_feedback,
167.                         CollectFeedbackForString(right_type)); }
168.       var_right = CallBuiltin(Builtin::kStringToNumber, context(), right);
169.       Goto(&loop);  }
170.     BIND(&use_symmetry);
171.     {
172.       var_left = right;
173.       var_right = left;
174.       Goto(&loop); }
175.   }
176.   BIND(&if_equal);
177.   {
178.     result = TrueConstant();
179.     Goto(&end); }
180.   BIND(&end);
181.   return result.value();}

我们根据测试用例代码的执行顺序来讲解上述代码。
第 3 行创建左操作数变量 var_left,并把参数 left 的值赋给 var_left;
第 4 行创建右操作数变量 var_right,并把参数 right的值赋给 var_right;
第 6 行进入循环;
第 8、9 行获取左、右操作数并赋给 left 和 right。根据我们的测试用例,left 为 "123",rigth 为 123。
第 11 行代码判断 left 和 right 的Tag 是否一致,left 和rigth 分别是 HeapObject 类型和 SMI 类型,所以 Tag 不一致;跳转到 if_notsame 标签;
第 14-15 行判断左操作数 left 是否为 SMI,left 是 HeapObject,再次跳转到 if_left_not_smi 标签;
第 38 行判断 right 是否为 SMI,结果为真,跳转到 use_symmetry;
第 170-174 行调换左、右操作数,重新到第 6 行进入循环;
再次执行第 11 行代码,依旧跳转到 if_notsame 标签;
第 15 行判断结果为真,跳转到 if_left_smi 标签;
第 21 行判断结果为假,跳转到 if_right_not_smi 标签;
第 30-35 行判断 right 的类型,目前右操作数为 "123",字串符串类型,跳转到 do_right_stringtonumber 标签;
第 163-169 行调用 Builtin::kStringToNumber 把 right 转为 Number 类型,我们的测试用例转为了 SMI;再次进入循环;
第 11 行判断结果为真,进入第 12 行代码;
第 12 行检测到 left 为SMI,所以得出结果 left = right,跳转到 if_equal 标签;
第 176-180 行生成 V8 值对象 true 并返回结果。
为什么没有做值的比较,第 16、18 行就得到了结果?
答:left、right 均为 SMI,所以他们的值是直接存储的,如果二者的 Tag 相等,也就是值相等。

4 StrictEqual 源码分析

与 Equal 不同,StrictEqual 要求左、右操作数的类型和值都相同时,返回值才为真。下面给出 StrictEqual 源码的算法伪代码:

1.  //Pseudo-code for the algorithm below:
2.  //伪代码选自 StrictEqual 源码
3.  if (lhs == rhs) {
4.    if (lhs->IsHeapNumber()) return HeapNumber::cast(lhs)->value() != NaN;
5.    return true;
6.  }
7.  if (!lhs->IsSmi()) {
8.    if (lhs->IsHeapNumber()) {
9.      if (rhs->IsSmi()) {
10.        return Smi::ToInt(rhs) == HeapNumber::cast(lhs)->value();
11.      } else if (rhs->IsHeapNumber()) {
12.        return HeapNumber::cast(rhs)->value() ==
13.        HeapNumber::cast(lhs)->value();
14.      } else {
15.        return false;
16.      }
17.    } else {
18.      if (rhs->IsSmi()) {
19.        return false;
20.      } else {
21.        if (lhs->IsString()) {
22.          if (rhs->IsString()) {
23.            return %StringEqual(lhs, rhs);
24.          } else {
25.            return false;
26.          }
27.        } else if (lhs->IsBigInt()) {
28.          if (rhs->IsBigInt()) {
29.            return %BigIntEqualToBigInt(lhs, rhs);
30.          } else {
31.            return false;
32.          }
33.        } else {
34.          return false;
35.        }
36.      }
37.    }
38.  } else {
39.    if (rhs->IsSmi()) {
40.      return false;
41.    } else {
42.      if (rhs->IsHeapNumber()) {
43.        return Smi::ToInt(lhs) == HeapNumber::cast(rhs)->value();
44.      } else {
45.        return false;
46.      }
47.    }
48.  }

上述代码中,第 3 行代码对左、右操作数进行判断、其中 "==" 为重载运行符;
第 4 行代码说明左、右操作数相同。如果均为 HeapNumber,再进一步判断 HeapNumber 的原始值是否为空,如果原始值存在则判断结果为真;
第 7-38 行代码处理左操作数不是 SMI 的情况,在此情况下:
第 8-9 行代码如果左操作数为 HeapNumber,右操作数为 SMI 时,则判断 SMI 与堆对象的原始值是否相同,相同结果为真;
第 11-13 行代码如果左、右均为 HeapNumber,则判断他们的原始值是否相同,相同结果为真;
第 18-19 行代码左操作数不是 SMI,如果右操作数是 SMI,则结果为假;
第 21-26 行代码如果左、右均为 String,使用 StringEqual() 方法判断;
第 27-34 行代码如果左、右均为 BigInt,使用 BigIntEqualToBigInt() 方法判断;
第 39-45 行代码如果右操作数是 SMI,结果为假;如果右操作数为 HeapNumber,则判断左操作数和右操作数的原始值。

5 技术总结

Equal 方法先将左、右操作数的类型统一,然后再进行值的判断。Equal 采用了如下的优化技巧:
(1) Equal 大部分情况下用于数字类型判断,所以 Equal 开始就判断 Tagged 和 SMI 类型,如果相同则直接返回结果;
(2) 为了简化 Equal的编码,将左、右操作调换以满足左操作数为 SMI 的情况;
(3) 如果左、右操作数均不是 SMI,通过循环的方式统一左、右操作数的类型。

好了,今天到这里,下次见。
个人能力有限,有不足与纰漏,欢迎批评指正
微信:qq9123013 备注:v8交流 知乎:https://www.zhihu.com/people/...

你可能感兴趣的:(《Chrome V8 源码》47. "Equal" 与 "StrictEqual" 为什么不同)