Skip to content

Commit

Permalink
Initial support for ocamlfdo (#2768)
Browse files Browse the repository at this point in the history
Initial support for ocamlfdo

Co-authored-by: Rudi Grinberg <me@rgrinberg.com>
  • Loading branch information
rgrinberg authored Oct 30, 2019
2 parents e603cd7 + 865bfcc commit ee2bc8a
Show file tree
Hide file tree
Showing 29 changed files with 638 additions and 34 deletions.
55 changes: 47 additions & 8 deletions src/dune/context.ml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type t =
; kind : Kind.t
; profile : Profile.t
; merlin : bool
; fdo_target_exe : Path.t option
; for_host : t option
; implicit : bool
; build_dir : Path.Build.t
Expand Down Expand Up @@ -109,6 +110,7 @@ let to_dyn t : Dyn.t =
; ( "for_host"
, option Context_name.to_dyn (Option.map t.for_host ~f:(fun t -> t.name))
)
; ("fdo_target_exe", option path t.fdo_target_exe)
; ("build_dir", Path.Build.to_dyn t.build_dir)
; ("toplevel_path", option path t.toplevel_path)
; ("ocaml_bin", path t.ocaml_bin)
Expand Down Expand Up @@ -214,8 +216,40 @@ let ocamlfind_printconf_path ~env ~ocamlfind ~toolchain =
let+ l = Process.run_capture_lines ~env Strict ocamlfind args in
List.map l ~f:Path.of_filename_relative_to_initial_cwd

let check_fdo_support has_native ocfg ~name =
let version = Ocaml_version.of_ocaml_config ocfg in
let version_string = Ocaml_config.version_string ocfg in
let err () =
User_error.raise
[ Pp.textf
"fdo requires ocamlopt version >= 4.10, current version is %s \
(context: %s)"
(Context_name.to_string name)
version_string
]
in
if not has_native then err ();
if Ocaml_config.is_dev_version ocfg then
( (* Allows fdo to be invoked with any dev version of the compiler. This is
experimental and will be removed when ocamlfdo is fully integrated
into the toolchain. When using a dev version of ocamlopt that does not
support the required options, fdo builds will fail because the
compiler won't recongnize the options. Normals builds won't be
affected. *) )
else if not (Ocaml_version.supports_split_at_emit version) then
if not (Ocaml_version.supports_function_sections version) then
err ()
else
User_warning.emit
[ Pp.textf
"fdo requires ocamlopt version >= 4.10, current version %s has \
partial support. Some optimizations are disabled! (context: %s)"
(Context_name.to_string name)
version_string
]

let create ~(kind : Kind.t) ~path ~env ~env_nodes ~name ~merlin ~targets
~host_context ~host_toolchain ~profile =
~host_context ~host_toolchain ~profile ~fdo_target_exe =
let opam_var_cache = Table.create (module String) 128 in
( match kind with
| Opam { root = Some root; _ } -> Table.set opam_var_cache "root" root
Expand Down Expand Up @@ -438,12 +472,15 @@ let create ~(kind : Kind.t) ~path ~env ~env_nodes ~name ~merlin ~targets
; stdlib_dir
}
in
if Option.is_some fdo_target_exe then
check_fdo_support lib_config.has_native ocfg ~name;
let t =
{ name
; implicit
; kind
; profile
; merlin
; fdo_target_exe
; env_nodes
; for_host = host
; build_dir
Expand Down Expand Up @@ -557,9 +594,9 @@ let extend_paths t ~env =
let opam_config_var t var =
opam_config_var ~env:t.env ~cache:t.opam_var_cache var

let default ~merlin ~env_nodes ~env ~targets =
let default ~merlin ~env_nodes ~env ~targets ~fdo_target_exe =
let path = Env.path env in
create ~kind:Default ~path ~env ~env_nodes ~merlin ~targets
create ~kind:Default ~path ~env ~env_nodes ~merlin ~targets ~fdo_target_exe

let opam_version =
let res = ref None in
Expand All @@ -583,8 +620,8 @@ let opam_version =
res := Some future;
Fiber.Future.wait future

let create_for_opam ~root ~env ~env_nodes ~targets ~profile
~(switch : Context_name.t) ~name ~merlin ~host_context ~host_toolchain =
let create_for_opam ~root ~env ~env_nodes ~targets ~profile ~switch ~name
~merlin ~host_context ~host_toolchain ~fdo_target_exe =
let opam =
match Lazy.force opam with
| None -> Utils.program_not_found "opam" ~loc:None
Expand Down Expand Up @@ -634,7 +671,7 @@ let create_for_opam ~root ~env ~env_nodes ~targets ~profile
create
~kind:(Opam { root; switch })
~profile ~targets ~path ~env ~env_nodes ~name ~merlin ~host_context
~host_toolchain
~host_toolchain ~fdo_target_exe

let instantiate_context env (workspace : Workspace.t)
~(context : Workspace.Context.t) ~host_context =
Expand All @@ -652,6 +689,7 @@ let instantiate_context env (workspace : Workspace.t)
; toolchain
; paths
; loc = _
; fdo_target_exe
} ->
let merlin =
workspace.merlin_context = Some (Workspace.Context.name context)
Expand All @@ -666,7 +704,7 @@ let instantiate_context env (workspace : Workspace.t)
in
let env = extend_paths ~env paths in
default ~env ~env_nodes ~profile ~targets ~name ~merlin ~host_context
~host_toolchain
~host_toolchain ~fdo_target_exe
| Opam
{ base =
{ targets
Expand All @@ -677,14 +715,15 @@ let instantiate_context env (workspace : Workspace.t)
; toolchain
; paths
; loc = _
; fdo_target_exe
}
; switch
; root
; merlin
} ->
let env = extend_paths ~env paths in
create_for_opam ~root ~env_nodes ~env ~profile ~switch ~name ~merlin
~targets ~host_context ~host_toolchain:toolchain
~targets ~host_context ~host_toolchain:toolchain ~fdo_target_exe

let create ~env (workspace : Workspace.t) =
let rec contexts : t list Fiber.Once.t Context_name.Map.t Lazy.t =
Expand Down
3 changes: 3 additions & 0 deletions src/dune/context.mli
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ type t =
; profile : Profile.t
(** [true] if this context is used for the .merlin files *)
; merlin : bool
(** [Some path/to/foo.exe] if this contexts is for feedback-directed
optimization of target path/to/foo.exe *)
; fdo_target_exe : Path.t option
(** If this context is a cross-compilation context, you need another
context for building tools used for the compilation that run on the
host. *)
Expand Down
2 changes: 2 additions & 0 deletions src/dune/exe.ml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ let link_exe ~loc ~name ~(linkage : Linkage.t) ~cm_files ~link_time_code_gen
let exe = exe_path_from_name cctx ~name ~linkage in
let compiler = Option.value_exn (Context.compiler ctx mode) in
let top_sorted_cms = Cm_files.top_sorted_cms cm_files ~mode:linkage.mode in
let fdo_linker_script = Fdo.Linker_script.create cctx (Path.build exe) in
SC.add_rule sctx ~loc ~dir
~mode:
( match promote with
Expand Down Expand Up @@ -166,6 +167,7 @@ let link_exe ~loc ~name ~(linkage : Linkage.t) ~cm_files ~link_time_code_gen
])
; Deps o_files
; Dyn (Build.map top_sorted_cms ~f:(fun x -> Command.Args.Deps x))
; Fdo.Linker_script.flags fdo_linker_script
])

