WebAssembly / WebAssembly/binaryen

Inlining exported functions, -O vs -O3

Open
#1,492 5 comments 1 reaction 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

Having the following unoptimized module with an exported and an internal function suitable for inlining:

(module
 (type $iii (func (param i32 i32) (result i32)))
 (export "add" (func $exported_add))
 (export "test" (func $test))
 (func $exported_add (; 0 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
  (i32.add
   (get_local $0)
   (get_local $1)
  )
 )
 (func $internal_sub (; 1 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
  (i32.sub
   (get_local $0)
   (get_local $1)
  )
 )
 (func $test (; 2 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
  (i32.add
   (call $exported_add
    (get_local $0)
    (get_local $1)
   )
   (call $internal_sub
    (get_local $0)
    (get_local $1)
   )
  )
 )
)

When optimizing with default optimization levels, this produces:

(module
 (type $iii (func (param i32 i32) (result i32)))
 (export "add" (func $exported_add))
 (export "test" (func $test))
 (func $exported_add (; 0 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
  (i32.add
   (get_local $0)
   (get_local $1)
  )
 )
 (func $test (; 1 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
  (i32.add
   (call $exported_add ;; exported call is not inlined
    (get_local $0)
    (get_local $1)
   )
   (i32.sub ;; internal call is inlined
    (get_local $0)
    (get_local $1)
   )
  )
 )
)

Which appears to keep the call to the exported function for size reasons (it inlines it in -O3, see below), but it probably should also inline in -O because an i32.add should always be smaller than a call with an additional function index immediate - is this correct?

For comparison, this is what's produced with -O3:

(module
 (type $iii (func (param i32 i32) (result i32)))
 (export "add" (func $exported_add))
 (export "test" (func $test))
 (func $exported_add (; 0 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
  (i32.add
   (get_local $0)
   (get_local $1)
  )
 )
 (func $test (; 1 ;) (type $iii) (param $0 i32) (param $1 i32) (result i32)
  (i32.add
   (i32.add ;; inlined as well
    (get_local $0)
    (get_local $1)
   )
   (i32.sub
    (get_local $0)
    (get_local $1)
   )
  )
 )
)

Fwiw, it also doesn't inline this call in -O2 without a shrinklevel, which seems odd, considering that -O3s also doesn't inline it (maybe) for size reasons.

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

Use the WAT module in the issue as the reproduction and compare output at -O, -O2, -O3, and -O3s, focusing on calls to exported versus internal functions. Determine whether the differing inlining decisions are intentional and whether the size behavior is consistent; done means the behavior is explained or corrected with an appropriate regression test.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.