SCons: Refactor LTO options with `lto=<none|thin|full>`

Adds support for LTO on macOS and Android. We don't have much experience
with LTO on these platforms so for now we keep it disabled by default
even when `production=yes` is set.

Similarly for iOS where we ship object files for the user to link in
Xcode so LTO makes builds extremely slow to link.

`production=yes` defaults to full LTO.
ThinLTO is much faster for LLVM-based compilers but seems to produce
bigger binaries (at least for the Web platform).
This commit is contained in:
Rémi Verschelde 2022-07-21 15:15:54 +02:00
parent 69233093d7
commit c2c659db32
7 changed files with 94 additions and 60 deletions

View File

@ -170,7 +170,7 @@ opts.Add(EnumVariable("arch", "CPU architecture", "auto", ["auto"] + architectur
opts.Add(EnumVariable("float", "Floating-point precision", "32", ("32", "64"))) opts.Add(EnumVariable("float", "Floating-point precision", "32", ("32", "64")))
opts.Add(EnumVariable("optimize", "Optimization type", "speed", ("speed", "size", "none"))) opts.Add(EnumVariable("optimize", "Optimization type", "speed", ("speed", "size", "none")))
opts.Add(BoolVariable("production", "Set defaults to build Godot for use in production", False)) opts.Add(BoolVariable("production", "Set defaults to build Godot for use in production", False))
opts.Add(BoolVariable("use_lto", "Use link-time optimization", False)) opts.Add(EnumVariable("lto", "Link-time optimization (for production buids)", "none", ("none", "thin", "full")))
# Components # Components
opts.Add(BoolVariable("deprecated", "Enable compatibility code for deprecated and removed features", True)) opts.Add(BoolVariable("deprecated", "Enable compatibility code for deprecated and removed features", True))
@ -438,35 +438,6 @@ if selected_platform in platform_list:
) )
env.SetOption("num_jobs", safer_cpu_count) env.SetOption("num_jobs", safer_cpu_count)
# 'dev' and 'production' are aliases to set default options if they haven't been set
# manually by the user.
if env["dev"]:
env["verbose"] = methods.get_cmdline_bool("verbose", True)
env["warnings"] = ARGUMENTS.get("warnings", "extra")
env["werror"] = methods.get_cmdline_bool("werror", True)
if env["tools"]:
env["tests"] = methods.get_cmdline_bool("tests", True)
if env["production"]:
env["use_static_cpp"] = methods.get_cmdline_bool("use_static_cpp", True)
env["use_lto"] = methods.get_cmdline_bool("use_lto", True)
env["debug_symbols"] = methods.get_cmdline_bool("debug_symbols", False)
if not env["tools"] and env["target"] == "debug":
print(
"WARNING: Requested `production` build with `tools=no target=debug`, "
"this will give you a full debug template (use `target=release_debug` "
"for an optimized template with debug features)."
)
if env.msvc:
print(
"WARNING: For `production` Windows builds, you should use MinGW with GCC "
"or Clang instead of Visual Studio, as they can better optimize the "
"GDScript VM in a very significant way. MSVC LTO also doesn't work "
"reliably for our use case."
"If you want to use MSVC nevertheless for production builds, set "
"`debug_symbols=no use_lto=no` instead of the `production=yes` option."
)
Exit(255)
env.extra_suffix = "" env.extra_suffix = ""
if env["extra_suffix"] != "": if env["extra_suffix"] != "":
@ -517,6 +488,37 @@ if selected_platform in platform_list:
# We apply it to CCFLAGS (both C and C++ code) in case it impacts C features. # We apply it to CCFLAGS (both C and C++ code) in case it impacts C features.
env.Prepend(CCFLAGS=["/std:c++17"]) env.Prepend(CCFLAGS=["/std:c++17"])
# 'dev' and 'production' are aliases to set default options if they haven't been set
# manually by the user.
if env["dev"]:
env["verbose"] = methods.get_cmdline_bool("verbose", True)
env["warnings"] = ARGUMENTS.get("warnings", "extra")
env["werror"] = methods.get_cmdline_bool("werror", True)
if env["tools"]:
env["tests"] = methods.get_cmdline_bool("tests", True)
if env["production"]:
env["use_static_cpp"] = methods.get_cmdline_bool("use_static_cpp", True)
env["lto"] = ARGUMENTS.get("lto", "full")
env["debug_symbols"] = methods.get_cmdline_bool("debug_symbols", False)
if not env["tools"] and env["target"] == "debug":
print(
"WARNING: Requested `production` build with `tools=no target=debug`, "
"this will give you a full debug template (use `target=release_debug` "
"for an optimized template with debug features)."
)
if env.msvc:
print(
"WARNING: For `production` Windows builds, you should use MinGW with GCC "
"or Clang instead of Visual Studio, as they can better optimize the "
"GDScript VM in a very significant way. MSVC LTO also doesn't work "
"reliably for our use case."
"If you want to use MSVC nevertheless for production builds, set "
"`debug_symbols=no lto=none` instead of the `production=yes` option."
)
Exit(255)
if env["lto"] != "none":
print("Using LTO: " + env["lto"])
# Enforce our minimal compiler version requirements # Enforce our minimal compiler version requirements
cc_version = methods.get_compiler_version(env) or { cc_version = methods.get_compiler_version(env) or {
"major": None, "major": None,

View File

@ -47,6 +47,9 @@ def get_flags():
return [ return [
("arch", "arm64"), # Default for convenience. ("arch", "arm64"), # Default for convenience.
("tools", False), ("tools", False),
# Benefits of LTO for Android (size, performance) haven't been clearly established yet.
# So for now we override the default value which may be set when using `production=yes`.
("lto", "none"),
] ]
@ -132,6 +135,15 @@ def configure(env):
env.Append(CPPDEFINES=["_DEBUG"]) env.Append(CPPDEFINES=["_DEBUG"])
env.Append(CPPFLAGS=["-UNDEBUG"]) env.Append(CPPFLAGS=["-UNDEBUG"])
# LTO
if env["lto"] != "none":
if env["lto"] == "thin":
env.Append(CCFLAGS=["-flto=thin"])
env.Append(LINKFLAGS=["-flto=thin"])
else:
env.Append(CCFLAGS=["-flto"])
env.Append(LINKFLAGS=["-flto"])
# Compiler configuration # Compiler configuration
env["SHLIBSUFFIX"] = ".so" env["SHLIBSUFFIX"] = ".so"

View File

@ -39,6 +39,9 @@ def get_flags():
("arch", "arm64"), # Default for convenience. ("arch", "arm64"), # Default for convenience.
("tools", False), ("tools", False),
("use_volk", False), ("use_volk", False),
# Disable by default even if production is set, as it makes linking in Xcode
# on exports very slow and that's not what most users expect.
("lto", "none"),
] ]
@ -70,9 +73,14 @@ def configure(env):
env.Append(CCFLAGS=["-gdwarf-2", "-O0"]) env.Append(CCFLAGS=["-gdwarf-2", "-O0"])
env.Append(CPPDEFINES=["_DEBUG", ("DEBUG", 1)]) env.Append(CPPDEFINES=["_DEBUG", ("DEBUG", 1)])
if env["use_lto"]: # LTO
env.Append(CCFLAGS=["-flto"]) if env["lto"] != "none":
env.Append(LINKFLAGS=["-flto"]) if env["lto"] == "thin":
env.Append(CCFLAGS=["-flto=thin"])
env.Append(LINKFLAGS=["-flto=thin"])
else:
env.Append(CCFLAGS=["-flto"])
env.Append(LINKFLAGS=["-flto"])
## Compiler configuration ## Compiler configuration

View File

@ -31,7 +31,6 @@ def get_opts():
return [ return [
EnumVariable("linker", "Linker program", "default", ("default", "bfd", "gold", "lld", "mold")), EnumVariable("linker", "Linker program", "default", ("default", "bfd", "gold", "lld", "mold")),
BoolVariable("use_llvm", "Use the LLVM compiler", False), BoolVariable("use_llvm", "Use the LLVM compiler", False),
BoolVariable("use_thinlto", "Use ThinLTO (LLVM only, requires linker=lld, implies use_lto=yes)", False),
BoolVariable("use_static_cpp", "Link libgcc and libstdc++ statically for better portability", True), BoolVariable("use_static_cpp", "Link libgcc and libstdc++ statically for better portability", True),
BoolVariable("use_coverage", "Test Godot coverage", False), BoolVariable("use_coverage", "Test Godot coverage", False),
BoolVariable("use_ubsan", "Use LLVM/GCC compiler undefined behavior sanitizer (UBSAN)", False), BoolVariable("use_ubsan", "Use LLVM/GCC compiler undefined behavior sanitizer (UBSAN)", False),
@ -129,13 +128,6 @@ def configure(env):
else: else:
env.Append(LINKFLAGS=["-fuse-ld=%s" % env["linker"]]) env.Append(LINKFLAGS=["-fuse-ld=%s" % env["linker"]])
if env["use_thinlto"]:
if not env["use_llvm"] or env["linker"] != "lld":
print("ThinLTO is only compatible with LLVM and the LLD linker, use `use_llvm=yes linker=lld`.")
sys.exit(255)
else:
env["use_lto"] = True # ThinLTO implies LTO
if env["use_coverage"]: if env["use_coverage"]:
env.Append(CCFLAGS=["-ftest-coverage", "-fprofile-arcs"]) env.Append(CCFLAGS=["-ftest-coverage", "-fprofile-arcs"])
env.Append(LINKFLAGS=["-ftest-coverage", "-fprofile-arcs"]) env.Append(LINKFLAGS=["-ftest-coverage", "-fprofile-arcs"])
@ -178,8 +170,12 @@ def configure(env):
env.Append(CCFLAGS=["-fsanitize-recover=memory"]) env.Append(CCFLAGS=["-fsanitize-recover=memory"])
env.Append(LINKFLAGS=["-fsanitize=memory"]) env.Append(LINKFLAGS=["-fsanitize=memory"])
if env["use_lto"]: # LTO
if env["use_thinlto"]: if env["lto"] != "none":
if env["lto"] == "thin":
if not env["use_llvm"]:
print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.")
sys.exit(255)
env.Append(CCFLAGS=["-flto=thin"]) env.Append(CCFLAGS=["-flto=thin"])
env.Append(LINKFLAGS=["-flto=thin"]) env.Append(LINKFLAGS=["-flto=thin"])
elif not env["use_llvm"] and env.GetOption("num_jobs") > 1: elif not env["use_llvm"] and env.GetOption("num_jobs") > 1:

View File

@ -40,6 +40,9 @@ def get_flags():
return [ return [
("arch", detect_arch()), ("arch", detect_arch()),
("use_volk", False), ("use_volk", False),
# Benefits of LTO for macOS (size, performance) haven't been clearly established yet.
# So for now we override the default value which may be set when using `production=yes`.
("lto", "none"),
] ]
@ -166,6 +169,15 @@ def configure(env):
env["RANLIB"] = basecmd + "ranlib" env["RANLIB"] = basecmd + "ranlib"
env["AS"] = basecmd + "as" env["AS"] = basecmd + "as"
# LTO
if env["lto"] != "none":
if env["lto"] == "thin":
env.Append(CCFLAGS=["-flto=thin"])
env.Append(LINKFLAGS=["-flto=thin"])
else:
env.Append(CCFLAGS=["-flto"])
env.Append(LINKFLAGS=["-flto"])
if env["use_ubsan"] or env["use_asan"] or env["use_tsan"]: if env["use_ubsan"] or env["use_asan"] or env["use_tsan"]:
env.extra_suffix += ".san" env.extra_suffix += ".san"
env.Append(CCFLAGS=["-DSANITIZERS_ENABLED"]) env.Append(CCFLAGS=["-DSANITIZERS_ENABLED"])

View File

@ -31,7 +31,6 @@ def get_opts():
return [ return [
("initial_memory", "Initial WASM memory (in MiB)", 32), ("initial_memory", "Initial WASM memory (in MiB)", 32),
BoolVariable("use_assertions", "Use Emscripten runtime assertions", False), BoolVariable("use_assertions", "Use Emscripten runtime assertions", False),
BoolVariable("use_thinlto", "Use ThinLTO", False),
BoolVariable("use_ubsan", "Use Emscripten undefined behavior sanitizer (UBSAN)", False), BoolVariable("use_ubsan", "Use Emscripten undefined behavior sanitizer (UBSAN)", False),
BoolVariable("use_asan", "Use Emscripten address sanitizer (ASAN)", False), BoolVariable("use_asan", "Use Emscripten address sanitizer (ASAN)", False),
BoolVariable("use_lsan", "Use Emscripten leak sanitizer (LSAN)", False), BoolVariable("use_lsan", "Use Emscripten leak sanitizer (LSAN)", False),
@ -110,12 +109,13 @@ def configure(env):
env["ENV"] = os.environ env["ENV"] = os.environ
# LTO # LTO
if env["use_thinlto"]: if env["lto"] != "none":
env.Append(CCFLAGS=["-flto=thin"]) if env["lto"] == "thin":
env.Append(LINKFLAGS=["-flto=thin"]) env.Append(CCFLAGS=["-flto=thin"])
elif env["use_lto"]: env.Append(LINKFLAGS=["-flto=thin"])
env.Append(CCFLAGS=["-flto=full"]) else:
env.Append(LINKFLAGS=["-flto=full"]) env.Append(CCFLAGS=["-flto"])
env.Append(LINKFLAGS=["-flto"])
# Sanitizers # Sanitizers
if env["use_ubsan"]: if env["use_ubsan"]:

View File

@ -191,7 +191,6 @@ def get_opts():
), ),
BoolVariable("use_mingw", "Use the Mingw compiler, even if MSVC is installed.", False), BoolVariable("use_mingw", "Use the Mingw compiler, even if MSVC is installed.", False),
BoolVariable("use_llvm", "Use the LLVM compiler", False), BoolVariable("use_llvm", "Use the LLVM compiler", False),
BoolVariable("use_thinlto", "Use ThinLTO", False),
BoolVariable("use_static_cpp", "Link MinGW/MSVC C++ runtime libraries statically", True), BoolVariable("use_static_cpp", "Link MinGW/MSVC C++ runtime libraries statically", True),
BoolVariable("use_asan", "Use address sanitizer (ASAN)", False), BoolVariable("use_asan", "Use address sanitizer (ASAN)", False),
] ]
@ -449,7 +448,10 @@ def configure_msvc(env, vcvars_msvc_config):
## LTO ## LTO
if env["use_lto"]: if env["lto"] != "none":
if env["lto"] == "thin":
print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.")
sys.exit(255)
env.AppendUnique(CCFLAGS=["/GL"]) env.AppendUnique(CCFLAGS=["/GL"])
env.AppendUnique(ARFLAGS=["/LTCG"]) env.AppendUnique(ARFLAGS=["/LTCG"])
if env["progress"]: if env["progress"]:
@ -562,17 +564,19 @@ def configure_mingw(env):
if try_cmd("gcc-ranlib --version", env["mingw_prefix"], env["arch"]): if try_cmd("gcc-ranlib --version", env["mingw_prefix"], env["arch"]):
env["RANLIB"] = mingw_bin_prefix + "gcc-ranlib" env["RANLIB"] = mingw_bin_prefix + "gcc-ranlib"
if env["use_lto"]: if env["lto"] != "none":
if not env["use_llvm"] and env.GetOption("num_jobs") > 1: if env["lto"] == "thin":
if not env["use_llvm"]:
print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.")
sys.exit(255)
env.Append(CCFLAGS=["-flto=thin"])
env.Append(LINKFLAGS=["-flto=thin"])
elif not env["use_llvm"] and env.GetOption("num_jobs") > 1:
env.Append(CCFLAGS=["-flto"]) env.Append(CCFLAGS=["-flto"])
env.Append(LINKFLAGS=["-flto=" + str(env.GetOption("num_jobs"))]) env.Append(LINKFLAGS=["-flto=" + str(env.GetOption("num_jobs"))])
else: else:
if env["use_thinlto"]: env.Append(CCFLAGS=["-flto"])
env.Append(CCFLAGS=["-flto=thin"]) env.Append(LINKFLAGS=["-flto"])
env.Append(LINKFLAGS=["-flto=thin"])
else:
env.Append(CCFLAGS=["-flto"])
env.Append(LINKFLAGS=["-flto"])
env.Append(LINKFLAGS=["-Wl,--stack," + str(STACK_SIZE)]) env.Append(LINKFLAGS=["-Wl,--stack," + str(STACK_SIZE)])