let link_js ~name ~cm_files ~promote cctx =
Expand Down
211 changes: 211 additions & 0 deletions src/dune/fdo.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
open! Stdune
module CC = Compilation_context

type phase =
| All
| Compile
| Emit

let linear_ext = ".cmir-linear"

let linear_fdo_ext = linear_ext ^ "-fdo"

let fdo_profile s = Path.extend_basename s ~suffix:".fdo-profile"

let linker_script s = Path.extend_basename s ~suffix:".linker-script"

let phase_flags = function
| None -> []
| Some All -> [ "-g"; "-function-sections" ]
| Some Compile ->
[ "-g"; "-stop-after"; "scheduling"; "-save-ir-after"; "scheduling" ]
| Some Emit -> [ "-g"; "-start-from"; "emit"; "-function-sections" ]

(* CR-soon gyorsh: add a rule to use profile with c/cxx profile if available,
similarly to opt_rule for ocaml modules. The profile will have to be
generated externally from perf data for example using google's autofdo
toolset: create_gcov for gcc or create_llvm_prof for llvm. *)
let c_flags (ctx : Context.t) =
match ctx.fdo_target_exe with
| None -> []
| Some _ -> [ "-ffunction-sections" ]

let cxx_flags = c_flags

(* Location of ocamlfdo binary tool is independent of the module, but may
depend on the context. *)
let ocamlfdo_binary sctx dir =
Super_context.resolve_program sctx ~dir ~loc:None "ocamlfdo"
~hint:"try: opam install ocamlfdo"

(* FDO flags are context dependent. *)
let get_flags var =
let f (ctx : Context.t) =
Env.get ctx.env var |> Option.value ~default:""
|> String.extract_blank_separated_words
in
let memo =
Memo.create_hidden var
~doc:(sprintf "parse %s environment variable in context" var)
~input:(module Context)
Sync f
in
Memo.exec memo

