WebAssembly / WebAssembly/binaryen

O2 regression in optimize-instructions due to coalesceLocals

Open
#7,458 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
WebAssembly
Stars
8.6k
Forks
885
Avg merge
1d 19h
Merged PRs (30d)
69

Description

Given the following code:

(module
  (import "External" "external_function" (func $external_function))
  (func $_start
    (local $0 i32) (local $3 i32) (local $5 i32) (local $6 i32) (local $11 i32) (local $1 i32) (local $2 i32) (local $7 i32)
    loop ;; label = @1
      block ;; label = @2
        i32.const 0
        i32.load
        br_if 0 (;@2;)
        i32.const 0
        local.get $11
        i32.store
        i32.const 0
        local.set $5
        i32.const 1
        local.set $0
        i32.const 0
        local.get $0
        i32.store
        i32.const -346457217
        local.set $6
        block ;; label = @3
          local.get $0
          local.get $6
          i32.ne
          br_if 0 (;@3;)
          call $external_function
        end
        local.get $5
        i32.const 0
        i32.load
        i32.lt_s
        i32.const 0
        local.set $1
        local.get $1
        i32.shl
        drop
        local.get $1
        unreachable
      end
      i32.const 0
      i32.load
      local.set $2
      local.get $2
      local.set $3
      local.get $2
      local.get $3
      i32.store
      br 0 (;@1;)
    end)
  (memory $0 1)
  (export "_start" (func $_start)))

wasm-opt (16dbac1) can eliminate the dead br_if body by -all -O1 but cannot by -all -O2.

Analysis

The direct issue is that optimize-instructions cannot deduce the condition to zero in -all -O2 while it can in -all -O1. (further the dead if statement body will be eliminated by --vacuum)

Before optimize-instructions, that is precompute:

`-all -O1`, after `precompute`, going to `optimize-instructions`
(module
  (type (;0;) (func))
  (import "External" "external_functional" (func (;0;) (type 0)))
  (func (;1;) (type 0)
    (local i32 i32)
    loop  ;; label = @1
      i32.const 0
      i32.load
      i32.eqz
      if  ;; label = @2
        i32.const 0
        local.get 1
        i32.store
        i32.const 0
        i32.const 1
        local.tee 0
        i32.store
        local.get 0
        i32.const -346457217
        i32.eq
        if  ;; label = @3
          call 0
        end
        i32.const 0
        i32.load
        drop
        unreachable
      else
        i32.const 0
        i32.load
        local.tee 1 ;; Note this line: using variable 1, not affects condition
        local.get 1
        i32.store
        br 1 (;@1;)
      end
      unreachable
    end
    unreachable)
  (memory (;0;) 1)
  (export "_start" (func 1)))
`-all -O2`, after `precompute`, going to `optimize-instructions`
(module
  (type (;0;) (func))
  (import "External" "external_function" (func (;0;) (type 0)))
  (func (;1;) (type 0)
    (local i32 i32)
    loop  ;; label = @1
      i32.const 0
      i32.load
      i32.eqz
      if  ;; label = @2
        i32.const 0
        local.get 1
        i32.store
        i32.const 0
        i32.const 1
        local.tee 0
        i32.store
        local.get 0
        i32.const -346457217
        i32.eq
        if  ;; label = @3
          call 0
        end
        i32.const 0
        i32.load
        unreachable
      else
        i32.const 0
        i32.load
        local.tee 0 ;; Note this line: using variable 0 will affect condition deduction
        local.get 0
        i32.store
        br 1 (;@1;)
      end
      unreachable
    end
    unreachable)
  (memory (;0;) 1)
  (export "_start" (func 1)))

As you can see, the condition are the same:

(if
 (i32.eq
  (local.get $0) 
  (i32.const -346457217)
 )
 (then
  (call $external_functional)
 )
)

However, the statements in else statements are slightly different, but critical: -all -O1 uses local variable 1, while -all -O2 uses local variable 0 (local.tee 0), which I thinks maybe affect the deduction of the condition in optimize-instructions .

Continue to trace back and analyze in the optimized pipeline, the slightly but critical difference begins in coalesce-locals, which is the optimizations of Key "register allocation" pass. Does a live range analysis and then reuses locals in order to minimize their number, as well as to remove copies between them.

Although the coalesce-locals performs aggressive optimizations, I think there is missed optimization in the optimize-instructions, for it should have deduced the condition to be 0.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce the WebAssembly example with wasm-opt -all -O1 and -all -O2, then compare the output after precompute, coalesce-locals, and optimize-instructions. Trace how local.tee 0 versus local.tee 1 affects condition deduction. Done means optimize-instructions recognizes the condition as zero at O2 and eliminates the dead br_if body without changing behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
wasm
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.