Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add --static-call-graph option for generating smaller binaries #55047

Draft
wants to merge 26 commits into
base: master
Choose a base branch
from

Conversation

JeffBezanson
Copy link
Sponsor Member

This adds a command line option --static-call-graph that builds images where code is only included if it is statically reachable from methods marked using the new function entrypoint. Compile-time errors are given for call sites that are too dynamic to allow trimming the call graph (however there is an unsafe option if you want to try building anyway to see what happens).

The PR has two other components. One is changes to Base that generally allow more code to be compiled in this mode. These changes will either be merged in separate PRs or moved to a separate part of the workflow (where we will build a custom system image for this purpose). The branch is set up this way to make it easy to check out and try the functionality.

The other component is everything in the juliac/ directory, which implements a compiler driver script based on this new option, along with some examples and tests. This will eventually become a package "app" that depends on PackageCompiler and provides a CLI for all of this stuff, so it will not be merged here. To try an example:

cd juliac
../julia juliac.jl --output-exe hello --trim exe_examples/hello_world.jl

When stripped the resulting executable is currently about 900kb on my machine.

Note the script calls this --trim, which will be the official name of this option (following similar functionality in .NET). It is not yet clear if this is identical to the internal --static-call-graph option, or if the latter is an implementation detail of the former.

Also includes a lot of work by @topolarity

@JeffBezanson JeffBezanson added the status:DO NOT MERGE Do not merge this PR! label Jul 5, 2024
@ViralBShah ViralBShah marked this pull request as draft July 5, 2024 23:14
@ViralBShah
Copy link
Member

ViralBShah commented Jul 6, 2024

➜  juliac git:(jb/gb/static-call-graph) ✗ ../julia juliac.jl --output-exe hello --trim exe_examples/hello_world.jl
ld: warning: ignoring duplicate libraries: '-ljulia'
ld: warning: reexported library with install name '@rpath/libunwind.1.dylib' found at '/Users/viral/julia/usr/lib/libunwind.1.0.dylib' couldn't be matched with any parent library and will be linked directly
run(`cc $(allflags) -o $outname -Wl,$(Base.Linking.WHOLE_ARCHIVE) $img_path -Wl,$(Base.Linking.NO_WHOLE_ARCHIVE) $init_path $(julia_libs)`) = Process(`cc -std=gnu11 -I/Users/viral/julia/usr/include/julia -fPIC -L/Users/viral/julia/usr/lib -L/Users/viral/julia/usr/lib/julia -Wl,-rpath,/Users/viral/julia/usr/lib -ljulia -o hello -Wl,-all_load /var/folders/qj/54cx7lcs2qv9z1b4f_110xb00000gn/T/jl_fPbFYU/img.a -Wl, /var/folders/qj/54cx7lcs2qv9z1b4f_110xb00000gn/T/jl_fPbFYU/init.a -ljulia -ljulia-internal`, ProcessExited(0))

1M binary on Mac M-series:

➜  juliac git:(jb/gb/static-call-graph) ✗ ls -l hello
-rwxr-xr-x  1 viral  staff  1082296 Jul  6 09:57 hello
➜  juliac git:(jb/gb/static-call-graph) ✗ ./hello
Hello, world!
➜  juliac git:(jb/gb/static-call-graph) ✗ otool -L hello
hello:
	@rpath/libjulia.1.12.dylib (compatibility version 1.12.0, current version 1.12.0)
	@rpath/libjulia-internal.dylib (compatibility version 1.12.0, current version 1.12.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1345.100.2)

topolarity and others added 3 commits July 8, 2024 01:35
…. hacks)

A lot of these changes will not be directly upstreamable, but this
sketches out the direction we'll have to go in to type-stabilize these
interfaces.
topolarity added a commit that referenced this pull request Jul 13, 2024
TypedCallable provides a wrapper for callable objects, with the following benefits:
    1. Enforced type-stability (for concrete AT/RT types)
    2. Fast calling convention (frequently < 10 ns / call)
    3. Normal Julia dispatch semantics (sees new Methods, etc.) + invoke_latest
    4. Pre-compilation support (including `--trim` compatibility)

It can be used like this:
```julia
callbacks = @TypedCallable{(::Int,::Int)->Bool}[]

register_callback!(callbacks, f::F) where {F<:Function} =
    push!(callbacks, @TypedCallable f(::Int,::Int)::Bool)

register_callback!(callbacks, (x,y)->(x == y))
register_callback!(callbacks, (x,y)->(x != y))

@Btime callbacks[rand(1:2)](1,1)
```