let ocamlfdo_flags = get_flags "OCAMLFDO_FLAGS"

module Mode = struct
type t =
| If_exists
| Always
| Never

let to_string = function
| If_exists -> "if-exists"
| Always -> "always"
| Never -> "never"

let default = If_exists

let all = [ If_exists; Never; Always ]

let var = "OCAMLFDO_USE_PROFILE"

let of_context (ctx : Context.t) =
match Env.get ctx.env var with
| None -> default
| Some v -> (
match List.find_opt ~f:(fun s -> String.equal v (to_string s)) all with
| Some v -> v
| None ->
User_error.raise
[ Pp.textf
"Failed to parse environment variable: %s=%s\n\
Permitted values: if-exists always never\n\
Default: %s"
var v (to_string default)
] )
end

let get_profile =
(* The dependency on the existence of profile file in source should be
detected automatically by Memo. *)
let f (ctx : Context.t) =
let path = ctx.fdo_target_exe |> Option.value_exn |> fdo_profile in
let profile_exists =
Memo.lazy_ (fun () ->
path |> Path.as_in_source_tree
|> Option.map ~f:File_tree.file_exists
|> Option.value ~default:false)
in
let use_profile =
match Mode.of_context ctx with
| If_exists -> Memo.Lazy.force profile_exists
| Always ->
if Memo.Lazy.force profile_exists then
true
else
User_error.raise
[ Pp.textf "%s=%s but profile file %s does not exist." Mode.var
(Mode.to_string Always) (Path.to_string path)
]
| Never -> false
in
if use_profile then
Some path
else
None
in
let memo =
Memo.create_hidden Mode.var
~doc:
(sprintf "use profile based on %s environment variable in context"
Mode.var)
~input:(module Context)
Sync f
in
Memo.exec memo

let opt_rule cctx m =
let sctx = CC.super_context cctx in
let ctx = CC.context cctx in
let dir = CC.dir cctx in
let obj_dir = CC.obj_dir cctx in
let linear = Obj_dir.Module.obj_file obj_dir m ~kind:Cmx ~ext:linear_ext in
let linear_fdo =
Obj_dir.Module.obj_file obj_dir m ~kind:Cmx ~ext:linear_fdo_ext
in
let flags () =
let open Command.Args in
match get_profile ctx with
| Some fdo_profile_path ->
S
[ A "-fdo-profile"
; Dep fdo_profile_path
; As [ "-md5-unit"; "-reorder-blocks"; "opt"; "-q" ]
]
| None -> As [ "-md5-unit"; "-extra-debug"; "-q" ]
in
Super_context.add_rule sctx ~dir
(Command.run ~dir:(Path.build dir) (ocamlfdo_binary sctx dir)
[ A "opt"
; Hidden_targets [ linear_fdo ]
; Dep (Path.build linear)
; As (ocamlfdo_flags ctx)
; Dyn (Build.delayed flags)
])

module Linker_script = struct
type t = Path.t option

let ocamlfdo_linker_script_flags = get_flags "OCAMLFDO_LINKER_SCRIPT_FLAGS"

let linker_script_rule cctx fdo_target_exe =
let sctx = CC.super_context cctx in
let ctx = CC.context cctx in
let dir = CC.dir cctx in
let linker_script = linker_script fdo_target_exe in
let linker_script_path =
Path.Build.(relative ctx.build_dir (Path.to_string linker_script))
in
let flags () =
let open Command.Args in
match get_profile ctx with
| Some fdo_profile_path -> S [ A "-fdo-profile"; Dep fdo_profile_path ]
| None -> As []
in
Super_context.add_rule sctx ~dir
(Command.run ~dir:(Path.build ctx.build_dir) (ocamlfdo_binary sctx dir)
[ A "linker-script"
; A "-o"
; Target linker_script_path
; Dyn (Build.delayed flags)
; A "-q"
; As (ocamlfdo_linker_script_flags ctx)
]);
linker_script

let create cctx name =
let ctx = CC.context cctx in
match ctx.fdo_target_exe with
| None -> None
| Some fdo_target_exe ->
if
Path.equal name fdo_target_exe
&& ( Ocaml_version.supports_function_sections ctx.version
|| Ocaml_config.is_dev_version ctx.ocaml_config )
then
Some (linker_script_rule cctx fdo_target_exe)
else
None

let flags t =
let open Command.Args in
match t with
| None -> As []
| Some linker_script ->
S
[ A "-ccopt"
; Concat ("", [ A "-Xlinker --script="; Dep linker_script ])
]
end
Loading

0 comments on commit ee2bc8a

Please sign in to comment.