github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/internal/engine/wazevo/ssa/pass_blk_layouts_test.go (about) 1 package ssa 2 3 import ( 4 "testing" 5 6 "github.com/tetratelabs/wazero/internal/testing/require" 7 ) 8 9 func Test_maybeInvertBranch(t *testing.T) { 10 insertJump := func(b *builder, src, dst *basicBlock) { 11 b.SetCurrentBlock(src) 12 jump := b.AllocateInstruction() 13 jump.AsJump(ValuesNil, dst) 14 b.InsertInstruction(jump) 15 } 16 17 insertBrz := func(b *builder, src, dst *basicBlock) { 18 b.SetCurrentBlock(src) 19 vinst := b.AllocateInstruction() 20 vinst.AsIconst32(0) 21 b.InsertInstruction(vinst) 22 v := vinst.Return() 23 brz := b.AllocateInstruction() 24 brz.AsBrz(v, ValuesNil, dst) 25 b.InsertInstruction(brz) 26 } 27 28 for _, tc := range []struct { 29 name string 30 setup func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) 31 exp bool 32 }{ 33 { 34 name: "ends with br_table", 35 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 36 now, next = b.allocateBasicBlock(), b.allocateBasicBlock() 37 inst := b.AllocateInstruction() 38 // TODO: we haven't implemented AsBrTable on Instruction. 39 inst.opcode = OpcodeBrTable 40 now.currentInstr = inst 41 verify = func(t *testing.T) { require.Equal(t, OpcodeBrTable, inst.opcode) } 42 return 43 }, 44 }, 45 { 46 name: "no conditional branch without previous instruction", 47 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 48 now, next = b.allocateBasicBlock(), b.allocateBasicBlock() 49 insertJump(b, now, next) 50 verify = func(t *testing.T) { 51 tail := now.currentInstr 52 require.Equal(t, OpcodeJump, tail.opcode) 53 } 54 return 55 }, 56 }, 57 { 58 name: "no conditional branch with previous instruction", 59 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 60 now, next = b.allocateBasicBlock(), b.allocateBasicBlock() 61 b.SetCurrentBlock(now) 62 prev := b.AllocateInstruction() 63 prev.AsIconst64(1) 64 b.InsertInstruction(prev) 65 insertJump(b, now, next) 66 verify = func(t *testing.T) { 67 tail := now.currentInstr 68 require.Equal(t, OpcodeJump, tail.opcode) 69 require.Equal(t, prev, tail.prev) 70 } 71 return 72 }, 73 }, 74 { 75 name: "tail target is already loop", 76 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 77 now, next, loopHeader, dummy := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 78 loopHeader.loopHeader = true 79 insertBrz(b, now, dummy) 80 insertJump(b, now, loopHeader) 81 verify = func(t *testing.T) { 82 tail := now.currentInstr 83 conditionalBr := tail.prev 84 require.Equal(t, OpcodeJump, tail.opcode) 85 require.Equal(t, OpcodeBrz, conditionalBr.opcode) // intact. 86 require.Equal(t, conditionalBr, tail.prev) 87 } 88 return 89 }, 90 }, 91 { 92 name: "tail target is already the next block", 93 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 94 now, next, dummy := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 95 insertBrz(b, now, dummy) 96 insertJump(b, now, next) 97 verify = func(t *testing.T) { 98 tail := now.currentInstr 99 conditionalBr := tail.prev 100 require.Equal(t, OpcodeJump, tail.opcode) 101 require.Equal(t, OpcodeBrz, conditionalBr.opcode) // intact. 102 require.Equal(t, conditionalBr, tail.prev) 103 } 104 return 105 }, 106 }, 107 { 108 name: "conditional target is loop", 109 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 110 now, next, loopHeader := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 111 loopHeader.loopHeader = true 112 insertBrz(b, now, loopHeader) // jump to loop, which needs inversion. 113 insertJump(b, now, next) 114 115 tail := now.currentInstr 116 conditionalBr := tail.prev 117 118 // Sanity check before inversion. 119 require.Equal(t, conditionalBr, loopHeader.preds[0].branch) 120 require.Equal(t, tail, next.preds[0].branch) 121 verify = func(t *testing.T) { 122 require.Equal(t, OpcodeJump, tail.opcode) 123 require.Equal(t, OpcodeBrnz, conditionalBr.opcode) // inversion. 124 require.Equal(t, loopHeader, tail.blk) // swapped. 125 require.Equal(t, next, conditionalBr.blk) // swapped. 126 require.Equal(t, conditionalBr, tail.prev) 127 128 // Predecessor info should correctly point to the inverted jump instruction. 129 require.Equal(t, tail, loopHeader.preds[0].branch) 130 require.Equal(t, conditionalBr, next.preds[0].branch) 131 } 132 return 133 }, 134 exp: true, 135 }, 136 { 137 name: "conditional target is the next block", 138 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 139 now, next = b.allocateBasicBlock(), b.allocateBasicBlock() 140 nowTarget := b.allocateBasicBlock() 141 insertBrz(b, now, next) // jump to the next block in conditional, which needs inversion. 142 insertJump(b, now, nowTarget) 143 144 tail := now.currentInstr 145 conditionalBr := tail.prev 146 147 // Sanity check before inversion. 148 require.Equal(t, tail, nowTarget.preds[0].branch) 149 require.Equal(t, conditionalBr, next.preds[0].branch) 150 151 verify = func(t *testing.T) { 152 require.Equal(t, OpcodeJump, tail.opcode) 153 require.Equal(t, OpcodeBrnz, conditionalBr.opcode) // inversion. 154 require.Equal(t, next, tail.blk) // swapped. 155 require.Equal(t, nowTarget, conditionalBr.blk) // swapped. 156 require.Equal(t, conditionalBr, tail.prev) 157 158 require.Equal(t, conditionalBr, nowTarget.preds[0].branch) 159 require.Equal(t, tail, next.preds[0].branch) 160 } 161 return 162 }, 163 exp: true, 164 }, 165 } { 166 t.Run(tc.name, func(t *testing.T) { 167 b := NewBuilder().(*builder) 168 now, next, verify := tc.setup(b) 169 actual := maybeInvertBranches(now, next) 170 verify(t) 171 require.Equal(t, tc.exp, actual) 172 }) 173 } 174 } 175 176 func TestBuilder_splitCriticalEdge(t *testing.T) { 177 b := NewBuilder().(*builder) 178 predBlk, dummyBlk, dummyBlk2 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 179 predBlk.reversePostOrder = 100 180 b.SetCurrentBlock(predBlk) 181 inst := b.AllocateInstruction() 182 inst.AsIconst32(1) 183 b.InsertInstruction(inst) 184 v := inst.Return() 185 originalBrz := b.AllocateInstruction() // This is the split edge. 186 originalBrz.AsBrz(v, ValuesNil, dummyBlk) 187 b.InsertInstruction(originalBrz) 188 dummyJump := b.AllocateInstruction() 189 dummyJump.AsJump(ValuesNil, dummyBlk2) 190 b.InsertInstruction(dummyJump) 191 192 predInfo := &basicBlockPredecessorInfo{blk: predBlk, branch: originalBrz} 193 trampoline := b.splitCriticalEdge(predBlk, dummyBlk, predInfo) 194 require.NotNil(t, trampoline) 195 require.Equal(t, 100, trampoline.reversePostOrder) 196 197 require.Equal(t, trampoline, predInfo.blk) 198 require.Equal(t, originalBrz, predInfo.branch) 199 require.Equal(t, trampoline.rootInstr, predInfo.branch) 200 require.Equal(t, trampoline.currentInstr, predInfo.branch) 201 require.Equal(t, trampoline.success[0], dummyBlk) 202 203 replacedBrz := predBlk.rootInstr.next 204 require.Equal(t, OpcodeBrz, replacedBrz.opcode) 205 require.Equal(t, trampoline, replacedBrz.blk) 206 } 207 208 func Test_swapInstruction(t *testing.T) { 209 t.Run("swap root", func(t *testing.T) { 210 b := NewBuilder().(*builder) 211 blk := b.allocateBasicBlock() 212 213 dummy := b.AllocateInstruction() 214 215 old := b.AllocateInstruction() 216 old.next, dummy.prev = dummy, old 217 newi := b.AllocateInstruction() 218 blk.rootInstr = old 219 swapInstruction(blk, old, newi) 220 221 require.Equal(t, newi, blk.rootInstr) 222 require.Equal(t, dummy, newi.next) 223 require.Equal(t, dummy.prev, newi) 224 require.Nil(t, old.next) 225 require.Nil(t, old.prev) 226 }) 227 t.Run("swap middle", func(t *testing.T) { 228 b := NewBuilder().(*builder) 229 blk := b.allocateBasicBlock() 230 b.SetCurrentBlock(blk) 231 i1, i2, i3 := b.AllocateInstruction(), b.AllocateInstruction(), b.AllocateInstruction() 232 i1.AsIconst32(1) 233 i2.AsIconst32(2) 234 i3.AsIconst32(3) 235 b.InsertInstruction(i1) 236 b.InsertInstruction(i2) 237 b.InsertInstruction(i3) 238 239 newi := b.AllocateInstruction() 240 newi.AsIconst32(100) 241 swapInstruction(blk, i2, newi) 242 243 require.Equal(t, i1, blk.rootInstr) 244 require.Equal(t, newi, i1.next) 245 require.Equal(t, i3, newi.next) 246 require.Equal(t, i1, newi.prev) 247 require.Equal(t, newi, i3.prev) 248 require.Nil(t, i2.next) 249 require.Nil(t, i2.prev) 250 }) 251 t.Run("swap tail", func(t *testing.T) { 252 b := NewBuilder().(*builder) 253 blk := b.allocateBasicBlock() 254 b.SetCurrentBlock(blk) 255 i1, i2 := b.AllocateInstruction(), b.AllocateInstruction() 256 i1.AsIconst32(1) 257 i2.AsIconst32(2) 258 b.InsertInstruction(i1) 259 b.InsertInstruction(i2) 260 261 newi := b.AllocateInstruction() 262 newi.AsIconst32(100) 263 swapInstruction(blk, i2, newi) 264 265 require.Equal(t, i1, blk.rootInstr) 266 require.Equal(t, newi, blk.currentInstr) 267 require.Equal(t, newi, i1.next) 268 require.Equal(t, i1, newi.prev) 269 require.Nil(t, newi.next) 270 require.Nil(t, i2.next) 271 require.Nil(t, i2.prev) 272 }) 273 } 274 275 func TestBuilder_LayoutBlocks(t *testing.T) { 276 insertJump := func(b *builder, src, dst *basicBlock, vs ...Value) { 277 b.SetCurrentBlock(src) 278 jump := b.AllocateInstruction() 279 args := b.varLengthPool.Allocate(len(vs)) 280 args = args.Append(&b.varLengthPool, vs...) 281 jump.AsJump(args, dst) 282 b.InsertInstruction(jump) 283 } 284 285 insertBrz := func(b *builder, src, dst *basicBlock, condVal Value, vs ...Value) { 286 b.SetCurrentBlock(src) 287 vinst := b.AllocateInstruction().AsIconst32(0) 288 b.InsertInstruction(vinst) 289 brz := b.AllocateInstruction() 290 args := b.varLengthPool.Allocate(len(vs)) 291 args = args.Append(&b.varLengthPool, vs...) 292 brz.AsBrz(condVal, args, dst) 293 b.InsertInstruction(brz) 294 } 295 296 for _, tc := range []struct { 297 name string 298 setup func(b *builder) 299 exp []BasicBlockID 300 }{ 301 { 302 name: "sequential - no critical edge", 303 setup: func(b *builder) { 304 b1, b2, b3, b4 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 305 insertJump(b, b1, b2) 306 insertJump(b, b2, b3) 307 insertJump(b, b3, b4) 308 b.Seal(b1) 309 b.Seal(b2) 310 b.Seal(b3) 311 b.Seal(b4) 312 }, 313 exp: []BasicBlockID{0, 1, 2, 3}, 314 }, 315 { 316 name: "sequential with unreachable predecessor", 317 setup: func(b *builder) { 318 b0, unreachable, b2 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 319 insertJump(b, b0, b2) 320 insertJump(b, unreachable, b2) 321 unreachable.invalid = true 322 b.Seal(b0) 323 b.Seal(unreachable) 324 b.Seal(b2) 325 }, 326 exp: []BasicBlockID{0, 2}, 327 }, 328 { 329 name: "merge - no critical edge", 330 // 0 -> 1 -> 3 331 // | ^ 332 // v | 333 // 2 --------- 334 setup: func(b *builder) { 335 b0, b1, b2, b3 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 336 b.SetCurrentBlock(b0) 337 c := b.AllocateInstruction().AsIconst32(0) 338 b.InsertInstruction(c) 339 insertBrz(b, b0, b2, c.Return()) 340 insertJump(b, b0, b1) 341 insertJump(b, b1, b3) 342 insertJump(b, b2, b3) 343 b.Seal(b0) 344 b.Seal(b1) 345 b.Seal(b2) 346 b.Seal(b3) 347 }, 348 exp: []BasicBlockID{0, 1, 2, 3}, 349 }, 350 { 351 name: "loop towards loop header in fallthrough", 352 // 0 353 // v 354 // 1<--+ 355 // | | <---- critical 356 // 2---+ 357 // v 358 // 3 359 // 360 // ==> 361 // 362 // 0 363 // v 364 // 1<---+ 365 // | | 366 // 2--->4 367 // v 368 // 3 369 setup: func(b *builder) { 370 b0, b1, b2, b3 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 371 insertJump(b, b0, b1) 372 insertJump(b, b1, b2) 373 b.SetCurrentBlock(b2) 374 c := b.AllocateInstruction().AsIconst32(0) 375 b.InsertInstruction(c) 376 insertBrz(b, b2, b1, c.Return()) 377 insertJump(b, b2, b3) 378 b.Seal(b0) 379 b.Seal(b1) 380 b.Seal(b2) 381 b.Seal(b3) 382 }, 383 // The trampoline 4 is placed right after 2, which is the hot path of the loop. 384 exp: []BasicBlockID{0, 1, 2, 4, 3}, 385 }, 386 { 387 name: "loop - towards loop header in conditional branch", 388 // 0 389 // v 390 // 1<--+ 391 // | | <---- critical 392 // 2---+ 393 // v 394 // 3 395 // 396 // ==> 397 // 398 // 0 399 // v 400 // 1<---+ 401 // | | 402 // 2--->4 403 // v 404 // 3 405 setup: func(b *builder) { 406 b0, b1, b2, b3 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 407 insertJump(b, b0, b1) 408 insertJump(b, b1, b2) 409 b.SetCurrentBlock(b2) 410 c := b.AllocateInstruction().AsIconst32(0) 411 b.InsertInstruction(c) 412 insertBrz(b, b2, b3, c.Return()) 413 insertJump(b, b2, b1) 414 b.Seal(b0) 415 b.Seal(b1) 416 b.Seal(b2) 417 b.Seal(b3) 418 }, 419 // The trampoline 4 is placed right after 2, which is the hot path of the loop. 420 exp: []BasicBlockID{0, 1, 2, 4, 3}, 421 }, 422 { 423 name: "loop with header is critical backward edge", 424 // 0 425 // v 426 // 1<--+ 427 // / | | 428 // 3 2 | <--- critical 429 // \ | | 430 // 4---+ 431 // v 432 // 5 433 // 434 // ==> 435 // 436 // 0 437 // v 438 // 1<----+ 439 // / | | 440 // 3 2 | 441 // \ | | 442 // 4---->6 443 // v 444 // 5 445 setup: func(b *builder) { 446 b0, b1, b2, b3, b4, b5 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), 447 b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 448 insertJump(b, b0, b1) 449 b.SetCurrentBlock(b0) 450 c1 := b.AllocateInstruction().AsIconst32(0) 451 b.InsertInstruction(c1) 452 insertBrz(b, b1, b2, c1.Return()) 453 insertJump(b, b1, b3) 454 insertJump(b, b3, b4) 455 insertJump(b, b2, b4) 456 b.SetCurrentBlock(b4) 457 c2 := b.AllocateInstruction().AsIconst32(0) 458 b.InsertInstruction(c2) 459 insertBrz(b, b4, b1, c2.Return()) 460 insertJump(b, b4, b5) 461 b.Seal(b0) 462 b.Seal(b1) 463 b.Seal(b2) 464 b.Seal(b3) 465 b.Seal(b4) 466 b.Seal(b5) 467 }, 468 // The trampoline 6 is placed right after 4, which is the hot path of the loop. 469 exp: []BasicBlockID{0, 1, 3, 2, 4, 6, 5}, 470 }, 471 { 472 name: "multiple critical edges", 473 // 0 474 // v 475 // +---1<--+ 476 // | v | <---- critical 477 // critical ---->| 2 --+ 478 // | | <-------- critical 479 // | v 480 // +-->3--->4 481 // 482 // ==> 483 // 484 // 0 485 // v 486 // +---1<---+ 487 // | v | 488 // 5 2 -->6 489 // | v 490 // | 7 491 // | v 492 // +-->3--->4 493 setup: func(b *builder) { 494 b0, b1, b2, b3, b4 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), 495 b.allocateBasicBlock(), b.allocateBasicBlock() 496 insertJump(b, b0, b1) 497 b.SetCurrentBlock(b1) 498 c1 := b.AllocateInstruction().AsIconst32(0) 499 b.InsertInstruction(c1) 500 insertBrz(b, b1, b2, c1.Return()) 501 insertJump(b, b1, b3) 502 503 b.SetCurrentBlock(b2) 504 c2 := b.AllocateInstruction().AsIconst32(0) 505 b.InsertInstruction(c2) 506 insertBrz(b, b2, b1, c2.Return()) 507 insertJump(b, b2, b3) 508 insertJump(b, b3, b4) 509 510 b.Seal(b0) 511 b.Seal(b1) 512 b.Seal(b2) 513 b.Seal(b3) 514 b.Seal(b4) 515 }, 516 exp: []BasicBlockID{ 517 0, 1, 518 // block 2 has loop header (1) as the conditional branch target, so it's inverted, 519 // and the split edge trampoline is placed right after 2 which is the hot path of the loop. 520 2, 6, 521 // Then the placement iteration goes to 3, which has two (5, 7) unplaced trampolines as predecessors, 522 // so they are placed before 3. 523 5, 7, 3, 524 // Then the final block. 525 4, 526 }, 527 }, 528 { 529 name: "brz with arg", 530 setup: func(b *builder) { 531 b0, b1, b2 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 532 p := b0.AddParam(b, TypeI32) 533 retval := b1.AddParam(b, TypeI32) 534 535 b.SetCurrentBlock(b0) 536 { 537 arg := b.AllocateInstruction().AsIconst32(1000).Insert(b).Return() 538 insertBrz(b, b0, b1, p, arg) 539 insertJump(b, b0, b2) 540 } 541 b.SetCurrentBlock(b1) 542 { 543 args := b.varLengthPool.Allocate(1) 544 args = args.Append(&b.varLengthPool, retval) 545 b.AllocateInstruction().AsReturn(args).Insert(b) 546 } 547 b.SetCurrentBlock(b2) 548 { 549 arg := b.AllocateInstruction().AsIconst32(1).Insert(b).Return() 550 insertJump(b, b2, b1, arg) 551 } 552 553 b.Seal(b0) 554 b.Seal(b1) 555 b.Seal(b2) 556 }, 557 exp: []BasicBlockID{0x0, 0x3, 0x1, 0x2}, 558 }, 559 { 560 name: "loop with output", 561 exp: []BasicBlockID{0x0, 0x2, 0x4, 0x1, 0x3, 0x6, 0x5}, 562 setup: func(b *builder) { 563 b.currentSignature = &Signature{Results: []Type{TypeI32}} 564 b0, b1, b2, b3 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 565 566 b.SetCurrentBlock(b0) 567 funcParam := b0.AddParam(b, TypeI32) 568 b2Param := b2.AddParam(b, TypeI32) 569 insertJump(b, b0, b2, funcParam) 570 571 b.SetCurrentBlock(b1) 572 { 573 returnParam := b1.AddParam(b, TypeI32) 574 insertJump(b, b1, b.returnBlk, returnParam) 575 } 576 577 b.SetCurrentBlock(b2) 578 { 579 c := b.AllocateInstruction().AsIconst32(100).Insert(b) 580 cmp := b.AllocateInstruction(). 581 AsIcmp(b2Param, c.Return(), IntegerCmpCondUnsignedLessThan). 582 Insert(b) 583 insertBrz(b, b2, b1, cmp.Return(), b2Param) 584 insertJump(b, b2, b3) 585 } 586 587 b.SetCurrentBlock(b3) 588 { 589 one := b.AllocateInstruction().AsIconst32(1).Insert(b) 590 minusOned := b.AllocateInstruction().AsIsub(b2Param, one.Return()).Insert(b) 591 c := b.AllocateInstruction().AsIconst32(150).Insert(b) 592 cmp := b.AllocateInstruction(). 593 AsIcmp(b2Param, c.Return(), IntegerCmpCondEqual). 594 Insert(b) 595 insertBrz(b, b3, b1, cmp.Return(), minusOned.Return()) 596 insertJump(b, b3, b2, minusOned.Return()) 597 } 598 599 b.Seal(b0) 600 b.Seal(b1) 601 b.Seal(b2) 602 b.Seal(b3) 603 }, 604 }, 605 } { 606 tc := tc 607 t.Run(tc.name, func(t *testing.T) { 608 b := NewBuilder().(*builder) 609 tc.setup(b) 610 611 b.runPreBlockLayoutPasses() // LayoutBlocks() must be called after RunPasses(). 612 b.runBlockLayoutPass() 613 var actual []BasicBlockID 614 for blk := b.BlockIteratorReversePostOrderBegin(); blk != nil; blk = b.BlockIteratorReversePostOrderNext() { 615 actual = append(actual, blk.(*basicBlock).id) 616 } 617 require.Equal(t, tc.exp, actual) 618 }) 619 } 620 }