This is very similar to the existing `FunctionWrappers.jl`, but there
are a few key differences:
  - Better type support: TypedCallable supports the full range of Julia
    types (incl. Varargs), and it has access to all of Julia's "internal"
    calling conventions so calls are fast (and allocation-free) for a
    wider range of input types
  - Improved dispatch handling: The `@cfunction` functionality used by
    FunctionWrappers has several dispatch bugs, which cause wrappers to
    occasionally not see new Methods. These bugs are fixed (or soon to
    be fixed) for TypedCallable.
  - Pre-compilation support including for `juliac` / `--trim` (#55047)

Many of the improvements here are actually thanks to the `OpaqueClosure`
introduced by @Keno - This type just builds on top of OpaqueClosure to
provide an interface with Julia's usual dispatch semantics.
topolarity added a commit that referenced this pull request Jul 13, 2024
TypedCallable provides a wrapper for callable objects, with the following benefits:
    1. Enforced type-stability (for concrete AT/RT types)
    2. Fast calling convention (frequently < 10 ns / call)
    3. Normal Julia dispatch semantics (sees new Methods, etc.) + invoke_latest
    4. Pre-compilation support (including `--trim` compatibility)

It can be used like this:
```julia
callbacks = @TypedCallable{(::Int,::Int)->Bool}[]

register_callback!(callbacks, f::F) where {F<:Function} =
    push!(callbacks, @TypedCallable f(::Int,::Int)::Bool)

register_callback!(callbacks, (x,y)->(x == y))
register_callback!(callbacks, (x,y)->(x != y))

@Btime callbacks[rand(1:2)](1,1)
```

This is very similar to the existing `FunctionWrappers.jl`, but there
are a few key differences:
  - Better type support: TypedCallable supports the full range of Julia
    types (incl. Varargs), and it has access to all of Julia's "internal"
    calling conventions so calls are fast (and allocation-free) for a
    wider range of input types
  - Improved dispatch handling: The `@cfunction` functionality used by
    FunctionWrappers has several dispatch bugs, which cause wrappers to
    occasionally not see new Methods. These bugs are fixed (or soon to
    be fixed) for TypedCallable.
  - Pre-compilation support including for `juliac` / `--trim` (#55047)

Many of the improvements here are actually thanks to the `OpaqueClosure`
introduced by @Keno - This type just builds on top of OpaqueClosure to
provide an interface with Julia's usual dispatch semantics.
topolarity added a commit that referenced this pull request Jul 13, 2024
TypedCallable provides a wrapper for callable objects, with the following benefits:
    1. Enforced type-stability (for concrete AT/RT types)
    2. Fast calling convention (frequently < 10 ns / call)
    3. Normal Julia dispatch semantics (sees new Methods, etc.) + invoke_latest
    4. Pre-compilation support (including `--trim` compatibility)

It can be used like this:
```julia
callbacks = @TypedCallable{(::Int,::Int)->Bool}[]

register_callback!(callbacks, f::F) where {F<:Function} =
    push!(callbacks, @TypedCallable f(::Int,::Int)::Bool)

register_callback!(callbacks, (x,y)->(x == y))
register_callback!(callbacks, (x,y)->(x != y))

@Btime callbacks[rand(1:2)](1,1)
```

This is very similar to the existing `FunctionWrappers.jl`, but there
are a few key differences:
  - Better type support: TypedCallable supports the full range of Julia
    types (incl. Varargs), and it has access to all of Julia's "internal"
    calling conventions so calls are fast (and allocation-free) for a
    wider range of input types
  - Improved dispatch handling: The `@cfunction` functionality used by
    FunctionWrappers has several dispatch bugs, which cause wrappers to
    occasionally not see new Methods. These bugs are fixed (or soon to
    be fixed) for TypedCallable.
  - Pre-compilation support including for `juliac` / `--trim` (#55047)

Many of the improvements here are actually thanks to the `OpaqueClosure`
introduced by @Keno - This type just builds on top of OpaqueClosure to
provide an interface with Julia's usual dispatch semantics.
@topolarity topolarity mentioned this pull request Jul 13, 2024
3 tasks
topolarity added a commit that referenced this pull request Jul 13, 2024
TypedCallable provides a wrapper for callable objects, with the following benefits:
    1. Enforced type-stability (for concrete AT/RT types)
    2. Fast calling convention (frequently < 10 ns / call)
    3. Normal Julia dispatch semantics (sees new Methods, etc.) + invoke_latest
    4. Pre-compilation support (including `--trim` compatibility)

It can be used like this:
```julia
callbacks = @TypedCallable{(::Int,::Int)->Bool}[]

register_callback!(callbacks, f::F) where {F<:Function} =
    push!(callbacks, @TypedCallable f(::Int,::Int)::Bool)

register_callback!(callbacks, (x,y)->(x == y))
register_callback!(callbacks, (x,y)->(x != y))

@Btime callbacks[rand(1:2)](1,1)
```

This is very similar to the existing `FunctionWrappers.jl`, but there
are a few key differences:
  - Better type support: TypedCallable supports the full range of Julia
    types (incl. Varargs), and it has access to all of Julia's "internal"
    calling conventions so calls are fast (and allocation-free) for a
    wider range of input types
  - Improved dispatch handling: The `@cfunction` functionality used by
    FunctionWrappers has several dispatch bugs, which cause wrappers to
    occasionally not see new Methods. These bugs are fixed (or soon to
    be fixed) for TypedCallable.
  - Pre-compilation support including for `juliac` / `--trim` (#55047)

Many of the improvements here are actually thanks to the `OpaqueClosure`
introduced by @Keno - This type just builds on top of OpaqueClosure to
provide an interface with Julia's usual dispatch semantics.
topolarity added a commit that referenced this pull request Jul 13, 2024
TypedCallable provides a wrapper for callable objects, with the following benefits:
    1. Enforced type-stability (for concrete AT/RT types)
    2. Fast calling convention (frequently < 10 ns / call)
    3. Normal Julia dispatch semantics (sees new Methods, etc.) + invoke_latest
    4. Pre-compilation support (including `--trim` compatibility)

It can be used like this:
```julia
callbacks = @TypedCallable{(::Int,::Int)->Bool}[]

register_callback!(callbacks, f::F) where {F<:Function} =
    push!(callbacks, @TypedCallable f(::Int,::Int)::Bool)

register_callback!(callbacks, (x,y)->(x == y))
register_callback!(callbacks, (x,y)->(x != y))

@Btime callbacks[rand(1:2)](1,1)
```

This is very similar to the existing `FunctionWrappers.jl`, but there
are a few key differences:
  - Better type support: TypedCallable supports the full range of Julia
    types (incl. Varargs), and it has access to all of Julia's "internal"
    calling conventions so calls are fast (and allocation-free) for a
    wider range of input types
  - Improved dispatch handling: The `@cfunction` functionality used by
    FunctionWrappers has several dispatch bugs, which cause wrappers to
    occasionally not see new Methods. These bugs are fixed (or soon to
    be fixed) for TypedCallable.
  - Pre-compilation support including for `juliac` / `--trim` (#55047)

Many of the improvements here are actually thanks to the `OpaqueClosure`
introduced by @Keno - This type just builds on top of OpaqueClosure to
provide an interface with Julia's usual dispatch semantics.

Co-authored-by: Gabriel Baraldi <[email protected]>
@timholy
Copy link
Sponsor Member

timholy commented Jul 15, 2024

#55104 gets the lib example working

@MasonProtter
Copy link
Contributor

MasonProtter commented Jul 15, 2024

Let me know if issues should be raised elsewhere, but I ran into a problem where the compiled binaries don't seem to get ARGS. Here's a MWE:

# args.jl
module ArgsDemo

Base.@ccallable function main()::Cint
    ccall(:jl_, Cvoid, (Any,), ARGS)
    return 0
end

end
❯ ~/julia/./julia --startup=no ~/julia/juliac/juliac.jl --output-exe args --trim args.jl
run(`cc $(allflags) -o $outname -Wl,$(Base.Linking.WHOLE_ARCHIVE) $img_path -Wl,$(Base.Linking.NO_WHOLE_ARCHIVE) $init_path $(julia_libs)`) = Process(`cc -std=gnu11 -I/home/masonp/julia/usr/include/julia -fPIC -L/home/masonp/julia/usr/lib -Wl,--export-dynamic -L/home/masonp/julia/usr/lib/julia -Wl,-rpath,/home/masonp/julia/usr/lib -Wl,-rpath,/home/masonp/julia/usr/lib/julia -ljulia -o args -Wl,--whole-archive /tmp/jl_2vsSFl/img.a -Wl,--no-whole-archive /tmp/jl_2vsSFl/init.a -ljulia -ljulia-internal`, ProcessExited(0))

❯ ./args
Array{String, 1}(dims=(0,), mem=Memory{String}(8, 0x5589c120d310)[#<null>, #<null>, #<null>, #<null>, #<null>, #<null>, #<null>, #<null>])

❯ ./args arg1 arg2 agr3
Array{String, 1}(dims=(0,), mem=Memory{String}(8, 0x558aac9f6310)[#<null>, #<null>, #<null>, #<null>, #<null>, #<null>, #<null>, #<null>])

Note this is a PR against #55047

Co-authored by: Gabriel Baraldi <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status:DO NOT MERGE Do not merge this PR!
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants