LCOV - code coverage report
Current view: top level - test/unittests/compiler/regalloc - move-optimizer-unittest.cc (source / functions) Hit Total Coverage
Test: app.info Lines: 284 289 98.3 %
Date: 2019-04-17 Functions: 29 39 74.4 %

          Line data    Source code
       1             : // Copyright 2014 the V8 project authors. All rights reserved.
       2             : // Use of this source code is governed by a BSD-style license that can be
       3             : // found in the LICENSE file.
       4             : 
       5             : #include "src/compiler/backend/move-optimizer.h"
       6             : #include "src/ostreams.h"
       7             : #include "test/unittests/compiler/backend/instruction-sequence-unittest.h"
       8             : 
       9             : namespace v8 {
      10             : namespace internal {
      11             : namespace compiler {
      12             : 
      13          20 : class MoveOptimizerTest : public InstructionSequenceTest {
      14             :  public:
      15             :   // FP register indices which don't interfere under simple or complex aliasing.
      16             :   static const int kF64_1 = 0;
      17             :   static const int kF64_2 = 1;
      18             :   static const int kF32_1 = 4;
      19             :   static const int kF32_2 = 5;
      20             :   static const int kS128_1 = 2;
      21             :   static const int kS128_2 = 3;
      22             : 
      23          56 :   Instruction* LastInstruction() { return sequence()->instructions().back(); }
      24             : 
      25          58 :   void AddMove(Instruction* instr, TestOperand from, TestOperand to,
      26             :                Instruction::GapPosition pos = Instruction::START) {
      27             :     auto parallel_move = instr->GetOrCreateParallelMove(pos, zone());
      28         116 :     parallel_move->AddMove(ConvertMoveArg(from), ConvertMoveArg(to));
      29          58 :   }
      30             : 
      31          19 :   int NonRedundantSize(ParallelMove* moves) {
      32             :     int i = 0;
      33          70 :     for (auto move : *moves) {
      34          51 :       if (move->IsRedundant()) continue;
      35          31 :       i++;
      36             :     }
      37          19 :     return i;
      38             :   }
      39             : 
      40          31 :   bool Contains(ParallelMove* moves, TestOperand from_op, TestOperand to_op) {
      41          31 :     auto from = ConvertMoveArg(from_op);
      42          31 :     auto to = ConvertMoveArg(to_op);
      43          85 :     for (auto move : *moves) {
      44          85 :       if (move->IsRedundant()) continue;
      45         115 :       if (move->source().Equals(from) && move->destination().Equals(to)) {
      46             :         return true;
      47             :       }
      48             :     }
      49             :     return false;
      50             :   }
      51             : 
      52             :   // TODO(dcarney): add a verifier.
      53          10 :   void Optimize() {
      54          10 :     WireBlocks();
      55          10 :     if (FLAG_trace_turbo) {
      56             :       StdoutStream{}
      57           0 :           << "----- Instruction sequence before move optimization -----\n"
      58           0 :           << *sequence();
      59             :     }
      60          20 :     MoveOptimizer move_optimizer(zone(), sequence());
      61          10 :     move_optimizer.Run();
      62          10 :     if (FLAG_trace_turbo) {
      63             :       StdoutStream{}
      64           0 :           << "----- Instruction sequence after move optimization -----\n"
      65           0 :           << *sequence();
      66             :     }
      67          10 :   }
      68             : 
      69             :  private:
      70           2 :   bool DoesRegisterAllocation() const override { return false; }
      71             : 
      72         178 :   InstructionOperand ConvertMoveArg(TestOperand op) {
      73         178 :     CHECK_EQ(kNoValue, op.vreg_.value_);
      74         178 :     CHECK_NE(kNoValue, op.value_);
      75         178 :     switch (op.type_) {
      76             :       case kConstant:
      77           6 :         return ConstantOperand(op.value_);
      78             :       case kFixedSlot:
      79             :         return AllocatedOperand(LocationOperand::STACK_SLOT,
      80           6 :                                 MachineRepresentation::kWord32, op.value_);
      81             :       case kFixedRegister: {
      82         158 :         MachineRepresentation rep = GetCanonicalRep(op);
      83         158 :         CHECK(0 <= op.value_ && op.value_ < GetNumRegs(rep));
      84         158 :         return AllocatedOperand(LocationOperand::REGISTER, rep, op.value_);
      85             :       }
      86             :       case kExplicit: {
      87           8 :         MachineRepresentation rep = GetCanonicalRep(op);
      88           8 :         CHECK(0 <= op.value_ && op.value_ < GetNumRegs(rep));
      89           8 :         return ExplicitOperand(LocationOperand::REGISTER, rep, op.value_);
      90             :       }
      91             :       default:
      92             :         break;
      93             :     }
      94           0 :     UNREACHABLE();
      95             :   }
      96             : };
      97             : 
      98       15444 : TEST_F(MoveOptimizerTest, RemovesRedundant) {
      99           1 :   StartBlock();
     100           1 :   auto first_instr = EmitNop();
     101           1 :   auto last_instr = EmitNop();
     102             : 
     103           1 :   AddMove(first_instr, Reg(0), Reg(1));
     104           1 :   AddMove(last_instr, Reg(1), Reg(0));
     105             : 
     106           1 :   AddMove(first_instr, FPReg(kS128_1, kSimd128), FPReg(kS128_2, kSimd128));
     107           1 :   AddMove(last_instr, FPReg(kS128_2, kSimd128), FPReg(kS128_1, kSimd128));
     108           1 :   AddMove(first_instr, FPReg(kF64_1, kFloat64), FPReg(kF64_2, kFloat64));
     109           1 :   AddMove(last_instr, FPReg(kF64_2, kFloat64), FPReg(kF64_1, kFloat64));
     110           1 :   AddMove(first_instr, FPReg(kF32_1, kFloat32), FPReg(kF32_2, kFloat32));
     111           1 :   AddMove(last_instr, FPReg(kF32_2, kFloat32), FPReg(kF32_1, kFloat32));
     112             : 
     113           1 :   EndBlock(Last());
     114             : 
     115           1 :   Optimize();
     116             : 
     117           1 :   CHECK_EQ(0, NonRedundantSize(first_instr->parallel_moves()[0]));
     118           1 :   auto move = last_instr->parallel_moves()[0];
     119           1 :   CHECK_EQ(4, NonRedundantSize(move));
     120           1 :   CHECK(Contains(move, Reg(0), Reg(1)));
     121           1 :   CHECK(Contains(move, FPReg(kS128_1, kSimd128), FPReg(kS128_2, kSimd128)));
     122           1 :   CHECK(Contains(move, FPReg(kF64_1, kFloat64), FPReg(kF64_2, kFloat64)));
     123           1 :   CHECK(Contains(move, FPReg(kF32_1, kFloat32), FPReg(kF32_2, kFloat32)));
     124           1 : }
     125             : 
     126       15444 : TEST_F(MoveOptimizerTest, RemovesRedundantExplicit) {
     127           1 :   int index1 = GetAllocatableCode(0);
     128           1 :   int index2 = GetAllocatableCode(1);
     129           1 :   int s128_1 = GetAllocatableCode(kS128_1, kSimd128);
     130           1 :   int s128_2 = GetAllocatableCode(kS128_2, kSimd128);
     131           1 :   int f64_1 = GetAllocatableCode(kF64_1, kFloat64);
     132           1 :   int f64_2 = GetAllocatableCode(kF64_2, kFloat64);
     133           1 :   int f32_1 = GetAllocatableCode(kF32_1, kFloat32);
     134           1 :   int f32_2 = GetAllocatableCode(kF32_2, kFloat32);
     135             : 
     136           1 :   StartBlock();
     137           1 :   auto first_instr = EmitNop();
     138           1 :   auto last_instr = EmitNop();
     139             : 
     140           1 :   AddMove(first_instr, Reg(index1), ExplicitReg(index2));
     141           1 :   AddMove(last_instr, Reg(index2), Reg(index1));
     142             : 
     143             :   AddMove(first_instr, FPReg(s128_1, kSimd128),
     144           1 :           ExplicitFPReg(s128_2, kSimd128));
     145           1 :   AddMove(last_instr, FPReg(s128_2, kSimd128), FPReg(s128_1, kSimd128));
     146           1 :   AddMove(first_instr, FPReg(f64_1, kFloat64), ExplicitFPReg(f64_2, kFloat64));
     147           1 :   AddMove(last_instr, FPReg(f64_2, kFloat64), FPReg(f64_1, kFloat64));
     148           1 :   AddMove(first_instr, FPReg(f32_1, kFloat32), ExplicitFPReg(f32_2, kFloat32));
     149           1 :   AddMove(last_instr, FPReg(f32_2, kFloat32), FPReg(f32_1, kFloat32));
     150             : 
     151           1 :   EndBlock(Last());
     152             : 
     153           1 :   Optimize();
     154             : 
     155           1 :   CHECK_EQ(0, NonRedundantSize(first_instr->parallel_moves()[0]));
     156           1 :   auto move = last_instr->parallel_moves()[0];
     157           1 :   CHECK_EQ(4, NonRedundantSize(move));
     158           1 :   CHECK(Contains(move, Reg(index1), ExplicitReg(index2)));
     159           1 :   CHECK(
     160             :       Contains(move, FPReg(s128_1, kSimd128), ExplicitFPReg(s128_2, kSimd128)));
     161           1 :   CHECK(Contains(move, FPReg(f64_1, kFloat64), ExplicitFPReg(f64_2, kFloat64)));
     162           1 :   CHECK(Contains(move, FPReg(f32_1, kFloat32), ExplicitFPReg(f32_2, kFloat32)));
     163           1 : }
     164             : 
     165       15444 : TEST_F(MoveOptimizerTest, SplitsConstants) {
     166           1 :   StartBlock();
     167           1 :   EndBlock(Last());
     168             : 
     169           1 :   auto gap = LastInstruction();
     170           1 :   AddMove(gap, Const(1), Slot(0));
     171           1 :   AddMove(gap, Const(1), Slot(1));
     172           1 :   AddMove(gap, Const(1), Reg(0));
     173           1 :   AddMove(gap, Const(1), Slot(2));
     174             : 
     175           1 :   Optimize();
     176             : 
     177           1 :   auto move = gap->parallel_moves()[0];
     178           1 :   CHECK_EQ(1, NonRedundantSize(move));
     179           1 :   CHECK(Contains(move, Const(1), Reg(0)));
     180             : 
     181           1 :   move = gap->parallel_moves()[1];
     182           1 :   CHECK_EQ(3, NonRedundantSize(move));
     183           1 :   CHECK(Contains(move, Reg(0), Slot(0)));
     184           1 :   CHECK(Contains(move, Reg(0), Slot(1)));
     185           1 :   CHECK(Contains(move, Reg(0), Slot(2)));
     186           1 : }
     187             : 
     188       15444 : TEST_F(MoveOptimizerTest, SimpleMerge) {
     189           1 :   StartBlock();
     190           2 :   EndBlock(Branch(Imm(), 1, 2));
     191             : 
     192           1 :   StartBlock();
     193           1 :   EndBlock(Jump(2));
     194           1 :   AddMove(LastInstruction(), Reg(0), Reg(1));
     195           1 :   AddMove(LastInstruction(), FPReg(kS128_1, kSimd128),
     196           1 :           FPReg(kS128_2, kSimd128));
     197           1 :   AddMove(LastInstruction(), FPReg(kF64_1, kFloat64), FPReg(kF64_2, kFloat64));
     198           1 :   AddMove(LastInstruction(), FPReg(kF32_1, kFloat32), FPReg(kF32_2, kFloat32));
     199             : 
     200           1 :   StartBlock();
     201           1 :   EndBlock(Jump(1));
     202           1 :   AddMove(LastInstruction(), Reg(0), Reg(1));
     203           1 :   AddMove(LastInstruction(), FPReg(kS128_1, kSimd128),
     204           1 :           FPReg(kS128_2, kSimd128));
     205           1 :   AddMove(LastInstruction(), FPReg(kF64_1, kFloat64), FPReg(kF64_2, kFloat64));
     206           1 :   AddMove(LastInstruction(), FPReg(kF32_1, kFloat32), FPReg(kF32_2, kFloat32));
     207             : 
     208           1 :   StartBlock();
     209           1 :   EndBlock(Last());
     210             : 
     211           1 :   auto last = LastInstruction();
     212             : 
     213           1 :   Optimize();
     214             : 
     215           1 :   auto move = last->parallel_moves()[0];
     216           1 :   CHECK_EQ(4, NonRedundantSize(move));
     217           1 :   CHECK(Contains(move, Reg(0), Reg(1)));
     218           1 :   CHECK(Contains(move, FPReg(kS128_1, kSimd128), FPReg(kS128_2, kSimd128)));
     219           1 :   CHECK(Contains(move, FPReg(kF64_1, kFloat64), FPReg(kF64_2, kFloat64)));
     220           1 :   CHECK(Contains(move, FPReg(kF32_1, kFloat32), FPReg(kF32_2, kFloat32)));
     221           1 : }
     222             : 
     223       15444 : TEST_F(MoveOptimizerTest, SimpleMergeCycle) {
     224           1 :   StartBlock();
     225           2 :   EndBlock(Branch(Imm(), 1, 2));
     226             : 
     227           1 :   StartBlock();
     228           1 :   EndBlock(Jump(2));
     229           1 :   auto gap_0 = LastInstruction();
     230           1 :   AddMove(gap_0, Reg(0), Reg(1));
     231           1 :   AddMove(LastInstruction(), Reg(1), Reg(0));
     232             : 
     233           1 :   AddMove(gap_0, FPReg(kS128_1, kSimd128), FPReg(kS128_2, kSimd128));
     234           1 :   AddMove(LastInstruction(), FPReg(kS128_2, kSimd128),
     235           1 :           FPReg(kS128_1, kSimd128));
     236           1 :   AddMove(gap_0, FPReg(kF64_1, kFloat64), FPReg(kF64_2, kFloat64));
     237           1 :   AddMove(LastInstruction(), FPReg(kF64_2, kFloat64), FPReg(kF64_1, kFloat64));
     238           1 :   AddMove(gap_0, FPReg(kF32_1, kFloat32), FPReg(kF32_2, kFloat32));
     239           1 :   AddMove(LastInstruction(), FPReg(kF32_2, kFloat32), FPReg(kF32_1, kFloat32));
     240             : 
     241           1 :   StartBlock();
     242           1 :   EndBlock(Jump(1));
     243           1 :   auto gap_1 = LastInstruction();
     244           1 :   AddMove(gap_1, Reg(0), Reg(1));
     245           1 :   AddMove(gap_1, Reg(1), Reg(0));
     246           1 :   AddMove(gap_1, FPReg(kS128_1, kSimd128), FPReg(kS128_2, kSimd128));
     247           1 :   AddMove(gap_1, FPReg(kS128_2, kSimd128), FPReg(kS128_1, kSimd128));
     248           1 :   AddMove(gap_1, FPReg(kF64_1, kFloat64), FPReg(kF64_2, kFloat64));
     249           1 :   AddMove(gap_1, FPReg(kF64_2, kFloat64), FPReg(kF64_1, kFloat64));
     250           1 :   AddMove(gap_1, FPReg(kF32_1, kFloat32), FPReg(kF32_2, kFloat32));
     251           1 :   AddMove(gap_1, FPReg(kF32_2, kFloat32), FPReg(kF32_1, kFloat32));
     252             : 
     253           1 :   StartBlock();
     254           1 :   EndBlock(Last());
     255             : 
     256           1 :   auto last = LastInstruction();
     257             : 
     258           1 :   Optimize();
     259             : 
     260           1 :   CHECK(gap_0->AreMovesRedundant());
     261           1 :   CHECK(gap_1->AreMovesRedundant());
     262           1 :   auto move = last->parallel_moves()[0];
     263           1 :   CHECK_EQ(8, NonRedundantSize(move));
     264           1 :   CHECK(Contains(move, Reg(0), Reg(1)));
     265           1 :   CHECK(Contains(move, Reg(1), Reg(0)));
     266           1 :   CHECK(Contains(move, FPReg(kS128_1, kSimd128), FPReg(kS128_2, kSimd128)));
     267           1 :   CHECK(Contains(move, FPReg(kS128_2, kSimd128), FPReg(kS128_1, kSimd128)));
     268           1 :   CHECK(Contains(move, FPReg(kF64_1, kFloat64), FPReg(kF64_2, kFloat64)));
     269           1 :   CHECK(Contains(move, FPReg(kF64_2, kFloat64), FPReg(kF64_1, kFloat64)));
     270           1 :   CHECK(Contains(move, FPReg(kF32_1, kFloat32), FPReg(kF32_2, kFloat32)));
     271           1 :   CHECK(Contains(move, FPReg(kF32_2, kFloat32), FPReg(kF32_1, kFloat32)));
     272           1 : }
     273             : 
     274       15444 : TEST_F(MoveOptimizerTest, GapsCanMoveOverInstruction) {
     275           1 :   StartBlock();
     276             :   int const_index = 1;
     277           1 :   DefineConstant(const_index);
     278           1 :   Instruction* ctant_def = LastInstruction();
     279           1 :   AddMove(ctant_def, Reg(1), Reg(0));
     280             : 
     281           1 :   Instruction* last = EmitNop();
     282           1 :   AddMove(last, Const(const_index), Reg(0));
     283           1 :   AddMove(last, Reg(0), Reg(1));
     284           1 :   EndBlock(Last());
     285           1 :   Optimize();
     286             : 
     287             :   ParallelMove* inst1_start =
     288             :       ctant_def->GetParallelMove(Instruction::GapPosition::START);
     289             :   ParallelMove* inst1_end =
     290             :       ctant_def->GetParallelMove(Instruction::GapPosition::END);
     291             :   ParallelMove* last_start =
     292             :       last->GetParallelMove(Instruction::GapPosition::START);
     293           1 :   CHECK(inst1_start == nullptr || NonRedundantSize(inst1_start) == 0);
     294           1 :   CHECK(inst1_end == nullptr || NonRedundantSize(inst1_end) == 0);
     295           1 :   CHECK_EQ(2, last_start->size());
     296             :   int redundants = 0;
     297             :   int assignment = 0;
     298           3 :   for (MoveOperands* move : *last_start) {
     299           2 :     if (move->IsRedundant()) {
     300           1 :       ++redundants;
     301             :     } else {
     302           1 :       ++assignment;
     303           1 :       CHECK(move->destination().IsRegister());
     304           1 :       CHECK(move->source().IsConstant());
     305             :     }
     306             :   }
     307           1 :   CHECK_EQ(1, redundants);
     308           1 :   CHECK_EQ(1, assignment);
     309           1 : }
     310             : 
     311       15444 : TEST_F(MoveOptimizerTest, SubsetMovesMerge) {
     312           1 :   StartBlock();
     313           2 :   EndBlock(Branch(Imm(), 1, 2));
     314             : 
     315           1 :   StartBlock();
     316           1 :   EndBlock(Jump(2));
     317           1 :   Instruction* last_move_b1 = LastInstruction();
     318           1 :   AddMove(last_move_b1, Reg(0), Reg(1));
     319           1 :   AddMove(last_move_b1, Reg(2), Reg(3));
     320             : 
     321           1 :   StartBlock();
     322           1 :   EndBlock(Jump(1));
     323           1 :   Instruction* last_move_b2 = LastInstruction();
     324           1 :   AddMove(last_move_b2, Reg(0), Reg(1));
     325           1 :   AddMove(last_move_b2, Reg(4), Reg(5));
     326             : 
     327           1 :   StartBlock();
     328           1 :   EndBlock(Last());
     329             : 
     330           1 :   Instruction* last = LastInstruction();
     331             : 
     332           1 :   Optimize();
     333             : 
     334           1 :   ParallelMove* last_move = last->parallel_moves()[0];
     335           1 :   CHECK_EQ(1, NonRedundantSize(last_move));
     336           1 :   CHECK(Contains(last_move, Reg(0), Reg(1)));
     337             : 
     338           1 :   ParallelMove* b1_move = last_move_b1->parallel_moves()[0];
     339           1 :   CHECK_EQ(1, NonRedundantSize(b1_move));
     340           1 :   CHECK(Contains(b1_move, Reg(2), Reg(3)));
     341             : 
     342           1 :   ParallelMove* b2_move = last_move_b2->parallel_moves()[0];
     343           1 :   CHECK_EQ(1, NonRedundantSize(b2_move));
     344           1 :   CHECK(Contains(b2_move, Reg(4), Reg(5)));
     345           1 : }
     346             : 
     347       15444 : TEST_F(MoveOptimizerTest, GapConflictSubsetMovesDoNotMerge) {
     348           1 :   StartBlock();
     349           2 :   EndBlock(Branch(Imm(), 1, 2));
     350             : 
     351           1 :   StartBlock();
     352           1 :   EndBlock(Jump(2));
     353           1 :   Instruction* last_move_b1 = LastInstruction();
     354           1 :   AddMove(last_move_b1, Reg(0), Reg(1));
     355           1 :   AddMove(last_move_b1, Reg(2), Reg(0));
     356           1 :   AddMove(last_move_b1, Reg(4), Reg(5));
     357             : 
     358           1 :   StartBlock();
     359           1 :   EndBlock(Jump(1));
     360           1 :   Instruction* last_move_b2 = LastInstruction();
     361           1 :   AddMove(last_move_b2, Reg(0), Reg(1));
     362           1 :   AddMove(last_move_b2, Reg(4), Reg(5));
     363             : 
     364           1 :   StartBlock();
     365           1 :   EndBlock(Last());
     366             : 
     367           1 :   Instruction* last = LastInstruction();
     368             : 
     369           1 :   Optimize();
     370             : 
     371           1 :   ParallelMove* last_move = last->parallel_moves()[0];
     372           1 :   CHECK_EQ(1, NonRedundantSize(last_move));
     373           1 :   CHECK(Contains(last_move, Reg(4), Reg(5)));
     374             : 
     375           1 :   ParallelMove* b1_move = last_move_b1->parallel_moves()[0];
     376           1 :   CHECK_EQ(2, NonRedundantSize(b1_move));
     377           1 :   CHECK(Contains(b1_move, Reg(0), Reg(1)));
     378           1 :   CHECK(Contains(b1_move, Reg(2), Reg(0)));
     379             : 
     380           1 :   ParallelMove* b2_move = last_move_b2->parallel_moves()[0];
     381           1 :   CHECK_EQ(1, NonRedundantSize(b2_move));
     382           1 :   CHECK(Contains(b1_move, Reg(0), Reg(1)));
     383           1 : }
     384             : 
     385       15444 : TEST_F(MoveOptimizerTest, ClobberedDestinationsAreEliminated) {
     386           1 :   StartBlock();
     387           1 :   EmitNop();
     388           1 :   Instruction* first_instr = LastInstruction();
     389           1 :   AddMove(first_instr, Reg(0), Reg(1));
     390           1 :   EmitOI(Reg(1), 0, nullptr);
     391           1 :   Instruction* last_instr = LastInstruction();
     392           1 :   EndBlock();
     393           1 :   Optimize();
     394             : 
     395           1 :   ParallelMove* first_move = first_instr->parallel_moves()[0];
     396           1 :   CHECK_EQ(0, NonRedundantSize(first_move));
     397             : 
     398           1 :   ParallelMove* last_move = last_instr->parallel_moves()[0];
     399           1 :   CHECK_EQ(0, NonRedundantSize(last_move));
     400           1 : }
     401             : 
     402       15444 : TEST_F(MoveOptimizerTest, ClobberedFPDestinationsAreEliminated) {
     403           1 :   StartBlock();
     404           1 :   EmitNop();
     405           1 :   Instruction* first_instr = LastInstruction();
     406           1 :   AddMove(first_instr, FPReg(4, kFloat64), FPReg(1, kFloat64));
     407             :   if (!kSimpleFPAliasing) {
     408             :     // We clobber q0 below. This is aliased by d0, d1, s0, s1, s2, and s3.
     409             :     // Add moves to registers s2 and s3.
     410             :     AddMove(first_instr, FPReg(10, kFloat32), FPReg(0, kFloat32));
     411             :     AddMove(first_instr, FPReg(11, kFloat32), FPReg(1, kFloat32));
     412             :   }
     413             :   // Clobbers output register 0.
     414           1 :   EmitOI(FPReg(0, kSimd128), 0, nullptr);
     415           1 :   Instruction* last_instr = LastInstruction();
     416           1 :   EndBlock();
     417           1 :   Optimize();
     418             : 
     419           1 :   ParallelMove* first_move = first_instr->parallel_moves()[0];
     420           1 :   CHECK_EQ(0, NonRedundantSize(first_move));
     421             : 
     422           1 :   ParallelMove* last_move = last_instr->parallel_moves()[0];
     423           1 :   CHECK_EQ(0, NonRedundantSize(last_move));
     424           1 : }
     425             : 
     426             : }  // namespace compiler
     427             : }  // namespace internal
     428        9264 : }  // namespace v8

Generated by: LCOV version 1.10