Commit 9711289d authored by Mihir Shah's avatar Mihir Shah Committed by V8 LUCI CQ

A jump-table implementation for constant case switch statements

The change is made since for switch statements with lots of cases,
where each case is a constant integer, the emitted bytecode is still
a series of jumps, when we can instead use a jump table.

If there are 6 or more cases (similar to GCC) of Smi literals, and
if the max Smi case minus the min Smi case is not more than 3 times
the number of cases, we use a jump table up front to handle Smi's,
and then use traditional if-else logic for the rest of the cases.

We then use the jump table in interpreter/bytecode-jump-table to
do the optimization.

This tries to go off issue 9738 in v8's issue tracker. It is not
exactly the same, since that recommends doing the work at JIT-time,
but has similar ideas. It also partially goes off issue 10764.

Bug: v8:9738
Change-Id: Ic805682ee3abf9ce464bb733b427fa0c83a6e10c
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2904926Reviewed-by: 's avatarLeszek Swirski <leszeks@chromium.org>
Commit-Queue: Leszek Swirski <leszeks@chromium.org>
Cr-Commit-Position: refs/heads/master@{#75323}
parent b0ad4a11
......@@ -163,6 +163,7 @@ Michael Lutz <michi@icosahedron.de>
Michael Mclaughlin <m8ch88l@gmail.com>
Michael Smith <mike@w3.org>
Michaël Zasso <mic.besace@gmail.com>
Mihir Shah <mihirshah.11204@gmail.com>
Mike Gilbert <floppymaster@gmail.com>
Mike Pennisi <mike@mikepennisi.com>
Mikhail Gusarov <dottedmag@dottedmag.net>
......
......@@ -426,7 +426,7 @@ void BaselineAssembler::AddSmi(Register lhs, Smi rhs) {
void BaselineAssembler::Switch(Register reg, int case_value_base,
Label** labels, int num_labels) {
Label fallthrough;
if (case_value_base > 0) {
if (case_value_base != 0) {
__ sub(reg, reg, Operand(case_value_base));
}
......
......@@ -503,7 +503,7 @@ void BaselineAssembler::AddSmi(Register lhs, Smi rhs) {
void BaselineAssembler::Switch(Register reg, int case_value_base,
Label** labels, int num_labels) {
Label fallthrough;
if (case_value_base > 0) {
if (case_value_base != 0) {
__ Sub(reg, reg, Immediate(case_value_base));
}
......
......@@ -390,7 +390,7 @@ void BaselineAssembler::Switch(Register reg, int case_value_base,
Register table = scope.AcquireScratch();
DCHECK(!AreAliased(reg, table));
Label fallthrough, jump_table;
if (case_value_base > 0) {
if (case_value_base != 0) {
__ sub(reg, Immediate(case_value_base));
}
__ cmp(reg, Immediate(num_labels));
......
......@@ -541,7 +541,7 @@ void BaselineAssembler::AddSmi(Register lhs, Smi rhs) {
void BaselineAssembler::Switch(Register reg, int case_value_base,
Label** labels, int num_labels) {
Label fallthrough;
if (case_value_base > 0) {
if (case_value_base != 0) {
__ Sub64(reg, reg, Operand(case_value_base));
}
......
......@@ -399,7 +399,7 @@ void BaselineAssembler::Switch(Register reg, int case_value_base,
ScratchRegisterScope scope(this);
Register table = scope.AcquireScratch();
Label fallthrough, jump_table;
if (case_value_base > 0) {
if (case_value_base != 0) {
__ subq(reg, Immediate(case_value_base));
}
__ cmpq(reg, Immediate(num_labels));
......
......@@ -5395,6 +5395,28 @@ void CodeStubAssembler::InitializeAllocationMemento(
Comment("]");
}
TNode<IntPtrT> CodeStubAssembler::TryTaggedToInt32AsIntPtr(
TNode<Object> acc, Label* if_not_possible) {
TVARIABLE(IntPtrT, acc_intptr);
Label is_not_smi(this), have_int32(this);
GotoIfNot(TaggedIsSmi(acc), &is_not_smi);
acc_intptr = SmiUntag(CAST(acc));
Goto(&have_int32);
BIND(&is_not_smi);
GotoIfNot(IsHeapNumber(CAST(acc)), if_not_possible);
TNode<Float64T> value = LoadHeapNumberValue(CAST(acc));
TNode<Int32T> value32 = RoundFloat64ToInt32(value);
TNode<Float64T> value64 = ChangeInt32ToFloat64(value32);
GotoIfNot(Float64Equal(value, value64), if_not_possible);
acc_intptr = ChangeInt32ToIntPtr(value32);
Goto(&have_int32);
BIND(&have_int32);
return acc_intptr.value();
}
TNode<Float64T> CodeStubAssembler::TryTaggedToFloat64(
TNode<Object> value, Label* if_valueisnotnumber) {
return Select<Float64T>(
......
......@@ -2297,6 +2297,8 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
TNode<IntPtrT> base_allocation_size,
TNode<AllocationSite> allocation_site);
TNode<IntPtrT> TryTaggedToInt32AsIntPtr(TNode<Object> value,
Label* if_not_possible);
TNode<Float64T> TryTaggedToFloat64(TNode<Object> value,
Label* if_valueisnotnumber);
TNode<Float64T> TruncateTaggedToFloat64(TNode<Context> context,
......
......@@ -1433,6 +1433,15 @@ DEFINE_BOOL(test_small_max_function_context_stub_size, false,
DEFINE_BOOL(inline_new, true, "use fast inline allocation")
DEFINE_NEG_NEG_IMPLICATION(inline_new, turbo_allocation_folding)
// bytecode-generator.cc
DEFINE_INT(switch_table_spread_threshold, 3,
"allow the jump table used for switch statements to span a range "
"of integers roughly equal to this number times the number of "
"clauses in the switch")
DEFINE_INT(switch_table_min_cases, 6,
"the number of Smi integer cases present in the switch statement "
"before using the jump table optimization")
// codegen-ia32.cc / codegen-arm.cc
DEFINE_BOOL(trace, false, "trace javascript function calls")
......
This diff is collapsed.
......@@ -98,15 +98,47 @@ SwitchBuilder::~SwitchBuilder() {
#endif
}
void SwitchBuilder::SetCaseTarget(int index, CaseClause* clause) {
BytecodeLabel& site = case_sites_.at(index);
builder()->Bind(&site);
if (block_coverage_builder_) {
block_coverage_builder_->IncrementBlockCounter(clause,
SourceRangeKind::kBody);
void SwitchBuilder::BindCaseTargetForJumpTable(int case_value,
CaseClause* clause) {
builder()->Bind(jump_table_, case_value);
BuildBlockCoverage(clause);
}
void SwitchBuilder::BindCaseTargetForCompareJump(int index,
CaseClause* clause) {
builder()->Bind(&case_sites_.at(index));
BuildBlockCoverage(clause);
}
void SwitchBuilder::JumpToCaseIfTrue(BytecodeArrayBuilder::ToBooleanMode mode,
int index) {
builder()->JumpIfTrue(mode, &case_sites_.at(index));
}
// Precondition: tag is in the accumulator
void SwitchBuilder::EmitJumpTableIfExists(
int min_case, int max_case, std::map<int, CaseClause*>& covered_cases) {
builder()->SwitchOnSmiNoFeedback(jump_table_);
fall_through_.Bind(builder());
for (int j = min_case; j <= max_case; ++j) {
if (covered_cases.find(j) == covered_cases.end()) {
this->BindCaseTargetForJumpTable(j, nullptr);
}
}
}
void SwitchBuilder::BindDefault(CaseClause* clause) {
default_.Bind(builder());
BuildBlockCoverage(clause);
}
void SwitchBuilder::JumpToDefault() { this->EmitJump(&default_); }
void SwitchBuilder::JumpToFallThroughIfFalse() {
this->EmitJumpIfFalse(BytecodeArrayBuilder::ToBooleanMode::kAlreadyBoolean,
&fall_through_);
}
TryCatchBuilder::~TryCatchBuilder() {
if (block_coverage_builder_ != nullptr) {
block_coverage_builder_->IncrementBlockCounter(
......
......@@ -5,11 +5,13 @@
#ifndef V8_INTERPRETER_CONTROL_FLOW_BUILDERS_H_
#define V8_INTERPRETER_CONTROL_FLOW_BUILDERS_H_
#include "src/interpreter/bytecode-array-builder.h"
#include <map>
#include "src/ast/ast-source-ranges.h"
#include "src/interpreter/block-coverage-builder.h"
#include "src/interpreter/bytecode-array-builder.h"
#include "src/interpreter/bytecode-generator.h"
#include "src/interpreter/bytecode-jump-table.h"
#include "src/interpreter/bytecode-label.h"
#include "src/zone/zone-containers.h"
......@@ -151,32 +153,49 @@ class V8_EXPORT_PRIVATE SwitchBuilder final
public:
SwitchBuilder(BytecodeArrayBuilder* builder,
BlockCoverageBuilder* block_coverage_builder,
SwitchStatement* statement, int number_of_cases)
SwitchStatement* statement, int number_of_cases,
BytecodeJumpTable* jump_table)
: BreakableControlFlowBuilder(builder, block_coverage_builder, statement),
case_sites_(builder->zone()) {
case_sites_(builder->zone()),
default_(builder->zone()),
fall_through_(builder->zone()),
jump_table_(jump_table) {
case_sites_.resize(number_of_cases);
}
~SwitchBuilder() override;
// This method should be called by the SwitchBuilder owner when the case
// statement with |index| is emitted to update the case jump site.
void SetCaseTarget(int index, CaseClause* clause);
void BindCaseTargetForJumpTable(int case_value, CaseClause* clause);
void BindCaseTargetForCompareJump(int index, CaseClause* clause);
// This method is called when visiting case comparison operation for |index|.
// Inserts a JumpIfTrue with ToBooleanMode |mode| to a unbound label that is
// patched when the corresponding SetCaseTarget is called.
void Case(BytecodeArrayBuilder::ToBooleanMode mode, int index) {
builder()->JumpIfTrue(mode, &case_sites_.at(index));
}
void JumpToCaseIfTrue(BytecodeArrayBuilder::ToBooleanMode mode, int index);
// This method is called when all cases comparisons have been emitted if there
// is a default case statement. Inserts a Jump to a unbound label that is
// patched when the corresponding SetCaseTarget is called.
void DefaultAt(int index) { builder()->Jump(&case_sites_.at(index)); }
void EmitJumpTableIfExists(int min_case, int max_case,
std::map<int, CaseClause*>& covered_cases);
void BindDefault(CaseClause* clause);
void JumpToDefault();
void JumpToFallThroughIfFalse();
private:
// Unbound labels that identify jumps for case statements in the code.
ZoneVector<BytecodeLabel> case_sites_;
BytecodeLabels default_;
BytecodeLabels fall_through_;
BytecodeJumpTable* jump_table_;
void BuildBlockCoverage(CaseClause* clause) {
if (block_coverage_builder_ && clause != nullptr) {
block_coverage_builder_->IncrementBlockCounter(clause,
SourceRangeKind::kBody);
}
}
};
// A class to help with co-ordinating control flow in try-catch statements.
......
......@@ -2181,6 +2181,7 @@ IGNITION_HANDLER(JumpLoop, InterpreterAssembler) {
// case_value falls outside of the table |table_length|, fall-through to the
// next bytecode.
IGNITION_HANDLER(SwitchOnSmiNoFeedback, InterpreterAssembler) {
// The accumulator must be a Smi.
TNode<Object> acc = GetAccumulator();
TNode<UintPtrT> table_start = BytecodeOperandIdx(0);
TNode<UintPtrT> table_length = BytecodeOperandUImmWord(1);
......@@ -2188,14 +2189,19 @@ IGNITION_HANDLER(SwitchOnSmiNoFeedback, InterpreterAssembler) {
Label fall_through(this);
// The accumulator must be a Smi.
// TODO(leszeks): Add a bytecode with type feedback that allows other
// accumulator values.
// TODO(leszeks): Use this as an alternative to adding extra bytecodes ahead
// of a jump-table optimized switch statement, using this code, in lieu of the
// current case_value line.
// TNode<IntPtrT> acc_intptr = TryTaggedToInt32AsIntPtr(acc, &fall_through);
// TNode<IntPtrT> case_value = IntPtrSub(acc_intptr, case_value_base);
CSA_ASSERT(this, TaggedIsSmi(acc));
TNode<IntPtrT> case_value = IntPtrSub(SmiUntag(CAST(acc)), case_value_base);
GotoIf(IntPtrLessThan(case_value, IntPtrConstant(0)), &fall_through);
GotoIf(IntPtrGreaterThanOrEqual(case_value, table_length), &fall_through);
TNode<WordT> entry = IntPtrAdd(table_start, case_value);
TNode<IntPtrT> relative_jump = LoadAndUntagConstantPoolEntry(entry);
Jump(relative_jump);
......
// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
load('../base.js');
load('switch_statement.js');
var success = true;
function PrintResult(name, result) {
print(name + '-SwitchStatement(Score): ' + result);
}
function PrintError(name, error) {
PrintResult(name, error);
success = false;
}
BenchmarkSuite.config.doWarmup = undefined;
BenchmarkSuite.config.doDeterministic = undefined;
BenchmarkSuite.RunSuites({ NotifyResult: PrintResult,
NotifyError: PrintError });
// Copyright 2017 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// running on old version takes approximately 50 seconds, so 50,000 milliseconds
new BenchmarkSuite('Big-Switch', [50000], [
new Benchmark('Big-Switch', false, false, 0, BigSwitch),
]);
function BigSwitch() {
"use strict";
const n = 100000;
const c = (a, b) => Array(a).fill().map((a, c) => b(c));
Function('n, c',
`
const a = c(n, a => a);
let ctr = 0;
for(let i = 0; i !== (1+n); i++){
switch(i){
${c(n, a => `case ${a}: ctr += i; break;`).join('\n')}
default: ctr += i; break;
}
}
return ctr;
`)(n,c);
}
......@@ -262,6 +262,25 @@ assertEquals(2016, f6(64), "largeSwitch.64");
assertEquals(4032, f6(128), "largeSwitch.128");
assertEquals(4222, f6(148), "largeSwitch.148");
function fhole(x) {
switch(x){
case 0:
x = 2;
case 2:
x = 3;
case 3:
x = 4;
case 4:
x = 5;
break;
case 5:
x = 6;
break;
}
return x;
}
assertEquals(1, fhole(1), "fhole.jumptablehole");
function f7(value) {
switch (value) {
......@@ -305,6 +324,120 @@ assertEquals("default", f7(1<<30), "0-1-switch.maxsmi++");
assertEquals("default", f7(-(1<<30)-1), "0-1-switch.minsmi--");
assertEquals("A", f7((170/16)-(170%16/16)), "0-1-switch.heapnum");
function zeroCheck1(value){
switch(value){
case -0:
return 313;
case -5:
return 5;
case -4:
return 4;
case -3:
return 3;
case -2:
return 1;
case -1:
case 0:
return 291949;
}
return 0;
}
assertEquals(313, zeroCheck1(0), "zero-check-1.1");
assertEquals(313, zeroCheck1(0.0), "zero-check-1.2");
assertEquals(313, zeroCheck1(-0), "zero-check-1.3");
assertEquals(291949, zeroCheck1(-1), "zero-check-1.4");
function zeroCheck2(value){
switch(value){
case 0:
return 291;
case -5:
return 5;
case -4:
case -0:
return 313;
case -3:
return 3;
case -2:
return 1;
case -1:
return 10;
}
return 0;
}
assertEquals(291, zeroCheck2(0), "zero-check-2.1");
assertEquals(291, zeroCheck2(0.0), "zero-check-2.2");
assertEquals(291, zeroCheck2(-0), "zero-check-2.3");
assertEquals(313, zeroCheck2(-4), "zero-check-2.4");
function duplicateCaseCheck(value){
switch(value){
case 1:
return 291;
case 2.0:
return 5;
case 3:
case 1.0:
return 324;
case 4:
case 2:
return 15;
case 5:
case 6:
return 18;
}
return 0;
}
assertEquals(291, duplicateCaseCheck(1.0), "duplicate-check.1");
assertEquals(324, duplicateCaseCheck(3), "duplicate-check.2");
assertEquals(15, duplicateCaseCheck(4), "duplicate-check.3");
assertEquals(5, duplicateCaseCheck(2), "duplicate-check.4");
function jumpTableHoleAliasCheck(value){
let y = 4;
switch(value){
case 0: return 10;
case 1: return 20;
case 2: return 30;
case 3: return 40;
case 5: return 60;
case 6: return 70;
case y: return 50;
}
return 0;
}
assertEquals(50, jumpTableHoleAliasCheck(4), "jump-table-hole-alias.1");
function caseSideEffects(value){
let y = 2;
switch(value){
case y--:
return 'first!';
case 3:
return 'const1';
case 4:
return 'const2';
case 5:
return 'const3';
case 6:
return 'const4';
case 7:
return 'const5';
case 8:
return 'const6';
case y:
return 'wow';
case 1:
return 'ouch';
}
return '';
}
assertEquals('wow', caseSideEffects(1), "case-side-effects.1");
function makeVeryLong(length) {
var res = "(function () {\n" +
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment