写在最前。
昨天晚上在微信里收到了 R 神的建(tu)议(cao),感谢 R 神的吐槽。
------------------------------------分割线------------------------------
以下是原文:
在我的 上一篇文章 中提到了使用按位取反(~~)运算来实现取整操作,并对比了和规范的 Math.floor 的异同。
至于性能,很多文章推荐使用~~来进行取整,并指出:位运算要比使用 Math.floor 快很多。那么,到底快多少呢?
测试1:Math.floor and Math.ceil vs bitwise operations
很遗憾,位运算比 Math.floor 还慢。
测试2:Math.floor and Math.ceil vs bitwise operations
对比结果显示,Math.floor 是最快的。
测试3:Math.floor vs Math.round vs parseInt vs Bitwise
Math.floor 和 Math.round 都很快,但是 parseInt 确实里面最慢的。这不难理解,因为parseInt 的参数是字符串, 而字符串的处理相比数字要慢很多。
大部分的第一直觉是位运算肯定要比函数调用快,而且肯定会快很多很多。我之前也犯过类似的错误。
也有一部分人持相反观点,认为 ~~ 进行取反时,执行了两次操作,所以不一定会很快。
那么 V8 到底如何执行 Math.floor 的呢?我们可以通过搜索“20.2.2.16”或者“Math.floor”找到函数的源码 src/compiler/js-builtin-reducer.cc。
// ES6 section 20.2.2.16 Math.floor ( x )
Reduction JSBuiltinReducer::ReduceMathFloor(Node* node) {
JSCallReduction r(node);
if (r.InputsMatchOne(Type::PlainPrimitive())) {
// Math.floor(a:plain-primitive) -> NumberFloor(ToNumber(a))
Node* input = ToNumber(r.GetJSCallInput(0));
Node* value = graph()->NewNode(simplified()->NumberFloor(), input);
return Replace(value);
}
return NoChange();
}
我们也可以去看关于 Math.floor 的测试用例:对 floor 的测试有两个,一个是 MathFloorWithNumber,一个是 MathFloorWithPlainPrimitive。
我们回到js-builtin-reducer.cc 的源码,如果传入原生类型,那么 ReduceMathFloor() 内部会调用 NumberFloor(),否则是 NoChange()。
NumberFloor 的处理在 src/compiler/typed-optimization.cc 文件:
Reduction TypedOptimization::ReduceNumberFloor(Node* node) {
Node* const input = NodeProperties::GetValueInput(node, 0);
Type* const input_type = NodeProperties::GetType(input);
if (input_type->Is(type_cache_.kIntegerOrMinusZeroOrNaN)) {
return Replace(input);
}
if (input_type->Is(Type::PlainNumber()) &&
(input->opcode() == IrOpcode::kNumberDivide ||
input->opcode() == IrOpcode::kSpeculativeNumberDivide)) {
Node* const lhs = NodeProperties::GetValueInput(input, 0);
Type* const lhs_type = NodeProperties::GetType(lhs);
Node* const rhs = NodeProperties::GetValueInput(input, 1);
Type* const rhs_type = NodeProperties::GetType(rhs);
if (lhs_type->Is(Type::Unsigned32()) && rhs_type->Is(Type::Unsigned32())) {
// We can replace
//
// NumberFloor(NumberDivide(lhs: unsigned32,
// rhs: unsigned32)): plain-number
//
// with
//
// NumberToUint32(NumberDivide(lhs, rhs))
//
// and just smash the type of the {lhs} on the {node},
// as the truncated result must be in the same range as
// {lhs} since {rhs} cannot be less than 1 (due to the
// plain-number type constraint on the {node}).
NodeProperties::ChangeOp(node, simplified()->NumberToUint32());
NodeProperties::SetType(node, lhs_type);
return Changed(node);
}
}
return NoChange();
}
NumberToUint32 是在 opcodes.h 中定义的,opcode 顾名思义就是操作码,是 V8 内部使用的类似汇编指令的代码
Type* OperationTyper::NumberToUint32(Type* type) {
DCHECK(type->Is(Type::Number()));
if (type->Is(Type::Unsigned32())) return type;
if (type->Is(cache_.kZeroish)) return cache_.kSingletonZero;
if (type->Is(unsigned32ish_)) {
return Type::Intersect(Type::Union(type, cache_.kSingletonZero, zone()),
Type::Unsigned32(), zone());
}
return Type::Unsigned32();
}
我在 V8 引擎是如何知道 js 数据类型的?- justjavac 的回答 中曾简单介绍了 V8 引擎对于不同类型数据的存储,包括简单数据类型和复杂数据类型,相应的,V8 也定义了 simplified-operator 用于数字(整数、小数、布尔)。
在 JavaScript 中的数值在 V8 中表示为:
- Tagged 数值
- SMI (31位或32位)
- float64 指针
- Int32
- Uint32
- Float64
除此之外还有一些非 JavaScript 代码,一般用于中间代码使用
- Int64
- Uint64
- Float32
Boolean 值有两种表示:
- tagged pointer:和表示对象类似,使用一个 tagged pointer 来表示 js 中的 true 和 false
- Bit:使用 untagged integer,0 或者 1(64位)
在 types.h 文件中我们可以看到,V8 内部大量使用宏和 C++ 位运算。因此并不是位运算不快,而是 V8 已经对很多操作进行了优化。
我的观点是:写业务代码,还是可读性最重要,把优化留给引擎去做吧。如果是写技术代码,可以适当的使用一些提示性能的奇技淫巧。